Compare commits

...

15 Commits

Author SHA1 Message Date
Zed Bot
f21d34e332 Bump to 0.180.2 for @osiewicz 2025-03-28 12:43:18 +00:00
gcp-cherry-pick-bot[bot]
b17df00837 language server: Fix restarts sometimes not working for buffers open in go-to-definition view (cherry-pick #27655) (#27658)
Cherry-picked language server: Fix restarts sometimes not working for
buffers open in go-to-definition view (#27655)

Closes #ISSUE

Release Notes:

- Fixed language server restarts sometimes not restarting a language
server.

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-03-28 13:15:47 +01:00
Zed Bot
3605e77397 Bump to 0.180.1 for @maxbrunsfeld 2025-03-27 16:14:16 +00:00
Marshall Bowers
4535b23c0d assistant2: Allow customizing tools for default profiles (#27594)
This PR adds support for customizing the tools for the default profiles.

Release Notes:

- N/A
2025-03-27 12:10:23 -04:00
Marshall Bowers
e1dde363f0 assistant2: Add ability to configure tools for profiles in the UI (#27562)
This PR adds the ability to configure tools for a profile in the UI:


https://github.com/user-attachments/assets/16642f14-8faa-4a91-bb9e-1d480692f1f2

Note: Doesn't yet work for customizing tools for the default profiles.

Release Notes:

- N/A
2025-03-27 12:10:23 -04:00
Marshall Bowers
2f6fd4798b assistant2: Start on modal for managing profiles (#27546)
This PR starts work on a modal for managing profiles.

Release Notes:

- N/A
2025-03-27 12:10:23 -04:00
Marshall Bowers
f6b6bfff86 assistant2: Rework enabled tool representation (#27527)
This PR reworks how we store enabled tools in the `ToolWorkingSet`.

We now track them based on which tools are explicitly enabled, rather
than by the tools that have been disabled.

Also fixed an issue where switching profiles wouldn't properly set the
right tools.

Release Notes:

- N/A
2025-03-27 12:10:23 -04:00
Marshall Bowers
a35b4569e8 assistant2: Add profile selector (#27520)
This PR replaces the tool selector with a new profile selector.

<img width="1394" alt="Screenshot 2025-03-26 at 2 35 42 PM"
src="https://github.com/user-attachments/assets/9631c6e9-9c47-411e-b9fc-5d61ed9ca1fe"
/>

<img width="1394" alt="Screenshot 2025-03-26 at 2 35 50 PM"
src="https://github.com/user-attachments/assets/3abe4e08-d044-4d3f-aa95-f472938452a8"
/>

Release Notes:

- N/A
2025-03-27 12:10:23 -04:00
João Marcos
62a722ebd8 Git: Fix hunks being skipped when staging too quickly (#27552)
Release Notes:

- Git: Fix hunks being skipped when staging too quickly.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-27 09:00:48 -07:00
João Marcos
a3b0221957 Fix crash when staging a hunk that overlaps multiple unstaged hunks (#27545)
Release Notes:

- Git: Fix crash when staging a hunk that overlaps multiple unstaged
hunks.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-27 09:00:31 -07:00
Smit Barmase
74c06d44ca Revert "editor: Do not use hide_mouse_while_typing for single line editor" (#27547)
Reverts zed-industries/zed#27536

Looks like hiding cursor on single editor is okay and is default
behavior for other apps.
2025-03-26 19:58:45 -04:00
Smit Barmase
3733ce8394 editor: Do not use hide_mouse_while_typing for single line editor (#27536)
Release Notes:

- N/A
2025-03-26 17:42:44 -04:00
Smit Barmase
bb58f18e01 Hide the mouse when the user is typing in the editor - take 2 (#27519)
Closes #4461

Take 2 on https://github.com/zed-industries/zed/pull/25040. 

Fixes panic caused due to using `setHiddenUntilMouseMoves` return type
to `set` cursor on macOS.

Release Notes:

- Now cursor hides when the user is typing in editor. It will stay
hidden until it is moved again. This behavior is `true` by default, and
can be configured with `hide_mouse_while_typing` in settings.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Thomas Mickley-Doyle <thomas@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Angelk90 <angelo.k90@hotmail.it>
2025-03-26 17:42:31 -04:00
gcp-cherry-pick-bot[bot]
27e784bb0c Fix drawing of 0-width borders when quad has other borders (cherry-pick #27511) (#27517)
Cherry-picked Fix drawing of 0-width borders when quad has other borders
(#27511)

Closes #27485

Release Notes:

- N/A

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-03-26 20:57:32 +02:00
Joseph T. Lyons
8d39ba9b7b v0.180.x preview 2025-03-26 11:14:49 -04:00
47 changed files with 1314 additions and 392 deletions

2
Cargo.lock generated
View File

@@ -17272,7 +17272,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.180.0"
version = "0.180.2"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -155,6 +155,8 @@
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
"hide_mouse_while_typing": true,
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
@@ -620,6 +622,7 @@
// The model to use.
"model": "claude-3-5-sonnet-latest"
},
"default_profile": "code-writer",
"profiles": {
"read-only": {
"name": "Read-only",

View File

@@ -11,12 +11,12 @@ mod history_store;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod profile_selector;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_selector;
mod tool_use;
mod ui;
@@ -32,7 +32,7 @@ use prompt_store::PromptBuilder;
use settings::Settings as _;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::AddContextServerModal;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
@@ -47,6 +47,7 @@ actions!(
RemoveAllContext,
OpenHistory,
OpenConfiguration,
ManageProfiles,
AddContextServer,
RemoveSelectedThread,
Chat,
@@ -89,6 +90,7 @@ pub fn init(
cx,
);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_assistant2_actions(cx);
}

View File

@@ -1,4 +1,7 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod profile_picker;
mod tool_picker;
use std::sync::Arc;
@@ -12,6 +15,7 @@ use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
pub(crate) use add_context_server_modal::AddContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;

View File

@@ -0,0 +1,201 @@
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::ToolWorkingSet;
use fs::Fs;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
use settings::Settings as _;
use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles};
enum Mode {
ChooseProfile(Entity<ProfilePicker>),
ViewProfile(ViewProfileMode),
ConfigureTools(Entity<ToolPicker>),
}
#[derive(Clone)]
pub struct ViewProfileMode {
profile_id: Arc<str>,
configure_tools: NavigableEntry,
}
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
focus_handle: FocusHandle,
mode: Mode,
}
impl ManageProfilesModal {
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store().read(cx);
let tools = thread_store.tools();
workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
}
});
}
pub fn new(
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let handle = cx.entity();
Self {
fs,
tools,
focus_handle,
mode: Mode::ChooseProfile(cx.new(|cx| {
let delegate = ProfilePickerDelegate::new(
move |profile_id, window, cx| {
handle.update(cx, |this, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
},
cx,
);
ProfilePicker::new(delegate, window, cx)
})),
}
}
pub fn view_profile(
&mut self,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
configure_tools: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
}
fn configure_tools(
&mut self,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let settings = AssistantSettings::get_global(cx);
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
return;
};
self.mode = Mode::ConfigureTools(cx.new(|cx| {
let delegate = ToolPickerDelegate::new(
self.fs.clone(),
self.tools.clone(),
profile_id,
profile,
cx,
);
ToolPicker::new(delegate, window, cx)
}));
self.focus_handle(cx).focus(window);
}
fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
}
impl ModalView for ManageProfilesModal {}
impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.focus_handle(cx),
Mode::ConfigureTools(tool_picker) => tool_picker.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
}
}
}
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
impl ManageProfilesModal {
fn render_view_profile(
&mut self,
mode: ViewProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(
v_flex().child(
div()
.id("configure-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-tools")
.toggle_state(
mode.configure_tools
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Cog))
.child(Label::new("Configure Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
})
}),
),
),
)
.into_any_element(),
)
.entry(mode.configure_tools)
}
}
impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("ManageProfilesModal")
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(),
Mode::ViewProfile(mode) => self
.render_view_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::ConfigureTools(tool_picker) => tool_picker.clone().into_any_element(),
})
}
}

