Compare commits
7 Commits
perf/proje
...
push-xvrxo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0069a1d179 | ||
|
|
61f512af03 | ||
|
|
dd5482a899 | ||
|
|
9094eb811b | ||
|
|
29f9853978 | ||
|
|
1e45c99c80 | ||
|
|
28f50977cf |
5
.github/workflows/run_tests.yml
vendored
5
.github/workflows/run_tests.yml
vendored
@@ -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
2
Cargo.lock
generated
@@ -16561,10 +16561,10 @@ checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
|
||||
name = "svg_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"file_icons",
|
||||
"gpui",
|
||||
"language",
|
||||
"multi_buffer",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
40
crates/agent_ui/src/ui/hold_for_default.rs
Normal file
40
crates/agent_ui/src/ui/hold_for_default.rs
Normal 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")
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>() {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>()
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user