From eaa1cb0ca32d978f2424c417fb10f4e622fb40cb Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 18 Sep 2025 00:02:44 -0400 Subject: [PATCH 1/9] acp: Add a basic test for ACP remoting (#38381) Tests that the downstream project can see custom agents configured in the remote server's settings, and that it constructs an appropriate `AgentServerCommand`. Release Notes: - N/A --- Cargo.lock | 2 + crates/remote_server/Cargo.toml | 2 + .../remote_server/src/remote_editing_tests.rs | 88 ++++++++++++++++++- crates/settings/src/settings_store.rs | 11 +++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 13acb75ee146f412f704ed7d2c209ac59928a3cc..294127b598926ddae6924f0af1dd273416cf1e1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13981,6 +13981,7 @@ dependencies = [ "clap", "client", "clock", + "collections", "crash-handler", "crashes", "dap", @@ -14009,6 +14010,7 @@ dependencies = [ "minidumper", "node_runtime", "paths", + "pretty_assertions", "project", "proto", "release_channel", diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 59b2af0f410c98385cf13a271833980aacd6c8bc..b1f12fb0a8133b95259b38bbec22dbd031937cd7 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -77,6 +77,7 @@ assistant_tool.workspace = true assistant_tools.workspace = true client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } +collections.workspace = true dap = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } @@ -85,6 +86,7 @@ gpui = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } node_runtime = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } remote = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index c0ccaf900d18ee176bab7193c2bfb65b8555318d..cb486732c0a0a63e7f6d5d5aed7fe0499ef98b80 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,6 +6,7 @@ use assistant_tool::{Tool as _, ToolResultContent}; use assistant_tools::{ReadFileTool, ReadFileToolInput}; use client::{Client, UserStore}; use clock::FakeSystemClock; +use collections::{HashMap, HashSet}; use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; use extension::ExtensionHostProxy; @@ -20,6 +21,7 @@ use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, Language use node_runtime::NodeRuntime; use project::{ Project, ProjectPath, + agent_server_store::AgentServerCommand, search::{SearchQuery, SearchResult}, }; use remote::RemoteClient; @@ -27,7 +29,6 @@ use serde_json::json; use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; use smol::stream::StreamExt; use std::{ - collections::HashSet, path::{Path, PathBuf}, sync::Arc, }; @@ -1770,6 +1771,91 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu does_not_exist_result.output.await.unwrap_err(); } +#[gpui::test] +async fn test_remote_external_agent_server( + cx: &mut TestAppContext, + server_cx: &mut TestAppContext, +) { + let fs = FakeFs::new(server_cx.executor()); + fs.insert_tree(path!("/project"), json!({})).await; + + let (project, _headless_project) = init_test(&fs, cx, server_cx).await; + project + .update(cx, |project, cx| { + project.find_or_create_worktree(path!("/project"), true, cx) + }) + .await + .unwrap(); + let names = project.update(cx, |project, cx| { + project + .agent_server_store() + .read(cx) + .external_agents() + .map(|name| name.to_string()) + .collect::>() + }); + pretty_assertions::assert_eq!(names, ["gemini", "claude"]); + server_cx.update_global::(|settings_store, cx| { + settings_store + .set_raw_server_settings( + Some(json!({ + "agent_servers": { + "foo": { + "command": "foo-cli", + "args": ["--flag"], + "env": { + "VAR": "val" + } + } + } + })), + cx, + ) + .unwrap(); + }); + server_cx.run_until_parked(); + cx.run_until_parked(); + let names = project.update(cx, |project, cx| { + project + .agent_server_store() + .read(cx) + .external_agents() + .map(|name| name.to_string()) + .collect::>() + }); + pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]); + let (command, root, login) = project + .update(cx, |project, cx| { + project.agent_server_store().update(cx, |store, cx| { + store + .get_external_agent(&"foo".into()) + .unwrap() + .get_command( + None, + HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]), + None, + None, + &mut cx.to_async(), + ) + }) + }) + .await + .unwrap(); + assert_eq!( + command, + AgentServerCommand { + path: "ssh".into(), + args: vec!["foo-cli".into(), "--flag".into()], + env: Some(HashMap::from_iter([ + ("VAR".into(), "val".into()), + ("OTHER_VAR".into(), "other-val".into()) + ])) + } + ); + assert_eq!(&PathBuf::from(root), paths::home_dir()); + assert!(login.is_none()); +} + pub async fn init_test( server_fs: &Arc, cx: &mut TestAppContext, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index abce84daf7cf2b0adc304894476d9c763e5e1a3d..e06e423e92aa3fe093c69d8436545d4a13d17a82 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -487,6 +487,17 @@ impl SettingsStore { Ok(()) } + /// Replaces current settings with the values from the given JSON. + pub fn set_raw_server_settings( + &mut self, + new_settings: Option, + cx: &mut App, + ) -> Result<()> { + self.raw_server_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 From 4b1e78cd5c4ed69a168409620a272fc63fa2f433 Mon Sep 17 00:00:00 2001 From: Miao Date: Thu, 18 Sep 2025 16:15:52 +0800 Subject: [PATCH 2/9] terminal: Fix COLORTERM regression for true color support (#38379) Closes #38304 Release Notes: - Fixed true color detection regression by setting `COLORTERM=truecolor` --- Reason: The regression is possibly introduced in [pr#36576: Inject venv environment via the toolchain](https://github.com/zed-industries/zed/pull/36576/files#diff-6f30387876b79f1de44f8193401d6c8fb49a2156479c4f2e32bc922ec5d54d76), where `alacritty_terminal::tty::setup_env();` is removed. The `alacritty_terminal::tty::setup_env();` does 2 things, which sets `TERM` & `COLORTERM` envvar. ```rs /// Setup environment variables. pub fn setup_env() { // Default to 'alacritty' terminfo if it is available, otherwise // default to 'xterm-256color'. May be overridden by user's config // below. let terminfo = if terminfo_exists("alacritty") { "alacritty" } else { "xterm-256color" }; unsafe { env::set_var("TERM", terminfo) }; // Advertise 24-bit color support. unsafe { env::set_var("COLORTERM", "truecolor") }; } ``` --- crates/terminal/src/terminal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a07aef5f7b4da90373bcbf7c406dd8277cb09387..0fce02a97b04484b5a91d6d43b456ecfb1f75f15 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -364,6 +364,7 @@ impl TerminalBuilder { env.insert("ZED_TERM".to_string(), "true".to_string()); env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); env.insert("TERM".to_string(), "xterm-256color".to_string()); + env.insert("COLORTERM".to_string(), "truecolor".to_string()); env.insert( "TERM_PROGRAM_VERSION".to_string(), release_channel::AppVersion::global(cx).to_string(), From d85a6db6a3f1115d60cea103d33944894a2b2d4c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 10:22:26 +0200 Subject: [PATCH 3/9] git_ui: Use margin instead of padding for blame entries (#38397) This makes the hover background change keep a visible border element between the gutter and blame entries Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/git_ui/src/blame_ui.rs | 139 ++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index ad5823c1674353f2e0531e5f71e1420fe464bfe6..6229c80c739ee73b20fe2640f30f9f751a1b4411 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -47,75 +47,84 @@ impl BlameRenderer for GitBlameRenderer { let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED); Some( - h_flex() - .w_full() - .justify_between() - .font_family(style.font().family) - .line_height(style.line_height) - .id(("blame", ix)) - .text_color(cx.theme().status().hint) - .pr_2() - .gap_2() + div() + .mr_2() .child( h_flex() - .items_center() + .w_full() + .justify_between() + .font_family(style.font().family) + .line_height(style.line_height) + .id(("blame", ix)) + .text_color(cx.theme().status().hint) .gap_2() - .child(div().text_color(sha_color).child(short_commit_id)) - .child(name), - ) - .child(relative_timestamp) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .cursor_pointer() - .on_mouse_down(MouseButton::Right, { - let blame_entry = blame_entry.clone(); - let details = details.clone(); - move |event, window, cx| { - deploy_blame_entry_context_menu( - &blame_entry, - details.as_ref(), - editor.clone(), - event.position, - window, - cx, - ); - } - }) - .on_click({ - let blame_entry = blame_entry.clone(); - let repository = repository.clone(); - let workspace = workspace.clone(); - move |_, window, cx| { - CommitView::open( - CommitSummary { - sha: blame_entry.sha.to_string().into(), - subject: blame_entry.summary.clone().unwrap_or_default().into(), - commit_timestamp: blame_entry.committer_time.unwrap_or_default(), - author_name: blame_entry - .committer_name - .clone() - .unwrap_or_default() - .into(), - has_parent: true, - }, - repository.downgrade(), - workspace.clone(), - window, - cx, - ) - } - }) - .hoverable_tooltip(move |_window, cx| { - cx.new(|cx| { - CommitTooltip::blame_entry( - &blame_entry, - details.clone(), - repository.clone(), - workspace.clone(), - cx, + .child( + h_flex() + .items_center() + .gap_2() + .child(div().text_color(sha_color).child(short_commit_id)) + .child(name), ) - }) - .into() - }) + .child(relative_timestamp) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .cursor_pointer() + .on_mouse_down(MouseButton::Right, { + let blame_entry = blame_entry.clone(); + let details = details.clone(); + move |event, window, cx| { + deploy_blame_entry_context_menu( + &blame_entry, + details.as_ref(), + editor.clone(), + event.position, + window, + cx, + ); + } + }) + .on_click({ + let blame_entry = blame_entry.clone(); + let repository = repository.clone(); + let workspace = workspace.clone(); + move |_, window, cx| { + CommitView::open( + CommitSummary { + sha: blame_entry.sha.to_string().into(), + subject: blame_entry + .summary + .clone() + .unwrap_or_default() + .into(), + commit_timestamp: blame_entry + .committer_time + .unwrap_or_default(), + author_name: blame_entry + .committer_name + .clone() + .unwrap_or_default() + .into(), + has_parent: true, + }, + repository.downgrade(), + workspace.clone(), + window, + cx, + ) + } + }) + .hoverable_tooltip(move |_window, cx| { + cx.new(|cx| { + CommitTooltip::blame_entry( + &blame_entry, + details.clone(), + repository.clone(), + workspace.clone(), + cx, + ) + }) + .into() + }), + ) .into_any(), ) } From ed46e2ca775b69126f6916534d4f027e51cae452 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Thu, 18 Sep 2025 09:47:15 +0100 Subject: [PATCH 4/9] helix: Apply modification (e.g. switch case) on a single character only in helix mode (#38119) Closes #34192 Without selection, only current character would be affected. Also if #38117 is merged too, then transformations in SelectMode behave correctly too and selection is not collapsed. Release Notes: - helix: Implemented `~`, `` ` ``, `` Alt-` `` correctly in normal and select modes --------- Co-authored-by: Jakub Konka --- assets/keymaps/vim.json | 5 ++--- crates/vim/src/normal/convert.rs | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8464e03d251afc166ac45a349894ecf2f7247944..817198659657814dcc597926d689063ae2182c78 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -442,9 +442,8 @@ ">": "vim::Indent", "<": "vim::Outdent", "=": "vim::AutoIndent", - "g u": "vim::PushLowercase", - "g shift-u": "vim::PushUppercase", - "g ~": "vim::PushOppositeCase", + "`": "vim::ConvertToLowerCase", + "alt-`": "vim::ConvertToUpperCase", "g q": "vim::PushRewrap", "g w": "vim::PushRewrap", "insert": "vim::InsertBefore", diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index d5875fe963778756daf5cd78f452b3436b53642f..11d040850d341155bf428ebc337cc9e3f4cc42c3 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -214,11 +214,10 @@ impl Vim { Mode::HelixNormal | Mode::HelixSelect => { if selection.is_empty() { - // Handle empty selection by operating on the whole word - let (word_range, _) = snapshot.surrounding_word(selection.start, false); - let word_start = snapshot.offset_to_point(word_range.start); - let word_end = snapshot.offset_to_point(word_range.end); - ranges.push(word_start..word_end); + // Handle empty selection by operating on single character + let start = selection.start; + let end = snapshot.clip_point(start + Point::new(0, 1), Bias::Right); + ranges.push(start..end); cursor_positions.push(selection.start..selection.start); } else { ranges.push(selection.start..selection.end); @@ -445,15 +444,26 @@ mod test { cx.simulate_keystrokes("~"); cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal); - // Cursor-only (empty) selection + // Cursor-only (empty) selection - switch case cx.set_state("The ˇquick brown", Mode::HelixNormal); cx.simulate_keystrokes("~"); - cx.assert_state("The ˇQUICK brown", Mode::HelixNormal); + cx.assert_state("The ˇQuick brown", Mode::HelixNormal); + cx.simulate_keystrokes("~"); + cx.assert_state("The ˇquick brown", Mode::HelixNormal); + + // Cursor-only (empty) selection - switch to uppercase and lowercase explicitly + cx.set_state("The ˇquick brown", Mode::HelixNormal); + cx.simulate_keystrokes("alt-`"); + cx.assert_state("The ˇQuick brown", Mode::HelixNormal); + cx.simulate_keystrokes("`"); + cx.assert_state("The ˇquick brown", Mode::HelixNormal); // With `e` motion (which extends selection to end of word in Helix) cx.set_state("The ˇquick brown fox", Mode::HelixNormal); cx.simulate_keystrokes("e"); cx.simulate_keystrokes("~"); cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal); + + // Cursor-only } } From 32c868ff7d50f4163072c72bc2a09cf710e7b521 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 18 Sep 2025 11:38:59 +0200 Subject: [PATCH 5/9] acp: Fix behavior of read_text_file for ACP agents (#38401) We were incorrectly handling the line number as well as stripping out line breaks when returning portions of files. It also makes sure following is updated even when we load a snapshot from cache, which wasn't the case before. We also are able to load the text via a range in the snapshot, rather than allocating a string for the entire file and then another after iterating over lines in the file. Release Notes: - acp: Fix incorrect behavior when ACP agents requested to read portions of files. --- crates/acp_thread/src/acp_thread.rs | 140 +++++++++++++++++++++------- 1 file changed, 106 insertions(+), 34 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 68e5266f06aa8bddfaa252bdc1cf5b21891c7f10..f2327ca70b104de12f44d74aacd1a5a2bb1eca3b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1781,6 +1781,9 @@ impl AcpThread { reuse_shared_snapshot: bool, cx: &mut Context, ) -> Task> { + // Args are 1-based, move to 0-based + let line = line.unwrap_or_default().saturating_sub(1); + let limit = limit.unwrap_or(u32::MAX); let project = self.project.clone(); let action_log = self.action_log.clone(); cx.spawn(async move |this, cx| { @@ -1808,44 +1811,37 @@ impl AcpThread { action_log.update(cx, |action_log, cx| { action_log.buffer_read(buffer.clone(), cx); })?; - project.update(cx, |project, cx| { - let position = buffer - .read(cx) - .snapshot() - .anchor_before(Point::new(line.unwrap_or_default(), 0)); - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }), - cx, - ); - })?; - buffer.update(cx, |buffer, _| buffer.snapshot())? + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; + this.update(cx, |this, _| { + this.shared_buffers.insert(buffer.clone(), snapshot.clone()); + })?; + snapshot }; - this.update(cx, |this, _| { - let text = snapshot.text(); - this.shared_buffers.insert(buffer.clone(), snapshot); - if line.is_none() && limit.is_none() { - return Ok(text); - } - let limit = limit.unwrap_or(u32::MAX) as usize; - let Some(line) = line else { - return Ok(text.lines().take(limit).collect::()); - }; + let max_point = snapshot.max_point(); + if line >= max_point.row { + anyhow::bail!( + "Attempting to read beyond the end of the file, line {}:{}", + max_point.row + 1, + max_point.column + ); + } - let count = text.lines().count(); - if count < line as usize { - anyhow::bail!("There are only {} lines", count); - } - Ok(text - .lines() - .skip(line as usize + 1) - .take(limit) - .collect::()) - })? + let start = snapshot.anchor_before(Point::new(line, 0)); + let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0)); + + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: start, + }), + cx, + ); + })?; + + Ok(snapshot.text_for_range(start..end).collect::()) }) } @@ -2391,6 +2387,82 @@ mod tests { request.await.unwrap(); } + #[gpui::test] + async fn test_reading_from_line(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\nfour\n"})) + .await; + let project = Project::test(fs.clone(), [], cx).await; + project + .update(cx, |project, cx| { + project.find_or_create_worktree(path!("/tmp/foo"), true, cx) + }) + .await + .unwrap(); + + let connection = Rc::new(FakeAgentConnection::new()); + + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) + .await + .unwrap(); + + // Whole file + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "one\ntwo\nthree\nfour\n"); + + // Only start line + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), Some(3), None, false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "three\nfour\n"); + + // Only limit + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "one\ntwo\n"); + + // Range + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), Some(2), Some(2), false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "two\nthree\n"); + + // Invalid + let err = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), Some(5), Some(2), false, cx) + }) + .await + .unwrap_err(); + + assert_eq!( + err.to_string(), + "Attempting to read beyond the end of the file, line 5:0" + ); + } + #[gpui::test] async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) { init_test(cx); From 9f9e8063fccfaebd5a3c8939c87ef206e0bdca58 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 12:03:35 +0200 Subject: [PATCH 6/9] workspace: Pop a toast if manually spawning a task fails (#38405) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/workspace/src/tasks.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 71394c874ae988d7b8fef3e3a224d25e1c290640..5f52cb49e74d67619b9ba7c033a33fe8a7ad51c8 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -8,7 +8,7 @@ use remote::ConnectionState; use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; use ui::Window; -use crate::Workspace; +use crate::{Toast, Workspace, notifications::NotificationId}; impl Workspace { pub fn schedule_task( @@ -73,8 +73,10 @@ impl Workspace { if let Some(terminal_provider) = self.terminal_provider.as_ref() { let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx); - let task = cx.background_spawn(async move { - match task_status.await { + + let task = cx.spawn(async |w, cx| { + let res = cx.background_spawn(task_status).await; + match res { Some(Ok(status)) => { if status.success() { log::debug!("Task spawn succeeded"); @@ -82,9 +84,15 @@ impl Workspace { log::debug!("Task spawn failed, code: {:?}", status.code()); } } - Some(Err(e)) => log::error!("Task spawn failed: {e:#}"), + Some(Err(e)) => { + log::error!("Task spawn failed: {e:#}"); + _ = w.update(cx, |w, cx| { + let id = NotificationId::unique::(); + w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx); + }) + } None => log::debug!("Task spawn got cancelled"), - } + }; }); self.scheduled_tasks.push(task); } From ca05ff89f434319f00c7d99a030bfd767d6edb39 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 18 Sep 2025 12:05:05 +0200 Subject: [PATCH 7/9] agent2: More efficent read file tool (#38407) Before we were always reading the entire file into memory as a string. Now we only read the range that is actually requested. Release Notes: - N/A --- crates/agent2/src/tools/read_file_tool.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 87163e769c26b0cee053fcf149d047fc451c470f..7be83d8bef174bf9b2799d67eb6240fcae4e5bb6 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -201,7 +201,6 @@ impl AgentTool for ReadFileTool { // Check if specific line ranges are provided let result = if input.start_line.is_some() || input.end_line.is_some() { let result = buffer.read_with(cx, |buffer, _cx| { - let text = buffer.text(); // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. let start = input.start_line.unwrap_or(1).max(1); let start_row = start - 1; @@ -210,13 +209,13 @@ impl AgentTool for ReadFileTool { anchor = Some(buffer.anchor_before(Point::new(start_row, column))); } - let lines = text.split('\n').skip(start_row as usize); - if let Some(end) = input.end_line { - let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line - itertools::intersperse(lines.take(count as usize), "\n").collect::() - } else { - itertools::intersperse(lines, "\n").collect::() + let mut end_row = input.end_line.unwrap_or(u32::MAX); + if end_row <= start_row { + end_row = start_row + 1; // read at least one lines } + let start = buffer.anchor_before(Point::new(start_row, 0)); + let end = buffer.anchor_before(Point::new(end_row, 0)); + buffer.text_for_range(start..end).collect::() })?; action_log.update(cx, |log, cx| { @@ -445,7 +444,7 @@ mod test { tool.run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into()); + assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4\n".into()); } #[gpui::test] @@ -475,7 +474,7 @@ mod test { tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 1\nLine 2".into()); + assert_eq!(result.unwrap(), "Line 1\nLine 2\n".into()); // end_line of 0 should result in at least 1 line let result = cx @@ -488,7 +487,7 @@ mod test { tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 1".into()); + assert_eq!(result.unwrap(), "Line 1\n".into()); // when start_line > end_line, should still return at least 1 line let result = cx @@ -501,7 +500,7 @@ mod test { tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 3".into()); + assert_eq!(result.unwrap(), "Line 3\n".into()); } fn init_test(cx: &mut TestAppContext) { From 59a609c9fcebd3f641e64a498e15e3cf2042a648 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 12:06:43 +0200 Subject: [PATCH 8/9] Partially revert "project: Fix terminal activation scripts failing on Windows for new shells (#37986) (#38406) This partially reverts commit 4002602a8926b7fe799acf50fcee6bcffb36d376. Specifically the parts that closes https://github.com/zed-industries/zed/issues/38343 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/acp/thread_view.rs | 2 +- crates/terminal/src/terminal.rs | 8 ++--- crates/terminal_view/src/terminal_panel.rs | 38 ++++++++++++++++++++-- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8e8f908bc2eea651babb73749e26cb2d6474f74f..cf5284f643cfe3d58ff62a4fa549a84f0a62db69 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1591,7 +1591,7 @@ impl AcpThreadView { task.shell = shell; let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(login.clone(), window, cx) + terminal_panel.spawn_task(&login, window, cx) })?; let terminal = terminal.await?; diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0fce02a97b04484b5a91d6d43b456ecfb1f75f15..6bdeb9638a329c2384e538e27e13c21f02df7284 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -533,14 +533,10 @@ impl TerminalBuilder { child_exited: None, }; - 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(if cfg!(windows) { - &b"\r\n"[..] - } else { - &b"\n"[..] - }); + terminal.write_to_pty(b"\n"); } terminal.clear(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6c93644f992dcd5d3c0126a28c9aa8b8bab020d3..ef574728acdc06bb0db686a1cc9b4c8f8bc0bcce 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -19,7 +19,7 @@ use itertools::Itertools; use project::{Fs, Project, ProjectEntryId}; use search::{BufferSearchBar, buffer_search::DivRegistrar}; use settings::Settings; -use task::{RevealStrategy, RevealTarget, SpawnInTerminal, TaskId}; +use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId}; use terminal::{ Terminal, terminal_settings::{TerminalDockPosition, TerminalSettings}, @@ -521,10 +521,42 @@ impl TerminalPanel { pub fn spawn_task( &mut self, - task: SpawnInTerminal, + task: &SpawnInTerminal, window: &mut Window, cx: &mut Context, ) -> Task>> { + 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 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); + + let task = SpawnInTerminal { + command_label, + command: Some(command), + args, + ..task.clone() + }; + if task.allow_concurrent_runs && task.use_new_terminal { return self.spawn_in_new_terminal(task, window, cx); } @@ -1558,7 +1590,7 @@ impl workspace::TerminalProvider for TerminalProvider { window.spawn(cx, async move |cx| { let terminal = terminal_panel .update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(task, window, cx) + terminal_panel.spawn_task(&task, window, cx) }) .ok()? .await; From b1aa2723e9b10350ec4270ab6685ffe57ffe4309 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 12:58:10 +0200 Subject: [PATCH 9/9] editor: Reverse range of pending selection if required (#38410) cc https://github.com/zed-industries/zed/issues/38129 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/selections_collection.rs | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index e562be10e92344c1c892878ab674cba39beb74c2..4343443ff8c4cb4e388984c9014b13ddc8726523 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -469,13 +469,24 @@ impl<'a> MutableSelectionsCollection<'a> { } pub(crate) fn set_pending_anchor_range(&mut self, range: Range, mode: SelectMode) { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); self.collection.pending = Some(PendingSelection { - selection: Selection { - id: post_inc(&mut self.collection.next_selection_id), - start: range.start, - end: range.end, - reversed: false, - goal: SelectionGoal::None, + selection: { + let mut start = range.start; + let mut end = range.end; + let reversed = if start.cmp(&end, &buffer).is_gt() { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } }, mode, });