View File

@@ -0,0 +1,194 @@
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
Window,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
pub struct ProfilePicker {
picker: Entity<Picker<ProfilePickerDelegate>>,
}
impl ProfilePicker {
pub fn new(
delegate: ProfilePickerDelegate,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl EventEmitter<DismissEvent> for ProfilePicker {}
impl Focusable for ProfilePicker {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ProfilePicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug)]
pub struct ProfileEntry {
pub id: Arc<str>,
pub name: SharedString,
}
pub struct ProfilePickerDelegate {
profile_picker: WeakEntity<ProfilePicker>,
profiles: Vec<ProfileEntry>,
matches: Vec<StringMatch>,
selected_index: usize,
on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
}
impl ProfilePickerDelegate {
pub fn new(
on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
cx: &mut Context<ProfilePicker>,
) -> Self {
let settings = AssistantSettings::get_global(cx);
let profiles = settings
.profiles
.iter()
.map(|(id, profile)| ProfileEntry {
id: id.clone(),
name: profile.name.clone(),
})
.collect::<Vec<_>>();
Self {
profile_picker: cx.entity().downgrade(),
profiles,
matches: Vec::new(),
selected_index: 0,
on_confirm: Arc::new(on_confirm),
}
}
}
impl PickerDelegate for ProfilePickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search profiles…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.profiles
.iter()
.enumerate()
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(window, cx);
return;
}
let candidate_id = self.matches[self.selected_index].candidate_id;
let profile = &self.profiles[candidate_id];
(self.on_confirm)(&profile.id, window, cx);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.profile_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let profile_match = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
profile_match.string.clone(),
profile_match.positions.clone(),
)),
)
}
}

View File

@@ -0,0 +1,285 @@
use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt as _;
pub struct ToolPicker {
picker: Entity<Picker<ToolPickerDelegate>>,
}
impl ToolPicker {
pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl EventEmitter<DismissEvent> for ToolPicker {}
impl Focusable for ToolPicker {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ToolPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug, Clone)]
pub struct ToolEntry {
pub name: Arc<str>,
pub source: ToolSource,
}
pub struct ToolPickerDelegate {
tool_picker: WeakEntity<ToolPicker>,
fs: Arc<dyn Fs>,
tools: Vec<ToolEntry>,
profile_id: Arc<str>,
profile: AgentProfile,
matches: Vec<StringMatch>,
selected_index: usize,
}
impl ToolPickerDelegate {
pub fn new(
fs: Arc<dyn Fs>,
tool_set: Arc<ToolWorkingSet>,
profile_id: Arc<str>,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
) -> Self {
let mut tool_entries = Vec::new();
for (source, tools) in tool_set.tools_by_source(cx) {
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
name: tool.name().into(),
source: source.clone(),
}));
}
Self {
tool_picker: cx.entity().downgrade(),
fs,
tools: tool_entries,
profile_id,
profile,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ToolPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search tools…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.tools
.iter()
.enumerate()
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(window, cx);
return;
}
let candidate_id = self.matches[self.selected_index].candidate_id;
let tool = &self.tools[candidate_id];
let is_enabled = match &tool.source {
ToolSource::Native => {
let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default();
*is_enabled = !*is_enabled;
*is_enabled
}
ToolSource::ContextServer { id } => {
let preset = self
.profile
.context_servers
.entry(id.clone().into())
.or_default();
let is_enabled = preset.tools.entry(tool.name.clone()).or_default();
*is_enabled = !*is_enabled;
*is_enabled
}
};
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
let profile_id = self.profile_id.clone();
let default_profile = self.profile.clone();
let tool = tool.clone();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
settings,
)) => {
let profiles = settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
context_servers: default_profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
});
match tool.source {
ToolSource::Native => {
*profile.tools.entry(tool.name).or_default() = is_enabled;
}
ToolSource::ContextServer { id } => {
let preset = profile
.context_servers
.entry(id.clone().into())
.or_default();
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
}
}
}
_ => {}
}
});
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.tool_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let tool_match = &self.matches[ix];
let tool = &self.tools[tool_match.candidate_id];
let is_enabled = match &tool.source {
ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false),
ToolSource::ContextServer { id } => self
.profile
.context_servers
.get(id.as_ref())
.and_then(|preset| preset.tools.get(&tool.name))
.copied()
.unwrap_or(false),
};
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
h_flex()
.gap_2()
.child(HighlightedLabel::new(
tool_match.string.clone(),
tool_match.positions.clone(),
))
.map(|parent| match &tool.source {
ToolSource::Native => parent,
ToolSource::ContextServer { id } => parent
.child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)),
}),
)
.end_slot::<Icon>(is_enabled.then(|| {
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success)
})),
)
}
}

View File

