Compare commits

...

7 Commits

Author SHA1 Message Date
Lukas Wirth
0069a1d179 diagnostics: Deduplicate diagnostics blocks 2025-11-22 11:10:34 +01:00
Finn Evers
61f512af03 Move protobuf action to default linux runner (#43085)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-20 12:48:39 +00:00
Arun Chapagain
dd5482a899 docs: Update developing extension docs for updating specific submodule (#42548)
Release Notes:

- N/A
2025-11-20 13:33:13 +01:00
Bhuminjay Soni
9094eb811b git: Compress diff for commit message generation (#42835)
This PR compresses diff capped at 20000 bytes by:
- Truncation of all lines to 256 chars
- Iteratively removing last hunks from each file until size <= 20000
bytes.


Closes #34486

Release Notes:

- Improved: Compress large diffs for commit message generation (thanks
@11happy)

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-11-20 11:30:34 +00:00
Lukas Wirth
29f9853978 svg_preview: Remove unnecessary dependency on editor (#43147)
Editor is a choke point in our compilation graph while also being a very
common crate that is being edited. So reducing things that depend on it
will generally improve compilation times for us.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-20 12:18:50 +01:00
Aaron Saunders
1e45c99c80 Improve readability of files in the git changes panel (#41857)
Closes _unknown_

<img width="1212" height="463" alt="image"
src="https://github.com/user-attachments/assets/ec00fcf0-7eb9-4291-b1e2-66e014dc30ac"
/>


This PR places the file_name before the file_path so that when the panel
is slim it is still usable, mirrors the behaviour of the file picker
(cmd+P)

Release Notes:
-  Improved readability of files in the git changes panel
2025-11-20 06:14:46 -05:00
Danilo Leal
28f50977cf agent_ui: Add support for setting a model as the default for external agents (#43122)
This PR builds on top of the `default_mode` feature where it was
possible to set an external agent mode as the default if you held a
modifier while clicking on the desired option. Now, if you want to have,
for example, Haiku as your default Claude Code model, you can do that.
This feature adds parity between external agents and Zed's built-in one,
which already supported this feature for a little while.

Note: This still doesn't work with external agents installed from
extensions. At the moment, this is limited to Claude Code, Codex, and
Gemini—the ones we include out of the box.

Release Notes:

- agent: Added the ability to set a model as the default for a given
built-in external agent (Claude Code, Codex CLI, or Gemini CLI).
2025-11-20 11:00:01 +01:00
52 changed files with 832 additions and 196 deletions

View File

@@ -493,7 +493,10 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: self-mini-macos
runs-on: namespace-profile-16x32-ubuntu-2204
env:
GIT_AUTHOR_NAME: Protobuf Action
GIT_AUTHOR_EMAIL: ci@zed.dev
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

2
Cargo.lock generated
View File

@@ -16561,10 +16561,10 @@ checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
name = "svg_preview"
version = "0.1.0"
dependencies = [
"editor",
"file_icons",
"gpui",
"language",
"multi_buffer",
"ui",
"workspace",
]

View File

@@ -1316,7 +1316,10 @@
// "hunk_style": "staged_hollow"
// 2. Show unstaged hunks hollow and staged hunks filled:
// "hunk_style": "unstaged_hollow"
"hunk_style": "staged_hollow"
"hunk_style": "staged_hollow",
// Should the name or path be displayed first in the git view.
// "path_style": "file_name_first" or "file_path_first"
"path_style": "file_name_first"
},
// The list of custom Git hosting providers.
"git_hosting_providers": [

View File

@@ -35,6 +35,7 @@ pub struct AcpConnection {
auth_methods: Vec<acp::AuthMethod>,
agent_capabilities: acp::AgentCapabilities,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
root_dir: PathBuf,
// NB: Don't move this into the wait_task, since we need to ensure the process is
// killed on drop (setting kill_on_drop on the command seems to not always work).
@@ -57,6 +58,7 @@ pub async fn connect(
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
@@ -66,6 +68,7 @@ pub async fn connect(
command.clone(),
root_dir,
default_mode,
default_model,
is_remote,
cx,
)
@@ -82,6 +85,7 @@ impl AcpConnection {
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
@@ -207,6 +211,7 @@ impl AcpConnection {
sessions,
agent_capabilities: response.agent_capabilities,
default_mode,
default_model,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
@@ -245,6 +250,7 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let default_mode = self.default_mode.clone();
let default_model = self.default_model.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers =
@@ -333,6 +339,7 @@ impl AgentConnection for AcpConnection {
let default_mode = default_mode.clone();
let session_id = response.session_id.clone();
let modes = modes.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
@@ -367,6 +374,53 @@ impl AgentConnection for AcpConnection {
}
}
if let Some(default_model) = default_model {
if let Some(models) = models.as_ref() {
let mut models_ref = models.borrow_mut();
let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model);
if has_model {
let initial_model_id = models_ref.current_model_id.clone();
cx.spawn({
let default_model = default_model.clone();
let session_id = response.session_id.clone();
let models = models.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id: default_model,
meta: None,
})
.await.log_err();
if result.is_none() {
models.borrow_mut().current_model_id = initial_model_id;
}
}
}).detach();
models_ref.current_model_id = default_model;
} else {
let available_models = models_ref
.available_models
.iter()
.map(|model| format!("- `{}`: {}", model.model_id, model.name))
.collect::<Vec<_>>()
.join("\n");
log::warn!(
"`{default_model}` is not a valid {name} model. Available options:\n{available_models}",
);
}
} else {
log::warn!(
"`{name}` does not support model selection, but `default_model` was set in settings.",
);
}
}
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {

View File

@@ -68,6 +68,18 @@ pub trait AgentServer: Send {
) {
}
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
None
}
fn set_default_model(
&self,
_model_id: Option<agent_client_protocol::ModelId>,
_fs: Arc<dyn Fs>,
_cx: &mut App,
) {
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -55,6 +55,27 @@ impl AgentServer for ClaudeCode {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.default_model = model_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -68,6 +89,7 @@ impl AgentServer for ClaudeCode {
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -90,6 +112,7 @@ impl AgentServer for ClaudeCode {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -56,6 +56,27 @@ impl AgentServer for Codex {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_model = model_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -69,6 +90,7 @@ impl AgentServer for Codex {
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -92,6 +114,7 @@ impl AgentServer for Codex {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -61,6 +61,34 @@ impl crate::AgentServer for CustomAgentServer {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
if let Some(settings) = settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
{
settings.default_model = model_id.map(|m| m.to_string())
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -72,6 +100,7 @@ impl crate::AgentServer for CustomAgentServer {
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
@@ -98,6 +127,7 @@ impl crate::AgentServer for CustomAgentServer {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -476,6 +476,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
}),
gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings {
@@ -484,6 +485,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
}),
custom: collections::HashMap::default(),
},

View File

@@ -37,6 +37,7 @@ impl AgentServer for Gemini {
let store = delegate.store.downgrade();
let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| {
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
@@ -69,6 +70,7 @@ impl AgentServer for Gemini {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -11,7 +11,7 @@ use ui::{
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector};
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
pub struct ModeSelector {
connection: Rc<dyn AgentSessionModes>,
@@ -108,36 +108,11 @@ impl ModeSelector {
entry.documentation_aside(side, DocumentationEdge::Bottom, {
let description = description.clone();
move |cx| {
move |_| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(h_flex().flex_shrink_0().children(
ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
),
))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
} else {
this.child("to also set as default")
}
})),
)
.child(HoldForDefault::new(is_default))
.into_any_element()
}
})

View File

@@ -1,8 +1,10 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_servers::AgentServer;
use anyhow::Result;
use collections::IndexMap;
use fs::Fs;
use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
@@ -14,14 +16,18 @@ use ui::{
};
use util::ResultExt;
use crate::ui::HoldForDefault;
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
pub fn acp_model_selector(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<AcpModelSelector>,
) -> AcpModelSelector {
let delegate = AcpModelPickerDelegate::new(selector, window, cx);
let delegate = AcpModelPickerDelegate::new(selector, agent_server, fs, window, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
@@ -35,10 +41,12 @@ enum AcpModelPickerEntry {
pub struct AcpModelPickerDelegate {
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
filtered_entries: Vec<AcpModelPickerEntry>,
models: Option<AgentModelList>,
selected_index: usize,
selected_description: Option<(usize, SharedString)>,
selected_description: Option<(usize, SharedString, bool)>,
selected_model: Option<AgentModelInfo>,
_refresh_models_task: Task<()>,
}
@@ -46,6 +54,8 @@ pub struct AcpModelPickerDelegate {
impl AcpModelPickerDelegate {
fn new(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<AcpModelSelector>,
) -> Self {
@@ -86,6 +96,8 @@ impl AcpModelPickerDelegate {
Self {
selector,
agent_server,
fs,
filtered_entries: Vec::new(),
models: None,
selected_model: None,
@@ -181,6 +193,21 @@ impl PickerDelegate for AcpModelPickerDelegate {
if let Some(AcpModelPickerEntry::Model(model_info)) =
self.filtered_entries.get(self.selected_index)
{
if window.modifiers().secondary() {
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
self.agent_server.set_default_model(
if is_default {
None
} else {
Some(model_info.id.clone())
},
self.fs.clone(),
cx,
);
}
self.selector
.select_model(model_info.id.clone(), cx)
.detach_and_log_err(cx);
@@ -225,6 +252,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
),
AcpModelPickerEntry::Model(model_info) => {
let is_selected = Some(model_info) == self.selected_model.as_ref();
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
let model_icon_color = if is_selected {
Color::Accent
@@ -239,8 +268,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
this
.on_hover(cx.listener(move |menu, hovered, _, cx| {
if *hovered {
menu.delegate.selected_description = Some((ix, description.clone()));
} else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
menu.delegate.selected_description = Some((ix, description.clone(), is_default));
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
menu.delegate.selected_description = None;
}
cx.notify();
@@ -283,14 +312,24 @@ impl PickerDelegate for AcpModelPickerDelegate {
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<ui::DocumentationAside> {
self.selected_description.as_ref().map(|(_, description)| {
let description = description.clone();
DocumentationAside::new(
DocumentationSide::Left,
DocumentationEdge::Top,
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
)
})
self.selected_description
.as_ref()
.map(|(_, description, is_default)| {
let description = description.clone();
let is_default = *is_default;
DocumentationAside::new(
DocumentationSide::Left,
DocumentationEdge::Top,
Rc::new(move |_| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(HoldForDefault::new(is_default))
.into_any_element()
}),
)
})
}
}

View File

@@ -1,6 +1,9 @@
use std::rc::Rc;
use std::sync::Arc;
use acp_thread::{AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{
@@ -20,13 +23,15 @@ pub struct AcpModelSelectorPopover {
impl AcpModelSelectorPopover {
pub(crate) fn new(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self {
selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
selector: cx.new(move |cx| acp_model_selector(selector, agent_server, fs, window, cx)),
menu_handle,
focus_handle,
}

View File

@@ -591,9 +591,13 @@ impl AcpThreadView {
.connection()
.model_selector(thread.read(cx).session_id())
.map(|selector| {
let agent_server = this.agent.clone();
let fs = this.project.read(cx).fs().clone();
cx.new(|cx| {
AcpModelSelectorPopover::new(
selector,
agent_server,
fs,
PopoverMenuHandle::default(),
this.focus_handle(cx),
window,

View File

@@ -1348,6 +1348,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
args: vec![],
env: Some(HashMap::default()),
default_mode: None,
default_model: None,
},
);
}

View File

@@ -13,8 +13,8 @@ use editor::{
scroll::Autoscroll,
};
use gpui::{
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
Action, AnyElement, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, Focusable,
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -580,11 +580,11 @@ impl Item for AgentDiffPane {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -22,11 +22,11 @@ use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
use futures::FutureExt;
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, WeakEntity, actions, div, img, point,
prelude::*, pulsating_between, size,
Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
pulsating_between, size,
};
use language::{
BufferSnapshot, LspAdapterDelegate, ToOffset,
@@ -66,7 +66,7 @@ use workspace::{
};
use workspace::{
Save, Toast, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
item::{self, FollowableItem, Item},
notifications::NotificationId,
pane,
searchable::{SearchEvent, SearchableItem},
@@ -2588,11 +2588,11 @@ impl Item for TextThreadEditor {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -4,6 +4,7 @@ mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
mod context_pill;
mod end_trial_upsell;
mod hold_for_default;
mod onboarding_modal;
mod unavailable_editing_tooltip;
mod usage_callout;
@@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
pub use hold_for_default::*;
pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*;
pub use usage_callout::*;

View File

@@ -0,0 +1,40 @@
use gpui::{App, IntoElement, Modifiers, RenderOnce, Window};
use ui::{prelude::*, render_modifiers};
#[derive(IntoElement)]
pub struct HoldForDefault {
is_default: bool,
}
impl HoldForDefault {
pub fn new(is_default: bool) -> Self {
Self { is_default }
}
}
impl RenderOnce for HoldForDefault {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(h_flex().flex_shrink_0().children(render_modifiers(
&Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(TextSize::Default.rems(cx).into()),
true,
)))
.child(div().map(|this| {
if self.is_default {
this.child("to unset as default")
} else {
this.child("to set as default")
}
}))
}
}

View File

@@ -123,7 +123,7 @@ impl Render for Breadcrumbs {
.upgrade()
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
{
callback(editor.to_any(), window, cx);
callback(editor.to_any_view(), window, cx);
}
}
})

View File

@@ -11,7 +11,7 @@ use editor::{
display_map::ToDisplayPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
Subscription, Task, VisualContext as _, WeakEntity, Window, actions,
};
use project::Project;
@@ -25,7 +25,7 @@ use util::ResultExt;
use workspace::{CollaboratorId, item::TabContentParams};
use workspace::{
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
item::{FollowableItem, Item, ItemEvent, ItemHandle},
item::{FollowableItem, Item, ItemEvent},
searchable::SearchableItemHandle,
};
use workspace::{item::Dedup, notifications::NotificationId};
@@ -441,11 +441,11 @@ impl Item for ChannelView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -7,7 +7,7 @@ use editor::{
RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
Subscription, Task, WeakEntity, Window,
};
use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions};
@@ -418,11 +418,11 @@ impl Item for StackTraceView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -680,11 +680,11 @@ impl Item for BufferDiagnosticsEditor {
type_id: std::any::TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -84,6 +84,7 @@ impl DiagnosticRenderer {
markdown: cx.new(|cx| {
Markdown::new(markdown.into(), language_registry.clone(), None, cx)
}),
diagnostic_message: entry.diagnostic.message.clone(),
});
} else {
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
@@ -98,6 +99,7 @@ impl DiagnosticRenderer {
markdown: cx.new(|cx| {
Markdown::new(markdown.into(), language_registry.clone(), None, cx)
}),
diagnostic_message: entry.diagnostic.message.clone(),
});
}
}
@@ -190,9 +192,21 @@ pub(crate) struct DiagnosticBlock {
pub(crate) initial_range: Range<Point>,
pub(crate) severity: DiagnosticSeverity,
pub(crate) markdown: Entity<Markdown>,
// Used solely for deduplicating purposes
pub(crate) diagnostic_message: String,
pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
}
impl std::fmt::Debug for DiagnosticBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiagnosticBlock")
.field("initial_range", &self.initial_range)
.field("severity", &self.severity)
.field("markdown", &self.markdown)
.finish()
}
}
impl DiagnosticBlock {
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
let cx = &bcx.app;

View File

@@ -17,7 +17,7 @@ use editor::{
multibuffer_context_lines,
};
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, FocusOutEvent,
AnyElement, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Task, WeakEntity, Window, actions, div,
};
@@ -540,25 +540,43 @@ impl ProjectDiagnosticsEditor {
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
let diagnostics_toolbar_editor = Arc::new(this.clone());
let languages = this
.read_with(cx, |t, cx| t.project.read(cx).languages().clone())
.ok();
let mut dedup_set = HashSet::default();
for (_, group) in grouped {
let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
if group_severity.is_none_or(|s| s > max_severity) {
continue;
}
let languages = this
.read_with(cx, |t, cx| t.project.read(cx).languages().clone())
.ok();
let more = cx.update(|_, cx| {
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
Some(diagnostics_toolbar_editor.clone()),
languages,
languages.clone(),
cx,
)
})?;
blocks.extend(more);
for block in more {
// Remove duplicate blocks, they add no value to the user
// Especially rustc tends to emit a lot of similar looking sub diagnostic spans
// which can clobber the diagnostics ui
//
// Note we do not use the markdown output as we may modify
// it to add backlinks to the diagnostics groups which will
// thus always differ due to pointing to a different
// diagnostic that is at the same location though
//
// TODO: We might steal diagnostics blocks away that are linked to givent he above
if dedup_set.insert((
block.initial_range,
block.severity,
block.diagnostic_message,
)) {
blocks.push(block);
}
}
}
let cmp_excerpts = |buffer_snapshot: &BufferSnapshot,
@@ -880,11 +898,11 @@ impl Item for ProjectDiagnosticsEditor {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -104,6 +104,30 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
message: "value moved here".to_string()
},
// intentional duplicate as we want to make sure that we do not render blocks with the same messages at the same locations
// rustc likes to do this at times
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
message: "value moved here".to_string()
},
]),
..Default::default()
},
// intentional duplicate as we want to make sure that we do not render blocks with the same messages at the same locations
// rustc likes to do this at times
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
severity:Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
message: "value moved here".to_string()
},
]),
..Default::default()
}

View File

@@ -26983,7 +26983,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
.unwrap();
assert_eq!(
handle.to_any().entity_type(),
handle.to_any_view().entity_type(),
TypeId::of::<InvalidItemView>()
);
}

View File

@@ -588,6 +588,21 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
impl Item for Editor {
type Event = EditorEvent;
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
cx: &'a App,
) -> Option<gpui::AnyEntity> {
if TypeId::of::<Self>() == type_id {
Some(self_handle.clone().into())
} else if TypeId::of::<MultiBuffer>() == type_id {
Some(self_handle.read(cx).buffer.clone().into())
} else {
None
}
}
fn navigate(
&mut self,
data: Box<dyn std::any::Any>,

View File

@@ -6,9 +6,9 @@ use editor::{
};
use git::repository::{CommitDetails, CommitDiff, RepoPath};
use gpui::{
Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task,
WeakEntity, Window, actions,
Action, AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task, WeakEntity,
Window, actions,
};
use language::{
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
@@ -499,11 +499,11 @@ impl Item for CommitView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -5,8 +5,8 @@ use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorEvent, MultiBuffer};
use futures::{FutureExt, select_biased};
use gpui::{
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
FocusHandle, Focusable, IntoElement, Render, Task, Window,
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, Render, Task, Window,
};
use language::Buffer;
use project::Project;
@@ -268,11 +268,11 @@ impl Item for FileDiffView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -11,6 +11,7 @@ use crate::{
use agent_settings::AgentSettings;
use anyhow::Context as _;
use askpass::AskPassDelegate;
use cloud_llm_client::CompletionIntent;
use db::kvp::KEY_VALUE_STORE;
use editor::{
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
@@ -51,6 +52,7 @@ use panel::{
use project::{
Fs, Project, ProjectPath,
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
project_settings::{GitPathStyle, ProjectSettings},
};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, StatusStyle};
@@ -67,14 +69,11 @@ use ui::{
use util::paths::PathStyle;
use util::{ResultExt, TryFutureExt, maybe};
use workspace::SERIALIZATION_THROTTLE_TIME;
use cloud_llm_client::CompletionIntent;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
};
actions!(
git_panel,
[
@@ -274,6 +273,69 @@ impl GitStatusEntry {
}
}
struct TruncatedPatch {
header: String,
hunks: Vec<String>,
hunks_to_keep: usize,
}
impl TruncatedPatch {
fn from_unified_diff(patch_str: &str) -> Option<Self> {
let lines: Vec<&str> = patch_str.lines().collect();
if lines.len() < 2 {
return None;
}
let header = format!("{}\n{}\n", lines[0], lines[1]);
let mut hunks = Vec::new();
let mut current_hunk = String::new();
for line in &lines[2..] {
if line.starts_with("@@") {
if !current_hunk.is_empty() {
hunks.push(current_hunk);
}
current_hunk = format!("{}\n", line);
} else if !current_hunk.is_empty() {
current_hunk.push_str(line);
current_hunk.push('\n');
}
}
if !current_hunk.is_empty() {
hunks.push(current_hunk);
}
if hunks.is_empty() {
return None;
}
let hunks_to_keep = hunks.len();
Some(TruncatedPatch {
header,
hunks,
hunks_to_keep,
})
}
fn calculate_size(&self) -> usize {
let mut size = self.header.len();
for (i, hunk) in self.hunks.iter().enumerate() {
if i < self.hunks_to_keep {
size += hunk.len();
}
}
size
}
fn to_string(&self) -> String {
let mut out = self.header.clone();
for (i, hunk) in self.hunks.iter().enumerate() {
if i < self.hunks_to_keep {
out.push_str(hunk);
}
}
let skipped_hunks = self.hunks.len() - self.hunks_to_keep;
if skipped_hunks > 0 {
out.push_str(&format!("[...skipped {} hunks...]\n", skipped_hunks));
}
out
}
}
pub struct GitPanel {
pub(crate) active_repository: Option<Entity<Repository>>,
pub(crate) commit_editor: Entity<Editor>,
@@ -1815,6 +1877,96 @@ impl GitPanel {
self.generate_commit_message(cx);
}
fn split_patch(patch: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current_patch = String::new();
for line in patch.lines() {
if line.starts_with("---") && !current_patch.is_empty() {
result.push(current_patch.trim_end_matches('\n').into());
current_patch = String::new();
}
current_patch.push_str(line);
current_patch.push('\n');
}
if !current_patch.is_empty() {
result.push(current_patch.trim_end_matches('\n').into());
}
result
}
fn truncate_iteratively(patch: &str, max_bytes: usize) -> String {
let mut current_size = patch.len();
if current_size <= max_bytes {
return patch.to_string();
}
let file_patches = Self::split_patch(patch);
let mut file_infos: Vec<TruncatedPatch> = file_patches
.iter()
.filter_map(|patch| TruncatedPatch::from_unified_diff(patch))
.collect();
if file_infos.is_empty() {
return patch.to_string();
}
current_size = file_infos.iter().map(|f| f.calculate_size()).sum::<usize>();
while current_size > max_bytes {
let file_idx = file_infos
.iter()
.enumerate()
.filter(|(_, f)| f.hunks_to_keep > 1)
.max_by_key(|(_, f)| f.hunks_to_keep)
.map(|(idx, _)| idx);
match file_idx {
Some(idx) => {
let file = &mut file_infos[idx];
let size_before = file.calculate_size();
file.hunks_to_keep -= 1;
let size_after = file.calculate_size();
let saved = size_before.saturating_sub(size_after);
current_size = current_size.saturating_sub(saved);
}
None => {
break;
}
}
}
file_infos
.iter()
.map(|info| info.to_string())
.collect::<Vec<_>>()
.join("\n")
}
pub fn compress_commit_diff(diff_text: &str, max_bytes: usize) -> String {
if diff_text.len() <= max_bytes {
return diff_text.to_string();
}
let mut compressed = diff_text
.lines()
.map(|line| {
if line.len() > 256 {
format!("{}...[truncated]\n", &line[..256])
} else {
format!("{}\n", line)
}
})
.collect::<Vec<_>>()
.join("");
if compressed.len() <= max_bytes {
return compressed;
}
compressed = Self::truncate_iteratively(&compressed, max_bytes);
compressed
}
/// Generates a commit message using an LLM.
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
if !self.can_commit() || !AgentSettings::get_global(cx).enabled(cx) {
@@ -1873,10 +2025,8 @@ impl GitPanel {
}
};
const ONE_MB: usize = 1_000_000;
if diff_text.len() > ONE_MB {
diff_text = diff_text.chars().take(ONE_MB).collect()
}
const MAX_DIFF_BYTES: usize = 20_000;
diff_text = Self::compress_commit_diff(&diff_text, MAX_DIFF_BYTES);
let subject = this.update(cx, |this, cx| {
this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default()
@@ -3954,6 +4104,7 @@ impl GitPanel {
cx: &Context<Self>,
) -> AnyElement {
let path_style = self.project.read(cx).path_style(cx);
let git_path_style = ProjectSettings::get_global(cx).git.path_style;
let display_name = entry.display_name(path_style);
let selected = self.selected_entry == Some(ix);
@@ -4053,7 +4204,6 @@ impl GitPanel {
} else {
cx.theme().colors().ghost_element_active
};
h_flex()
.id(id)
.h(self.list_item_height())
@@ -4151,28 +4301,70 @@ impl GitPanel {
h_flex()
.items_center()
.flex_1()
// .overflow_hidden()
.when_some(entry.parent_dir(path_style), |this, parent| {
if !parent.is_empty() {
this.child(
self.entry_label(
format!("{parent}{}", path_style.separator()),
path_color,
)
.when(status.is_deleted(), |this| this.strikethrough()),
)
} else {
this
}
})
.child(
self.entry_label(display_name, label_color)
.when(status.is_deleted(), |this| this.strikethrough()),
),
.child(h_flex().items_center().flex_1().map(|this| {
self.path_formatted(
this,
entry.parent_dir(path_style),
path_color,
display_name,
label_color,
path_style,
git_path_style,
status.is_deleted(),
)
})),
)
.into_any_element()
}
fn path_formatted(
&self,
parent: Div,
directory: Option<String>,
path_color: Color,
file_name: String,
label_color: Color,
path_style: PathStyle,
git_path_style: GitPathStyle,
strikethrough: bool,
) -> Div {
parent
.when(git_path_style == GitPathStyle::FileNameFirst, |this| {
this.child(
self.entry_label(
match directory.as_ref().is_none_or(|d| d.is_empty()) {
true => file_name.clone(),
false => format!("{file_name} "),
},
label_color,
)
.when(strikethrough, Label::strikethrough),
)
})
.when_some(directory, |this, dir| {
match (
!dir.is_empty(),
git_path_style == GitPathStyle::FileNameFirst,
) {
(true, true) => this.child(
self.entry_label(dir, path_color)
.when(strikethrough, Label::strikethrough),
),
(true, false) => this.child(
self.entry_label(format!("{dir}{}", path_style.separator()), path_color)
.when(strikethrough, Label::strikethrough),
),
_ => this,
}
})
.when(git_path_style == GitPathStyle::FilePathFirst, |this| {
this.child(
self.entry_label(file_name, label_color)
.when(strikethrough, Label::strikethrough),
)
})
}
fn has_write_access(&self, cx: &App) -> bool {
!self.project.read(cx).is_read_only(cx)
}
@@ -4989,6 +5181,7 @@ mod tests {
status::{StatusCode, UnmergedStatus, UnmergedStatusCode},
};
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
use indoc::indoc;
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
@@ -5688,4 +5881,64 @@ mod tests {
);
}
}
#[test]
fn test_compress_diff_no_truncation() {
let diff = indoc! {"
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,2 @@
-old
+new
"};
let result = GitPanel::compress_commit_diff(diff, 1000);
assert_eq!(result, diff);
}
#[test]
fn test_compress_diff_truncate_long_lines() {
let long_line = "a".repeat(300);
let diff = indoc::formatdoc! {"
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
context
+{}
more context
", long_line};
let result = GitPanel::compress_commit_diff(&diff, 100);
assert!(result.contains("...[truncated]"));
assert!(result.len() < diff.len());
}
#[test]
fn test_compress_diff_truncate_hunks() {
let diff = indoc! {"
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,2 @@
context
-old1
+new1
@@ -5,2 +5,2 @@
context 2
-old2
+new2
@@ -10,2 +10,2 @@
context 3
-old3
+new3
"};
let result = GitPanel::compress_commit_diff(diff, 100);
let expected = indoc! {"
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,2 @@
context
-old1
+new1
[...skipped 2 hunks...]
"};
assert_eq!(result, expected);
}
}

View File

@@ -19,7 +19,7 @@ use git::{
status::FileStatus,
};
use gpui::{
Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,
Action, AnyElement, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,
FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions,
};
use language::{Anchor, Buffer, Capability, OffsetRangeExt};
@@ -775,11 +775,11 @@ impl Item for ProjectDiff {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -5,8 +5,8 @@ use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorEvent, MultiBuffer, ToPoint, actions::DiffClipboardWithSelectionData};
use futures::{FutureExt, select_biased};
use gpui::{
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
FocusHandle, Focusable, IntoElement, Render, Task, Window,
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, Render, Task, Window,
};
use language::{self, Buffer, Point};
use project::Project;
@@ -329,11 +329,11 @@ impl Item for TextDiffView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.diff_editor.to_any())
Some(self.diff_editor.clone().into())
} else {
None
}

View File

@@ -2,8 +2,8 @@ use collections::VecDeque;
use copilot::Copilot;
use editor::{Editor, EditorEvent, MultiBufferOffset, actions::MoveToEnd, scroll::Autoscroll};
use gpui::{
AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement,
Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
};
use itertools::Itertools;
use language::{LanguageServerId, language_settings::SoftWrap};
@@ -748,11 +748,11 @@ impl Item for LspLogView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -1777,6 +1777,7 @@ pub struct BuiltinAgentServerSettings {
pub env: Option<HashMap<String, String>>,
pub ignore_system_version: Option<bool>,
pub default_mode: Option<String>,
pub default_model: Option<String>,
}
impl BuiltinAgentServerSettings {
@@ -1799,6 +1800,7 @@ impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
env: value.env,
ignore_system_version: value.ignore_system_version,
default_mode: value.default_mode,
default_model: value.default_model,
}
}
}
@@ -1823,6 +1825,12 @@ pub struct CustomAgentServerSettings {
///
/// Default: None
pub default_mode: Option<String>,
/// The default model to use for this agent.
///
/// This should be the model ID as reported by the agent.
///
/// Default: None
pub default_model: Option<String>,
}
impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
@@ -1834,6 +1842,7 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
env: value.env,
},
default_mode: value.default_mode,
default_model: value.default_model,
}
}
}
@@ -2156,6 +2165,7 @@ mod extension_agent_tests {
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
};
let BuiltinAgentServerSettings { path, .. } = settings.into();
@@ -2171,6 +2181,7 @@ mod extension_agent_tests {
args: vec!["serve".into()],
env: None,
default_mode: None,
default_model: None,
};
let CustomAgentServerSettings {

View File

@@ -348,6 +348,26 @@ pub struct GitSettings {
///
/// Default: staged_hollow
pub hunk_style: settings::GitHunkStyleSetting,
/// How file paths are displayed in the git gutter.
///
/// Default: file_name_first
pub path_style: GitPathStyle,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum GitPathStyle {
#[default]
FileNameFirst,
FilePathFirst,
}
impl From<settings::GitPathStyle> for GitPathStyle {
fn from(style: settings::GitPathStyle) -> Self {
match style {
settings::GitPathStyle::FileNameFirst => GitPathStyle::FileNameFirst,
settings::GitPathStyle::FilePathFirst => GitPathStyle::FilePathFirst,
}
}
}
#[derive(Clone, Copy, Debug)]
@@ -501,6 +521,7 @@ impl Settings for ProjectSettings {
}
},
hunk_style: git.hunk_style.unwrap(),
path_style: git.path_style.unwrap().into(),
};
Self {
context_servers: project

View File

@@ -17,10 +17,9 @@ use editor::{
};
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, UpdateGlobal, WeakEntity, Window, actions,
div,
Action, AnyElement, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, Render,
SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions, div,
};
use language::{Buffer, Language};
use menu::Confirm;
@@ -497,7 +496,7 @@ impl Item for ProjectSearchView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {

View File

@@ -332,6 +332,12 @@ pub struct BuiltinAgentServerSettings {
///
/// Default: None
pub default_mode: Option<String>,
/// The default model to use for this agent.
///
/// This should be the model ID as reported by the agent.
///
/// Default: None
pub default_model: Option<String>,
}
#[skip_serializing_none]
@@ -348,4 +354,10 @@ pub struct CustomAgentServerSettings {
///
/// Default: None
pub default_mode: Option<String>,
/// The default model to use for this agent.
///
/// This should be the model ID as reported by the agent.
///
/// Default: None
pub default_model: Option<String>,
}

View File

@@ -311,6 +311,10 @@ pub struct GitSettings {
///
/// Default: staged_hollow
pub hunk_style: Option<GitHunkStyleSetting>,
/// How file paths are displayed in the git gutter.
///
/// Default: file_name_first
pub path_style: Option<GitPathStyle>,
}
#[derive(
@@ -406,6 +410,28 @@ pub enum GitHunkStyleSetting {
UnstagedHollow,
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum GitPathStyle {
/// Show file name first, then path
#[default]
FileNameFirst,
/// Show full path first
FilePathFirst,
}
#[skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct DiagnosticsSettingsContent {

View File

@@ -5494,6 +5494,19 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Path Style",
description: "Should the name or path be displayed first in the git view.",
field: Box::new(SettingField {
json_path: Some("git.path_style"),
pick: |settings_content| settings_content.git.as_ref()?.path_style.as_ref(),
write: |settings_content, value| {
settings_content.git.get_or_insert_default().path_style = value;
},
}),
metadata: None,
files: USER,
}),
],
},
SettingsPage {

View File

@@ -452,6 +452,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::DockPosition>(render_dropdown)
.add_basic_renderer::<settings::GitGutterSetting>(render_dropdown)
.add_basic_renderer::<settings::GitHunkStyleSetting>(render_dropdown)
.add_basic_renderer::<settings::GitPathStyle>(render_dropdown)
.add_basic_renderer::<settings::DiagnosticSeverityContent>(render_dropdown)
.add_basic_renderer::<settings::SeedQuerySetting>(render_dropdown)
.add_basic_renderer::<settings::DoubleClickInMultibuffer>(render_dropdown)

View File

@@ -12,7 +12,7 @@ workspace = true
path = "src/svg_preview.rs"
[dependencies]
editor.workspace = true
multi_buffer.workspace = true
file_icons.workspace = true
gpui.workspace = true
language.workspace = true

View File

@@ -1,13 +1,13 @@
use std::mem;
use std::sync::Arc;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img,
};
use language::{Buffer, BufferEvent};
use multi_buffer::MultiBuffer;
use ui::prelude::*;
use workspace::item::Item;
use workspace::{Pane, Workspace};
@@ -34,7 +34,7 @@ pub enum SvgPreviewMode {
impl SvgPreviewView {
pub fn new(
mode: SvgPreviewMode,
active_editor: Entity<Editor>,
active_buffer: Entity<MultiBuffer>,
workspace_handle: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Workspace>,
@@ -48,11 +48,7 @@ impl SvgPreviewView {
None
};
let buffer = active_editor
.read(cx)
.buffer()
.clone()
.read_with(cx, |buffer, _cx| buffer.as_singleton());
let buffer = active_buffer.read_with(cx, |buffer, _cx| buffer.as_singleton());
let subscription = buffer
.as_ref()
@@ -84,10 +80,10 @@ impl SvgPreviewView {
if let workspace::Event::ActiveItemChanged = event {
let workspace = workspace.read(cx);
if let Some(active_item) = workspace.active_item(cx)
&& let Some(editor) = active_item.downcast::<Editor>()
&& Self::is_svg_file(&editor, cx)
&& let Some(buffer) = active_item.downcast::<MultiBuffer>()
&& Self::is_svg_file(&buffer, cx)
{
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
let Some(buffer) = buffer.read(cx).as_singleton() else {
return;
};
if this.buffer.as_ref() != Some(&buffer) {
@@ -142,10 +138,10 @@ impl SvgPreviewView {
fn find_existing_preview_item_idx(
pane: &Pane,
editor: &Entity<Editor>,
buffer: &Entity<MultiBuffer>,
cx: &App,
) -> Option<usize> {
let buffer_id = editor.read(cx).buffer().entity_id();
let buffer_id = buffer.entity_id();
pane.items_of_type::<SvgPreviewView>()
.find(|view| {
view.read(cx)
@@ -156,25 +152,25 @@ impl SvgPreviewView {
.and_then(|view| pane.index_for_item(&view))
}
pub fn resolve_active_item_as_svg_editor(
pub fn resolve_active_item_as_svg_buffer(
workspace: &Workspace,
cx: &mut Context<Workspace>,
) -> Option<Entity<Editor>> {
) -> Option<Entity<MultiBuffer>> {
workspace
.active_item(cx)?
.act_as::<Editor>(cx)
.filter(|editor| Self::is_svg_file(&editor, cx))
.act_as::<MultiBuffer>(cx)
.filter(|buffer| Self::is_svg_file(&buffer, cx))
}
fn create_svg_view(
mode: SvgPreviewMode,
workspace: &mut Workspace,
editor: Entity<Editor>,
buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<SvgPreviewView> {
let workspace_handle = workspace.weak_handle();
SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
SvgPreviewView::new(mode, buffer, workspace_handle, window, cx)
}
fn create_buffer_subscription(
@@ -194,10 +190,8 @@ impl SvgPreviewView {
)
}
pub fn is_svg_file(editor: &Entity<Editor>, cx: &App) -> bool {
editor
.read(cx)
.buffer()
pub fn is_svg_file(buffer: &Entity<MultiBuffer>, cx: &App) -> bool {
buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
@@ -210,19 +204,19 @@ 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)
&& Self::is_svg_file(&editor, cx)
if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
&& Self::is_svg_file(&buffer, cx)
{
let view = Self::create_svg_view(
SvgPreviewMode::Default,
workspace,
editor.clone(),
buffer.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)
Self::find_existing_preview_item_idx(pane, &buffer, cx)
{
pane.activate_item(existing_view_idx, true, true, window, cx);
} else {
@@ -234,7 +228,7 @@ impl SvgPreviewView {
});
workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
&& Self::is_svg_file(&editor, cx)
{
let editor_clone = editor.clone();
@@ -269,7 +263,7 @@ impl SvgPreviewView {
});
workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
&& Self::is_svg_file(&editor, cx)
{
let view =

View File

@@ -11,9 +11,9 @@ use anyhow::Result;
use client::{Client, proto};
use futures::{StreamExt, channel::mpsc};
use gpui::{
Action, AnyElement, AnyView, App, AppContext, Context, Entity, EntityId, EventEmitter,
FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task,
WeakEntity, Window,
Action, AnyElement, AnyEntity, AnyView, App, AppContext, Context, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render,
SharedString, Task, WeakEntity, Window,
};
use project::{Project, ProjectEntryId, ProjectPath};
pub use settings::{
@@ -279,7 +279,7 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<AnyEntity> {
if TypeId::of::<Self>() == type_id {
Some(self_handle.clone().into())
} else {
@@ -454,7 +454,7 @@ pub trait ItemHandle: 'static + Send {
fn workspace_deactivated(&self, window: &mut Window, cx: &mut App);
fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool;
fn item_id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
fn to_any_view(&self) -> AnyView;
fn is_dirty(&self, cx: &App) -> bool;
fn has_deleted_file(&self, cx: &App) -> bool;
fn has_conflict(&self, cx: &App) -> bool;
@@ -480,7 +480,7 @@ pub trait ItemHandle: 'static + Send {
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>>;
fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option<AnyView>;
fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option<AnyEntity>;
fn to_followable_item_handle(&self, cx: &App) -> Option<Box<dyn FollowableItemHandle>>;
fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>>;
fn on_release(
@@ -513,7 +513,7 @@ pub trait WeakItemHandle: Send + Sync {
impl dyn ItemHandle {
pub fn downcast<V: 'static>(&self) -> Option<Entity<V>> {
self.to_any().downcast().ok()
self.to_any_view().downcast().ok()
}
pub fn act_as<V: 'static>(&self, cx: &App) -> Option<Entity<V>> {
@@ -911,7 +911,7 @@ impl<T: Item> ItemHandle for Entity<T> {
self.entity_id()
}
fn to_any(&self) -> AnyView {
fn to_any_view(&self) -> AnyView {
self.clone().into()
}
@@ -964,7 +964,7 @@ impl<T: Item> ItemHandle for Entity<T> {
self.update(cx, |item, cx| item.reload(project, window, cx))
}
fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option<AnyView> {
fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option<AnyEntity> {
self.read(cx).act_as_type(type_id, self, cx)
}
@@ -1009,7 +1009,7 @@ impl<T: Item> ItemHandle for Entity<T> {
}
fn to_serializable_item_handle(&self, cx: &App) -> Option<Box<dyn SerializableItemHandle>> {
SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
SerializableItemRegistry::view_to_serializable_item_handle(self.to_any_view(), cx)
}
fn preserve_preview(&self, cx: &App) -> bool {
@@ -1030,13 +1030,13 @@ impl<T: Item> ItemHandle for Entity<T> {
impl From<Box<dyn ItemHandle>> for AnyView {
fn from(val: Box<dyn ItemHandle>) -> Self {
val.to_any()
val.to_any_view()
}
}
impl From<&Box<dyn ItemHandle>> for AnyView {
fn from(val: &Box<dyn ItemHandle>) -> Self {
val.to_any()
val.to_any_view()
}
}
@@ -1247,7 +1247,7 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
window: &mut Window,
cx: &mut App,
) -> Option<Dedup> {
let existing = existing.to_any().downcast::<T>().ok()?;
let existing = existing.to_any_view().downcast::<T>().ok()?;
self.read(cx).dedup(existing.read(cx), window, cx)
}

View File

@@ -1203,7 +1203,7 @@ impl Pane {
pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = Entity<T>> {
self.items
.iter()
.filter_map(|item| item.to_any().downcast().ok())
.filter_map(|item| item.to_any_view().downcast().ok())
}
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
@@ -3869,7 +3869,7 @@ impl Render for Pane {
.size_full()
.overflow_hidden()
.child(self.toolbar.clone())
.child(item.to_any())
.child(item.to_any_view())
} else {
let placeholder = div
.id("pane_placeholder")
@@ -6957,7 +6957,7 @@ mod tests {
.enumerate()
.map(|(ix, item)| {
let mut state = item
.to_any()
.to_any_view()
.downcast::<TestItem>()
.unwrap()
.read(cx)

View File

@@ -399,13 +399,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()
this.to_any_view()
}
}
impl From<&Box<dyn SearchableItemHandle>> for AnyView {
fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
this.to_any()
this.to_any_view()
}
}

View File

@@ -2869,7 +2869,7 @@ impl Workspace {
pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
let item = self.active_item(cx)?;
item.to_any().downcast::<I>().ok()
item.to_any_view().downcast::<I>().ok()
}
fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
@@ -11214,7 +11214,7 @@ mod tests {
// Now we can check if the handle we got back errored or not
assert_eq!(
handle.to_any().entity_type(),
handle.to_any_view().entity_type(),
TypeId::of::<TestPngItemView>()
);
@@ -11227,7 +11227,7 @@ mod tests {
.unwrap();
assert_eq!(
handle.to_any().entity_type(),
handle.to_any_view().entity_type(),
TypeId::of::<TestIpynbItemView>()
);
@@ -11276,7 +11276,7 @@ mod tests {
// This _must_ be the second item registered
assert_eq!(
handle.to_any().entity_type(),
handle.to_any_view().entity_type(),
TypeId::of::<TestAlternatePngItemView>()
);

View File

@@ -32,7 +32,7 @@ impl QuickActionBar {
.is_some()
{
preview_type = Some(PreviewType::Markdown);
} else if SvgPreviewView::resolve_active_item_as_svg_editor(workspace, cx).is_some()
} else if SvgPreviewView::resolve_active_item_as_svg_buffer(workspace, cx).is_some()
{
preview_type = Some(PreviewType::Svg);
}

View File

@@ -423,7 +423,10 @@ impl Zeta {
let cursor_offset = cursor_point.to_offset(&snapshot);
let prompt_for_events = {
let events = events.clone();
move || prompt_for_events_impl(&events, MAX_EVENT_TOKENS)
move || {
// let _move_me = &mut cx;
prompt_for_events_impl(&events, MAX_EVENT_TOKENS)
}
};
let gather_task = gather_context(
full_path_str,

View File

@@ -24,7 +24,7 @@ use ui::{
IconSize, InteractiveElement, IntoElement, ListHeader, ListItem, StyledTypography, div, h_flex,
v_flex,
};
use workspace::{Item, ItemHandle as _};
use workspace::Item;
use zeta2::{
Zeta, ZetaContextRetrievalDebugInfo, ZetaContextRetrievalStartedDebugInfo, ZetaDebugInfo,
ZetaSearchQueryDebugInfo,
@@ -402,11 +402,11 @@ impl Item for Zeta2ContextView {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.runs.get(self.current_ix)?.editor.to_any())
Some(self.runs.get(self.current_ix)?.editor.clone().into())
} else {
None
}

View File

@@ -109,6 +109,18 @@ git submodule init
git submodule update
```
## Update Your Extension
When developing/updating your extension, you will likely need to update its content from its submodule in the extensions repository.
To quickly fetch the latest code for only specific extension (and avoid updating all others), use the specific path:
```sh
# From the root of the repository:
git submodule update --remote extensions/your-extension-name
```
> Note: If you need to update all submodules (e.g., if multiple extensions have changed, or for a full clean build), you can run `git submodule update` without a path, but this will take longer.
## Extension License Requirements
As of October 1st, 2025, extension repositories must include a license.

View File

@@ -363,7 +363,9 @@ pub(crate) fn check_postgres_and_protobuf_migrations() -> NamedJob {
named::job(
release_job(&[])
.runs_on(runners::MAC_DEFAULT)
.runs_on(runners::LINUX_DEFAULT)
.add_env(("GIT_AUTHOR_NAME", "Protobuf Action"))
.add_env(("GIT_AUTHOR_EMAIL", "ci@zed.dev"))
.add_step(steps::checkout_repo().with(("fetch-depth", 0))) // fetch full history
.add_step(remove_untracked_files())
.add_step(ensure_fresh_merge())