@@ -26,9 +26,9 @@ use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::tool_selector::ToolSelector;
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
pub struct MessageEditor {
@@ -43,7 +43,7 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
tool_selector: Entity<ToolSelector>,
profile_selector: Entity<ProfileSelector>,
_subscriptions: Vec<Subscription>,
}
@@ -57,7 +57,6 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let tools = thread.read(cx).tools().clone();
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -129,14 +128,14 @@ impl MessageEditor {
inline_context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs,
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
window,
cx,
)
}),
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
_subscriptions: subscriptions,
}
}
@@ -624,7 +623,7 @@ impl Render for MessageEditor {
.child(
h_flex()
.justify_between()
.child(h_flex().gap_2().child(self.tool_selector.clone()))
.child(h_flex().gap_2().child(self.profile_selector.clone()))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message")

View File

@@ -0,0 +1,121 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use fs::Fs;
use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
use indexmap::IndexMap;
use settings::{update_settings_file, Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
use util::ResultExt as _;
use crate::{ManageProfiles, ThreadStore};
pub struct ProfileSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
_subscriptions: Vec<Subscription>,
}
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.refresh_profiles(cx);
});
let mut this = Self {
profiles: IndexMap::default(),
fs,
thread_store,
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
this
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
self.profiles = settings.profiles.clone();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
let icon_position = IconPosition::Start;
menu = menu.header("Profiles");
for (profile_id, profile) in self.profiles.clone() {
menu = menu.toggleable_entry(
profile.name.clone(),
profile_id == settings.default_profile,
icon_position,
None,
{
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
});
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(&profile_id, cx);
})
.log_err();
}
},
);
}
menu = menu.separator();
menu = menu.item(
ContextMenuEntry::new("Configure Profiles")
.icon(IconName::Pencil)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(ManageProfiles.boxed_clone(), cx);
}),
);
menu
})
}
}
impl Render for ProfileSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile = settings
.profiles
.get(&settings.default_profile)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
Button::new("profile-selector-button", profile)
.style(ButtonStyle::Filled)
.label_size(LabelSize::Small),
Tooltip::text("Change Profile"),
)
.anchor(gpui::Corner::BottomLeft)
}
}

View File

@@ -3,7 +3,8 @@ use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::{ToolId, ToolWorkingSet};
use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::manager::ContextServerManager;
@@ -19,6 +20,7 @@ use language_model::{LanguageModelToolUseId, Role};
use project::Project;
use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use util::ResultExt as _;
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
@@ -57,6 +59,7 @@ impl ThreadStore {
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
};
this.load_default_profile(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
@@ -184,6 +187,45 @@ impl ThreadStore {
})
}
fn load_default_profile(&self, cx: &Context<Self>) {
let assistant_settings = AssistantSettings::get_global(cx);
self.load_profile_by_id(&assistant_settings.default_profile, cx);
}
pub fn load_profile_by_id(&self, profile_id: &Arc<str>, cx: &Context<Self>) {
let assistant_settings = AssistantSettings::get_global(cx);
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
self.load_profile(profile);
}
}
pub fn load_profile(&self, profile: &AgentProfile) {
self.tools.disable_all_tools();
self.tools.enable(
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
for (context_server_id, preset) in &profile.context_servers {
self.tools.enable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
)
}
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),

View File

@@ -1,172 +0,0 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::{Entity, Subscription};
use indexmap::IndexMap;
use settings::{Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
pub struct ToolSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
tools: Arc<ToolWorkingSet>,
_subscriptions: Vec<Subscription>,
}
impl ToolSelector {
pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.refresh_profiles(cx);
});
let mut this = Self {
profiles: IndexMap::default(),
tools,
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
this
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
self.profiles = settings.profiles.clone();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let profiles = self.profiles.clone();
let tool_set = self.tools.clone();
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for (_id, profile) in profiles.clone() {
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
tools.disable_all_tools(cx);
tools.enable(
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
for (context_server_id, preset) in &profile.context_servers {
tools.enable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
)
}
}
});
}
menu = menu.separator();
let tools_by_source = tool_set.tools_by_source(cx);
let all_tools_enabled = tool_set.are_all_tools_enabled();
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
if all_tools_enabled {
tools.disable_all_tools(cx);
} else {
tools.enable_all_tools();
}
}
});
for (source, tools) in tools_by_source {
let mut tools = tools
.into_iter()
.map(|tool| {
let source = tool.source();
let name = tool.name().into();
let is_enabled = tool_set.is_enabled(&source, &name);
(source, name, is_enabled)
})
.collect::<Vec<_>>();
if ToolSource::Native == source {
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
}
menu = match &source {
ToolSource::Native => menu.separator().header("Zed Tools"),
ToolSource::ContextServer { id } => {
let all_tools_from_source_enabled =
tool_set.are_all_tools_from_source_enabled(&source);
menu.separator().header(id).toggleable_entry(
"All Tools",
all_tools_from_source_enabled,
icon_position,
None,
{
let tools = tool_set.clone();
let source = source.clone();
move |_window, cx| {
if all_tools_from_source_enabled {
tools.disable_source(source.clone(), cx);
} else {
tools.enable_source(&source);
}
}
},
)
}
};
for (source, name, is_enabled) in tools {
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, _cx| {
if is_enabled {
tools.disable(source.clone(), &[name.clone()]);
} else {
tools.enable(source.clone(), &[name.clone()]);
}
}
});
}
}
menu
})
}
}
impl Render for ToolSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
IconButton::new("tool-selector-button", IconName::SettingsAlt)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
Tooltip::text("Customize Tools"),
)
.anchor(gpui::Corner::BottomLeft)
}
}

View File

@@ -12,7 +12,7 @@ pub struct AgentProfile {
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}

View File

@@ -71,6 +71,7 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub default_profile: Arc<str>,
pub profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: bool,
@@ -174,6 +175,7 @@ impl AssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
@@ -198,6 +200,7 @@ impl AssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
@@ -307,6 +310,18 @@ impl AssistantSettingsContent {
}
}
}
pub fn set_profile(&mut self, profile_id: Arc<str>) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V2(settings) => {
settings.default_profile = Some(profile_id);
}
VersionedAssistantSettingsContent::V1(_) => {}
},
AssistantSettingsContent::Legacy(_) => {}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -330,6 +345,7 @@ impl Default for VersionedAssistantSettingsContent {
editor_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
@@ -370,7 +386,9 @@ pub struct AssistantSettingsContentV2 {
/// Default: false
enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)]
profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
default_profile: Option<Arc<str>>,
#[schemars(skip)]
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
@@ -424,7 +442,7 @@ pub struct AgentProfileContent {
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
@@ -531,6 +549,7 @@ impl Settings for AssistantSettings {
&mut settings.notify_when_agent_waiting,
value.notify_when_agent_waiting,
);
merge(&mut settings.default_profile, value.default_profile);
if let Some(profiles) = value.profiles {
settings
@@ -621,6 +640,7 @@ mod tests {
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,

View File

@@ -19,7 +19,7 @@ pub struct ToolWorkingSet {
struct WorkingSetState {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
next_tool_id: ToolId,
}
@@ -41,38 +41,23 @@ impl ToolWorkingSet {
self.state.lock().tools_by_source(cx)
}
pub fn are_all_tools_enabled(&self) -> bool {
let state = self.state.lock();
state.disabled_tools_by_source.is_empty()
}
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
let state = self.state.lock();
!state.disabled_tools_by_source.contains_key(source)
}
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
self.state.lock().enabled_tools(cx)
}
pub fn enable_all_tools(&self) {
pub fn disable_all_tools(&self) {
let mut state = self.state.lock();
state.disabled_tools_by_source.clear();
state.disable_all_tools();
}
pub fn disable_all_tools(&self, cx: &App) {
pub fn enable_source(&self, source: ToolSource, cx: &App) {
let mut state = self.state.lock();
state.disable_all_tools(cx);
state.enable_source(source, cx);
}
pub fn enable_source(&self, source: &ToolSource) {
pub fn disable_source(&self, source: &ToolSource) {
let mut state = self.state.lock();
state.enable_source(source);
}
pub fn disable_source(&self, source: ToolSource, cx: &App) {
let mut state = self.state.lock();
state.disable_source(source, cx);
state.disable_source(source);
}
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
@@ -159,40 +144,36 @@ impl WorkingSetState {
}
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
!self.is_disabled(source, name)
self.enabled_tools_by_source
.get(source)
.map_or(false, |enabled_tools| enabled_tools.contains(name))
}
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
self.disabled_tools_by_source
.get(source)
.map_or(false, |disabled_tools| disabled_tools.contains(name))
!self.is_enabled(source, name)
}
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
self.disabled_tools_by_source
self.enabled_tools_by_source
.entry(source)
.or_default()
.retain(|name| !tools_to_enable.contains(name));
.extend(tools_to_enable.into_iter().cloned());
}
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
self.disabled_tools_by_source
self.enabled_tools_by_source
.entry(source)
.or_default()
.extend(tools_to_disable.into_iter().cloned());
.retain(|name| !tools_to_disable.contains(name));
}
fn enable_source(&mut self, source: &ToolSource) {
self.disabled_tools_by_source.remove(source);
}
fn disable_source(&mut self, source: ToolSource, cx: &App) {
fn enable_source(&mut self, source: ToolSource, cx: &App) {
let tools_by_source = self.tools_by_source(cx);
let Some(tools) = tools_by_source.get(&source) else {
return;
};
self.disabled_tools_by_source.insert(
self.enabled_tools_by_source.insert(
source,
tools
.into_iter()
@@ -201,16 +182,11 @@ impl WorkingSetState {
);
}
fn disable_all_tools(&mut self, cx: &App) {
let tools = self.tools_by_source(cx);
fn disable_source(&mut self, source: &ToolSource) {
self.enabled_tools_by_source.remove(source);
}
for (source, tools) in tools {
let tool_names = tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<Vec<_>>();
self.disable(source, &tool_names);
}
fn disable_all_tools(&mut self) {
self.enabled_tools_by_source.clear();
}
}

View File

@@ -3,9 +3,7 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task};
use language::{Language, LanguageRegistry};
use rope::Rope;
use std::cmp::Ordering;
use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc};
use std::{cmp::Ordering, future::Future, iter, mem, ops::Range, sync::Arc};
use sum_tree::SumTree;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
use util::ResultExt;
@@ -195,7 +193,7 @@ impl BufferDiffInner {
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
) -> (Option<Rope>, SumTree<PendingHunk>) {
) -> Option<Rope> {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
@@ -208,7 +206,7 @@ impl BufferDiffInner {
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(index_text, head_text) => {
let (rope, new_status) = if stage {
let (new_index_text, new_status) = if stage {
log::debug!("stage all");
(
file_exists.then(|| buffer.as_rope().clone()),
@@ -228,15 +226,13 @@ impl BufferDiffInner {
buffer_version: buffer.version().clone(),
new_status,
};
let tree = SumTree::from_item(hunk, buffer);
return (rope, tree);
self.pending_hunks = SumTree::from_item(hunk, buffer);
return new_index_text;
}
};
let mut pending_hunks = SumTree::new(buffer);
let mut old_pending_hunks = unstaged_diff
.pending_hunks
.cursor::<DiffHunkSummary>(buffer);
let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
for DiffHunk {
@@ -261,7 +257,6 @@ impl BufferDiffInner {
old_pending_hunks.next(buffer);
}
// merge into pending hunks
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
@@ -288,56 +283,71 @@ impl BufferDiffInner {
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
for PendingHunk {
let mut prev_unstaged_hunk_buffer_end = 0;
let mut prev_unstaged_hunk_base_text_end = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
while let Some(PendingHunk {
buffer_range,
diff_base_byte_range,
..
} in pending_hunks.iter().cloned()
}) = pending_hunks_iter.next()
{
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
// Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
let skipped_unstaged =
unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
prev_unstaged_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_offset =
secondary_hunk.buffer_range.end.to_offset(buffer);
if let Some(unstaged_hunk) = skipped_unstaged.last() {
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
}
// Find where this hunk is in the index if it doesn't overlap
let mut buffer_offset_range = buffer_range.to_offset(buffer);
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_offset;
let mut index_start = prev_unstaged_hunk_base_text_offset + start_overshoot;
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
while let Some(unstaged_hunk) = unstaged_hunk_cursor.item().filter(|item| {
item.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
prev_unstaged_hunk_base_text_offset = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_offset = unstaged_hunk_offset_range.end;
loop {
// Merge this hunk with any overlapping unstaged hunks.
if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
buffer_offset_range.start = buffer_offset_range
.start
.min(unstaged_hunk_offset_range.start);
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
buffer_offset_range.start = buffer_offset_range
.start
.min(unstaged_hunk_offset_range.start);
buffer_offset_range.end =
buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
unstaged_hunk_cursor.next(buffer);
unstaged_hunk_cursor.next(buffer);
continue;
}
}
// If any unstaged hunks were merged, then subsequent pending hunks may
// now overlap this hunk. Merge them.
if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
let next_pending_hunk_offset_range =
next_pending_hunk.buffer_range.to_offset(buffer);
if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
buffer_offset_range.end = next_pending_hunk_offset_range.end;
pending_hunks_iter.next();
continue;
}
}
break;
}
let end_overshoot = buffer_offset_range
.end
.saturating_sub(prev_unstaged_hunk_buffer_offset);
let index_end = prev_unstaged_hunk_base_text_offset + end_overshoot;
let index_range = index_start..index_end;
buffer_offset_range.end = buffer_offset_range
.end
.max(prev_unstaged_hunk_buffer_offset);
.saturating_sub(prev_unstaged_hunk_buffer_end);
let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
let index_byte_range = index_start..index_end;
let replacement_text = if stage {
log::debug!("stage hunk {:?}", buffer_offset_range);
@@ -351,8 +361,11 @@ impl BufferDiffInner {
.collect::<String>()
};
edits.push((index_range, replacement_text));
edits.push((index_byte_range, replacement_text));
}
drop(pending_hunks_iter);
drop(old_pending_hunks);
self.pending_hunks = pending_hunks;
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
{
@@ -371,7 +384,7 @@ impl BufferDiffInner {
new_index_text.push(&replacement_text);
}
new_index_text.append(index_cursor.suffix());
(Some(new_index_text), pending_hunks)
Some(new_index_text)
}
fn hunks_intersecting_range<'a>(
@@ -408,15 +421,14 @@ impl BufferDiffInner {
]
});
let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
pending_hunks_cursor.next(buffer);
let mut secondary_cursor = None;
let mut pending_hunks_cursor = None;
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
secondary_cursor = Some(cursor);
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
pending_hunks_cursor = Some(cursor);
}
let max_point = buffer.max_point();
@@ -438,29 +450,27 @@ impl BufferDiffInner {
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut has_pending = false;
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
if start_anchor
.cmp(&pending_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
if start_anchor
.cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
}
if let Some(pending_hunk) = pending_hunks_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if let Some(pending_hunk) = pending_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
}
@@ -839,10 +849,8 @@ impl BufferDiff {
}
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if let Some(secondary_diff) = &self.secondary_diff {
secondary_diff.update(cx, |diff, _| {
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
});
if self.secondary_diff.is_some() {
self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX),
});
@@ -857,7 +865,7 @@ impl BufferDiff {
file_exists: bool,
cx: &mut Context<Self>,
) -> Option<Rope> {
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks_impl(
let new_index_text = self.inner.stage_or_unstage_hunks_impl(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
@@ -865,11 +873,6 @@ impl BufferDiff {
file_exists,
);
if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| {
diff.inner.pending_hunks = new_pending_hunks;
});
}
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
new_index_text.clone(),
));
@@ -1649,6 +1652,75 @@ mod tests {
"
.unindent(),
},
Example {
name: "one unstaged hunk that contains two uncommitted hunks",
head_text: "
one
two
three
four
"
.unindent(),
index_text: "
one
two
three
four
"
.unindent(),
buffer_marked_text: "
«one
three // modified
four»
"
.unindent(),
final_index_text: "
one
three // modified
four
"
.unindent(),
},
Example {
name: "one uncommitted hunk that contains two unstaged hunks",
head_text: "
one
two
three
four
five
"
.unindent(),
index_text: "
ZERO
one
TWO
THREE
FOUR
five
"
.unindent(),
buffer_marked_text: "
«one
TWO_HUNDRED
THREE
FOUR_HUNDRED
five»
"
.unindent(),
final_index_text: "
ZERO
one
TWO_HUNDRED
THREE
FOUR_HUNDRED
five
"
.unindent(),
},
];
for example in table {

View File

@@ -570,7 +570,7 @@ async fn test_following_tab_order(
client_a
.fs()
.insert_tree(
"/a",
util::path!("/a"),
json!({
"1.txt": "one",
"2.txt": "two",
@@ -578,7 +578,7 @@ async fn test_following_tab_order(
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(util::path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await

View File

@@ -790,6 +790,8 @@ pub struct Editor {
_scroll_cursor_center_top_bottom_task: Task<()>,
serialize_selections: Task<()>,
serialize_folds: Task<()>,
mouse_cursor_hidden: bool,
hide_mouse_while_typing: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1568,6 +1570,10 @@ impl Editor {
serialize_folds: Task::ready(()),
text_style_refinement: None,
load_diff_task: load_uncommitted_diff,
mouse_cursor_hidden: false,
hide_mouse_while_typing: EditorSettings::get_global(cx)
.hide_mouse_while_typing
.unwrap_or(true),
};
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
this._subscriptions
@@ -2999,6 +3005,8 @@ impl Editor {
return;
}
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let selections = self.selections.all_adjusted(cx);
let mut bracket_inserted = false;
let mut edits = Vec::new();
@@ -3403,6 +3411,7 @@ impl Editor {
}
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.transact(window, cx, |this, window, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
@@ -3518,6 +3527,8 @@ impl Editor {
}
pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
@@ -3575,6 +3586,8 @@ impl Editor {
}
pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
@@ -7765,6 +7778,7 @@ impl Editor {
}
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.transact(window, cx, |this, window, cx| {
this.select_autoclose_pair(window, cx);
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
@@ -7863,6 +7877,7 @@ impl Editor {
}
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.transact(window, cx, |this, window, cx| {
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
let line_mode = s.line_mode;
@@ -7884,7 +7899,7 @@ impl Editor {
if self.move_to_prev_snippet_tabstop(window, cx) {
return;
}
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
self.outdent(&Outdent, window, cx);
}
@@ -7892,7 +7907,7 @@ impl Editor {
if self.move_to_next_snippet_tabstop(window, cx) || self.read_only(cx) {
return;
}
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
let mut selections = self.selections.all_adjusted(cx);
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
@@ -16669,6 +16684,11 @@ impl Editor {
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default();
self.hide_mouse_while_typing = editor_settings.hide_mouse_while_typing.unwrap_or(true);
if !self.hide_mouse_while_typing {
self.mouse_cursor_hidden = false;
}
}
if old_cursor_shape != self.cursor_shape {

View File

@@ -39,6 +39,7 @@ pub struct EditorSettings {
#[serde(default)]
pub go_to_definition_fallback: GoToDefinitionFallback,
pub jupyter: Jupyter,
pub hide_mouse_while_typing: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -235,6 +236,10 @@ pub struct EditorSettingsContent {
///
/// Default: None
pub cursor_shape: Option<CursorShape>,
/// Determines whether the mouse cursor should be hidden while typing in an editor or input box.
///
/// Default: true
pub hide_mouse_while_typing: Option<bool>,
/// How to highlight the current line in the editor.
///
/// Default: all

View File

@@ -894,6 +894,7 @@ impl EditorElement {
let gutter_hovered = gutter_hitbox.is_hovered(window);
editor.set_gutter_hovered(gutter_hovered, cx);
editor.gutter_breakpoint_indicator = None;
editor.mouse_cursor_hidden = false;
if gutter_hovered {
let new_point = position_map
@@ -4307,7 +4308,7 @@ impl EditorElement {
let is_singleton = self.editor.read(cx).is_singleton(cx);
let line_height = layout.position_map.line_height;
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox));
for LineNumberLayout {
shaped_line,
@@ -4340,9 +4341,9 @@ impl EditorElement {
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox));
} else {
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox));
}
}
}
@@ -4564,7 +4565,7 @@ impl EditorElement {
.read(cx)
.all_diff_hunks_expanded()
{
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox));
}
}
}
@@ -4636,18 +4637,24 @@ impl EditorElement {
bounds: layout.position_map.text_hitbox.bounds,
}),
|window| {
let cursor_style = if self
.editor
.read(cx)
let editor = self.editor.read(cx);
if editor.mouse_cursor_hidden {
window.set_cursor_style(CursorStyle::None, None);
} else if editor
.hovered_link_state
.as_ref()
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
{
CursorStyle::PointingHand
window.set_cursor_style(
CursorStyle::PointingHand,
Some(&layout.position_map.text_hitbox),
);
} else {
CursorStyle::IBeam
window.set_cursor_style(
CursorStyle::IBeam,
Some(&layout.position_map.text_hitbox),
);
};
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
self.paint_lines_background(layout, window, cx);
let invisible_display_ranges = self.paint_highlights(layout, window);
@@ -4842,7 +4849,7 @@ impl EditorElement {
));
})
}
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
}
window.on_mouse_event({
@@ -6598,6 +6605,7 @@ impl Element for EditorElement {
},
false,
);
// Offset the content_bounds from the text_bounds by the gutter margin (which
// is roughly half a character wide) to make hit testing work more like how we want.
let content_origin =

View File

@@ -61,7 +61,7 @@ impl Render for WindowShadow {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

@@ -1617,7 +1617,7 @@ impl Interactivity {
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
window.set_cursor_style(mouse_cursor, hitbox);
window.set_cursor_style(mouse_cursor, Some(hitbox));
}
}

View File

@@ -700,7 +700,7 @@ impl Element for InteractiveText {
.iter()
.any(|range| range.contains(&ix))
{
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox))
}
}

View File

@@ -1228,6 +1228,9 @@ pub enum CursorStyle {
/// A cursor indicating that the operation will result in a context menu
/// corresponds to the CSS cursor value `context-menu`
ContextualMenu,
/// Hide the cursor
None,
}
impl Default for CursorStyle {

View File

@@ -532,6 +532,12 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
quad.border_widths.top,
center_to_point.y < 0.0));
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
// The purpose of this is to not draw antialiasing pixels in this case.
let reduced_border =
vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
select(border.y, -antialias_threshold, border.y == 0.0));
// Vector from the corner of the quad bounds to the point, after mirroring
// the point into the bottom right quadrant. Both components are <= 0.
let corner_to_point = abs(center_to_point) - half_size;
@@ -546,15 +552,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
corner_center_to_point.y >= 0;
// Vector from straight border inner corner to point.
let straight_border_inner_corner_to_point = corner_to_point + border;
let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
// Whether the point is beyond the inner edge of the straight border.
let is_beyond_inner_straight_border =
straight_border_inner_corner_to_point.x > 0 ||
straight_border_inner_corner_to_point.y > 0;
// Whether the point is far enough inside the straight border such that
// pixels are not affected by it.
// Whether the point is far enough inside the quad, such that the pixels are
// not affected by the straight border.
let is_within_inner_straight_border =
straight_border_inner_corner_to_point.x < -antialias_threshold &&
straight_border_inner_corner_to_point.y < -antialias_threshold;
@@ -589,11 +595,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
} else if (is_beyond_inner_straight_border) {
// Fast path for points that must be outside the inner edge.
inner_sdf = -1.0;
} else if (border.x == border.y) {
} else if (reduced_border.x == reduced_border.y) {
// Fast path for circular inner edge.
inner_sdf = -(outer_sdf + border.x);
inner_sdf = -(outer_sdf + reduced_border.x);
} else {
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - border);
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
}

View File

@@ -666,6 +666,12 @@ impl CursorStyle {
CursorStyle::DragLink => "alias",
CursorStyle::DragCopy => "copy",
CursorStyle::ContextualMenu => "context-menu",
CursorStyle::None => {
#[cfg(debug_assertions)]
panic!("CursorStyle::None should be handled separately in the client");
#[cfg(not(debug_assertions))]
"default"
}
}
.to_string()
}

View File

@@ -35,6 +35,12 @@ impl CursorStyle {
CursorStyle::DragLink => Shape::Alias,
CursorStyle::DragCopy => Shape::Copy,
CursorStyle::ContextualMenu => Shape::ContextMenu,
CursorStyle::None => {
#[cfg(debug_assertions)]
panic!("CursorStyle::None should be handled separately in the client");
#[cfg(not(debug_assertions))]
Shape::Default
}
}
}
}

View File

@@ -667,7 +667,13 @@ impl LinuxClient for WaylandClient {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
state.cursor_style = Some(style);
if let Some(cursor_shape_device) = &state.cursor_shape_device {
if let CursorStyle::None = style {
let wl_pointer = state
.wl_pointer
.clone()
.expect("window is focused by pointer");
wl_pointer.set_cursor(serial, None, 0, 0);
} else if let Some(cursor_shape_device) = &state.cursor_shape_device {
cursor_shape_device.set_shape(serial, style.to_shape());
} else if let Some(focused_window) = &state.mouse_focused_window {
// cursor-shape-v1 isn't supported, set the cursor using a surface.

View File

@@ -1438,13 +1438,16 @@ impl LinuxClient for X11Client {
let cursor = match state.cursor_cache.get(&style) {
Some(cursor) => *cursor,
None => {
let Some(cursor) = state
.cursor_handle
.load_cursor(&state.xcb_connection, &style.to_icon_name())
.log_err()
else {
let Some(cursor) = (match style {
CursorStyle::None => create_invisible_cursor(&state.xcb_connection).log_err(),
_ => state
.cursor_handle
.load_cursor(&state.xcb_connection, &style.to_icon_name())
.log_err(),
}) else {
return;
};
state.cursor_cache.insert(style, cursor);
cursor
}
@@ -1938,3 +1941,19 @@ fn make_scroll_wheel_event(
touch_phase: TouchPhase::default(),
}
}
fn create_invisible_cursor(
connection: &XCBConnection,
) -> anyhow::Result<crate::platform::linux::x11::client::xproto::Cursor> {
let empty_pixmap = connection.generate_id()?;
let root = connection.setup().roots[0].root;
connection.create_pixmap(1, empty_pixmap, root, 1, 1)?;
let cursor = connection.generate_id()?;
connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?;
connection.free_pixmap(empty_pixmap)?;
connection.flush()?;
Ok(cursor)
}

View File

@@ -891,6 +891,11 @@ impl Platform for MacPlatform {
/// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
fn set_cursor_style(&self, style: CursorStyle) {
unsafe {
if style == CursorStyle::None {
let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves:YES];
return;
}
let new_cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
@@ -925,6 +930,7 @@ impl Platform for MacPlatform {
CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
CursorStyle::None => unreachable!(),
};
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];

View File

@@ -133,6 +133,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom
);
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
// The purpose of this is to not draw antialiasing pixels in this case.
float2 reduced_border = float2(
border.x == 0.0 ? -antialias_threshold : border.x,
border.y == 0.0 ? -antialias_threshold : border.y);
// Vector from the corner of the quad bounds to the point, after mirroring
// the point into the bottom right quadrant. Both components are <= 0.
float2 corner_to_point = fabs(center_to_point) - half_size;
@@ -146,16 +152,20 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
corner_center_to_point.x >= 0.0 &&
corner_center_to_point.y >= 0.0;
// Vector from straight border inner corner to point
float2 straight_border_inner_corner_to_point = corner_to_point + border;
// Vector from straight border inner corner to point.
//
// 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near
// the border. Without this, antialiasing pixels would be drawn.
float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border;
// Whether the point is beyond the inner edge of the straight border
bool is_beyond_inner_straight_border =
straight_border_inner_corner_to_point.x > 0.0 ||
straight_border_inner_corner_to_point.y > 0.0;
// Whether the point is far enough inside the straight border such that
// pixels are not affected by it
// Whether the point is far enough inside the quad, such that the pixels are
// not affected by the straight border.
bool is_within_inner_straight_border =
straight_border_inner_corner_to_point.x < -antialias_threshold &&
straight_border_inner_corner_to_point.y < -antialias_threshold;
@@ -184,11 +194,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
} else if (is_beyond_inner_straight_border) {
// Fast path for points that must be outside the inner edge
inner_sdf = -1.0;
} else if (border.x == border.y) {
} else if (reduced_border.x == reduced_border.y) {
// Fast path for circular inner edge.
inner_sdf = -(outer_sdf + border.x);
inner_sdf = -(outer_sdf + reduced_border.x);
} else {
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - border);
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border);
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
}

View File

@@ -1127,7 +1127,19 @@ fn handle_nc_mouse_up_msg(
}
fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
state_ptr.state.borrow_mut().current_cursor = HCURSOR(lparam.0 as _);
let mut state = state_ptr.state.borrow_mut();
let had_cursor = state.current_cursor.is_some();
state.current_cursor = if lparam.0 == 0 {
None
} else {
Some(HCURSOR(lparam.0 as _))
};
if had_cursor != state.current_cursor.is_some() {
unsafe { SetCursor(state.current_cursor) };
}
Some(0)
}
@@ -1138,7 +1150,9 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
) {
return None;
}
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
unsafe {
SetCursor(state_ptr.state.borrow().current_cursor);
};
Some(1)
}

View File

@@ -54,7 +54,7 @@ pub(crate) struct WindowsPlatformState {
menus: Vec<OwnedMenu>,
dock_menu_actions: Vec<Box<dyn Action>>,
// NOTE: standard cursor handles don't need to close.
pub(crate) current_cursor: HCURSOR,
pub(crate) current_cursor: Option<HCURSOR>,
}
#[derive(Default)]
@@ -558,11 +558,11 @@ impl Platform for WindowsPlatform {
fn set_cursor_style(&self, style: CursorStyle) {
let hcursor = load_cursor(style);
let mut lock = self.state.borrow_mut();
if lock.current_cursor.0 != hcursor.0 {
if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) {
self.post_message(
WM_GPUI_CURSOR_STYLE_CHANGED,
WPARAM(0),
LPARAM(hcursor.0 as isize),
LPARAM(hcursor.map_or(0, |c| c.0 as isize)),
);
lock.current_cursor = hcursor;
}
@@ -683,7 +683,7 @@ impl Drop for WindowsPlatform {
pub(crate) struct WindowCreationInfo {
pub(crate) icon: HICON,
pub(crate) executor: ForegroundExecutor,
pub(crate) current_cursor: HCURSOR,
pub(crate) current_cursor: Option<HCURSOR>,
pub(crate) windows_version: WindowsVersion,
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<Runnable>,

View File

@@ -106,7 +106,7 @@ pub(crate) fn windows_credentials_target_name(url: &str) -> String {
format!("zed:url={}", url)
}
pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
pub(crate) fn load_cursor(style: CursorStyle) -> Option<HCURSOR> {
static ARROW: OnceLock<SafeCursor> = OnceLock::new();
static IBEAM: OnceLock<SafeCursor> = OnceLock::new();
static CROSS: OnceLock<SafeCursor> = OnceLock::new();
@@ -127,17 +127,20 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
| CursorStyle::ResizeUpDown
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
CursorStyle::None => return None,
_ => (&ARROW, IDC_ARROW),
};
*(*lock.get_or_init(|| {
HCURSOR(
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
.log_err()
.unwrap_or_default()
.0,
)
.into()
}))
Some(
*(*lock.get_or_init(|| {
HCURSOR(
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
.log_err()
.unwrap_or_default()
.0,
)
.into()
})),
)
}
/// This function is used to configure the dark mode for the window built-in title bar.

View File

@@ -48,7 +48,7 @@ pub struct WindowsWindowState {
pub click_state: ClickState,
pub system_settings: WindowsSystemSettings,
pub current_cursor: HCURSOR,
pub current_cursor: Option<HCURSOR>,
pub nc_button_pressed: Option<u32>,
pub display: WindowsDisplay,
@@ -76,7 +76,7 @@ impl WindowsWindowState {
hwnd: HWND,
transparent: bool,
cs: &CREATESTRUCTW,
current_cursor: HCURSOR,
current_cursor: Option<HCURSOR>,
display: WindowsDisplay,
gpu_context: &BladeContext,
) -> Result<Self> {
@@ -351,7 +351,7 @@ struct WindowCreateContext<'a> {
transparent: bool,
is_movable: bool,
executor: ForegroundExecutor,
current_cursor: HCURSOR,
current_cursor: Option<HCURSOR>,
windows_version: WindowsVersion,
validation_number: usize,
main_receiver: flume::Receiver<Runnable>,

View File

@@ -407,7 +407,7 @@ pub(crate) type AnyMouseListener =
#[derive(Clone)]
pub(crate) struct CursorStyleRequest {
pub(crate) hitbox_id: HitboxId,
pub(crate) hitbox_id: Option<HitboxId>, // None represents whole window
pub(crate) style: CursorStyle,
}
@@ -1928,10 +1928,10 @@ impl Window {
/// Updates the cursor style at the platform level. This method should only be called
/// during the prepaint phase of element drawing.
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) {
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: Option<&Hitbox>) {
self.invalidator.debug_assert_paint();
self.next_frame.cursor_styles.push(CursorStyleRequest {
hitbox_id: hitbox.id,
hitbox_id: hitbox.map(|hitbox| hitbox.id),
style,
});
}
@@ -2984,7 +2984,11 @@ impl Window {
.cursor_styles
.iter()
.rev()
.find(|request| request.hitbox_id.is_hovered(self))
.find(|request| {
request
.hitbox_id
.map_or(true, |hitbox_id| hitbox_id.is_hovered(self))
})
.map(|request| request.style)
.unwrap_or(CursorStyle::Arrow);
cx.platform.set_cursor_style(style);
@@ -3241,6 +3245,7 @@ impl Window {
keystroke,
&dispatch_path,
);
if !match_result.to_replay.is_empty() {
self.replay_pending_input(match_result.to_replay, cx)
}

View File

@@ -326,6 +326,13 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft);
self
}
/// Sets cursor style when hovering over an element to `none`.
/// [Docs](https://tailwindcss.com/docs/cursor)
#visibility fn cursor_none(mut self, cursor: CursorStyle) -> Self {
self.style().mouse_cursor = Some(gpui::CursorStyle::None);
self
}
};
output.into()

View File

@@ -411,9 +411,9 @@ impl MarkdownElement {
.is_some();
if is_hovering_link {
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
} else {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
}
self.on_mouse_event(window, cx, {

View File

@@ -84,6 +84,7 @@ struct BufferDiffState {
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
diff_updated_futures: Vec<oneshot::Sender<()>>,
hunk_staging_operation_count: usize,
head_text: Option<Arc<String>>,
index_text: Option<Arc<String>>,
@@ -500,7 +501,7 @@ impl GitStore {
}
}
let rx = diff_state.diff_bases_changed(text_snapshot, diff_bases_change, cx);
let rx = diff_state.diff_bases_changed(text_snapshot, diff_bases_change, 0, cx);
anyhow::Ok(async move {
rx.await.ok();
@@ -1008,7 +1009,11 @@ impl GitStore {
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
let buffer = buffer.read(cx).text_snapshot();
futures.push(diff_state.update(cx, |diff_state, cx| {
diff_state.recalculate_diffs(buffer, cx)
diff_state.recalculate_diffs(
buffer,
diff_state.hunk_staging_operation_count,
cx,
)
}));
}
}
@@ -1025,6 +1030,11 @@ impl GitStore {
) {
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
let buffer_id = diff.read(cx).buffer_id;
if let Some(diff_state) = self.diffs.get(&buffer_id) {
diff_state.update(cx, |diff_state, _| {
diff_state.hunk_staging_operation_count += 1;
});
}
if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
let recv = repo.update(cx, |repo, cx| {
log::debug!("updating index text for buffer {}", path.display());
@@ -1097,6 +1107,7 @@ impl GitStore {
file.path.clone(),
has_unstaged_diff.then(|| diff_state.index_text.clone()),
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
diff_state.hunk_staging_operation_count,
);
diff_state_updates.entry(repo_id).or_default().push(update);
}
@@ -1120,8 +1131,13 @@ impl GitStore {
};
let mut diff_bases_changes_by_buffer = Vec::new();
for (buffer, path, current_index_text, current_head_text) in
&repo_diff_state_updates
for (
buffer,
path,
current_index_text,
current_head_text,
hunk_staging_operation_count,
) in &repo_diff_state_updates
{
let Some(local_repo) = snapshot.local_repo_for_path(&path) else {
continue;
@@ -1174,12 +1190,18 @@ impl GitStore {
(false, false) => None,
};
diff_bases_changes_by_buffer.push((buffer, diff_bases_change))
diff_bases_changes_by_buffer.push((
buffer,
diff_bases_change,
*hunk_staging_operation_count,
))
}
git_store
.update(&mut cx, |git_store, cx| {
for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
for (buffer, diff_bases_change, hunk_staging_operation_count) in
diff_bases_changes_by_buffer
{
let Some(diff_state) =
git_store.diffs.get(&buffer.read(cx).remote_id())
else {
@@ -1224,6 +1246,7 @@ impl GitStore {
let _ = diff_state.diff_bases_changed(
buffer.text_snapshot(),
diff_bases_change,
hunk_staging_operation_count,
cx,
);
});
@@ -2013,7 +2036,11 @@ impl BufferDiffState {
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.language = buffer.read(cx).language().cloned();
self.language_changed = true;
let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
let _ = self.recalculate_diffs(
buffer.read(cx).text_snapshot(),
self.hunk_staging_operation_count,
cx,
);
}
fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
@@ -2046,7 +2073,12 @@ impl BufferDiffState {
},
};
let _ = self.diff_bases_changed(buffer, diff_bases_change, cx);
let _ = self.diff_bases_changed(
buffer,
diff_bases_change,
self.hunk_staging_operation_count,
cx,
);
}
pub fn wait_for_recalculation(&mut self) -> Option<oneshot::Receiver<()>> {
@@ -2062,6 +2094,7 @@ impl BufferDiffState {
&mut self,
buffer: text::BufferSnapshot,
diff_bases_change: DiffBasesChange,
prev_hunk_staging_operation_count: usize,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
match diff_bases_change {
@@ -2103,12 +2136,13 @@ impl BufferDiffState {
}
}
self.recalculate_diffs(buffer, cx)
self.recalculate_diffs(buffer, prev_hunk_staging_operation_count, cx)
}
fn recalculate_diffs(
&mut self,
buffer: text::BufferSnapshot,
prev_hunk_staging_operation_count: usize,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
log::debug!("recalculate diffs");
@@ -2168,6 +2202,12 @@ impl BufferDiffState {
}
}
if this.update(cx, |this, _| {
this.hunk_staging_operation_count > prev_hunk_staging_operation_count
})? {
return Ok(());
}
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
{

View File

@@ -3891,7 +3891,7 @@ impl LspStore {
*refcount += 1;
}
if !ignore_refcounts || *refcount == 1 {
if ignore_refcounts || *refcount == 1 {
local.register_buffer_with_language_servers(buffer, cx);
}
if !ignore_refcounts {

View File

@@ -886,9 +886,9 @@ impl Element for TerminalElement {
&& bounds.contains(&window.mouse_position())
&& self.terminal_view.read(cx).hover_target_tooltip.is_some()
{
window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(&layout.hitbox));
} else {
window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
window.set_cursor_style(gpui::CursorStyle::IBeam, Some(&layout.hitbox));
}
let cursor = layout.cursor.take();

View File

@@ -311,7 +311,7 @@ mod uniform_list {
});
let mut hovered_hitbox_id = None;
for (i, hitbox) in hitboxes.iter().enumerate() {
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(hitbox));
let indent_guide = &self.indent_guides[i];
let fill_color = if hitbox.is_hovered(window) {
hovered_hitbox_id = Some(hitbox.id);

View File

@@ -2,6 +2,7 @@ use crate::prelude::*;
use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle};
/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use
#[derive(IntoElement)]
pub struct Navigable {
child: AnyElement,
selectable_children: Vec<NavigableEntry>,

View File

@@ -1176,7 +1176,7 @@ mod element {
Axis::Vertical => CursorStyle::ResizeRow,
Axis::Horizontal => CursorStyle::ResizeColumn,
};
window.set_cursor_style(cursor_style, &handle.hitbox);
window.set_cursor_style(cursor_style, Some(&handle.hitbox));
window.paint_quad(gpui::fill(
handle.divider_bounds,
cx.theme().colors().pane_group_border,

View File

@@ -6702,7 +6702,7 @@ pub fn client_side_decorations(
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.180.0"
version = "0.180.2"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -553,6 +553,16 @@ List of `string` values
"cursor_shape": "hollow"
```
## Hide Mouse While Typing
- Description: Determines whether the mouse cursor should be hidden while typing in an editor or input box.
- Setting: `hide_mouse_while_typing`
- Default: `true`
**Options**
`boolean` values
## Editor Scrollbar
- Description: Whether or not to show the editor scrollbar and various elements in it.