branch_picker: Add button to filter remote branches (#54632)

This PR brings back the button to filter remote branches when accessing
the title bar's branch picker with the mouse. It was unintentionally
removed when we introduced the new worktree picker.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2026-04-23 15:26:44 -03:00 committed by GitHub
parent 398f430818
commit 0ab64d6414
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 1589 additions and 6097 deletions

View file

@ -160,7 +160,7 @@ jobs:
name: steps::bot_commit
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
with:
message: ${{ needs.resolve_versions.outputs.preview_branch }} preview for @${{ github.actor }}
message: ${{ needs.resolve_versions.outputs.preview_branch }} preview
ref: refs/heads/${{ needs.resolve_versions.outputs.preview_branch }}
files: crates/zed/RELEASE_CHANNEL
token: ${{ steps.generate-token.outputs.token }}
@ -206,7 +206,7 @@ jobs:
name: steps::bot_commit
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
with:
message: ${{ needs.resolve_versions.outputs.stable_branch }} stable for @${{ github.actor }}
message: ${{ needs.resolve_versions.outputs.stable_branch }} stable
ref: refs/heads/${{ needs.resolve_versions.outputs.stable_branch }}
files: crates/zed/RELEASE_CHANNEL
token: ${{ steps.generate-token.outputs.token }}

View file

@ -241,7 +241,7 @@ jobs:
timeout-minutes: 60
check_scripts:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-8x16-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd

View file

@ -705,7 +705,7 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_action_checks == 'true'
runs-on: namespace-profile-8x16-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd

6
Cargo.lock generated
View file

@ -1293,12 +1293,10 @@ dependencies = [
"gpui",
"markdown_preview",
"notifications",
"project",
"release_channel",
"semver",
"serde",
"serde_json",
"settings",
"smol",
"telemetry",
"ui",
@ -6308,7 +6306,6 @@ dependencies = [
"theme",
"theme_settings",
"ui",
"ui_input",
"util",
"workspace",
"zed_actions",
@ -6609,7 +6606,6 @@ dependencies = [
"serde",
"serde_json",
"smol",
"telemetry",
"tempfile",
"text",
"thiserror 2.0.17",
@ -7409,7 +7405,6 @@ dependencies = [
"smallvec",
"smol",
"strum 0.27.2",
"task",
"telemetry",
"theme",
"theme_settings",
@ -17771,7 +17766,6 @@ dependencies = [
"libc",
"log",
"parking_lot",
"percent-encoding",
"rand 0.9.3",
"regex",
"release_channel",

View file

@ -1,3 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8644 1.04416C12.2744 1.04416 12.6675 1.20702 12.9574 1.4969C13.2473 1.78679 13.4101 2.17996 13.4101 2.5899V14.183C13.4101 14.3184 13.3744 14.4513 13.3069 14.5685C13.2393 14.6859 13.1422 14.7833 13.0251 14.8513C12.9081 14.9192 12.7752 14.9551 12.6399 14.9556C12.5045 14.956 12.3715 14.921 12.2539 14.8538L8.76673 12.8614C8.53321 12.728 8.26896 12.6578 8.00004 12.6578C7.73111 12.6578 7.46686 12.728 7.23334 12.8614L3.74615 14.8538C3.62863 14.921 3.49553 14.956 3.36019 14.9556C3.22485 14.9551 3.092 14.9192 2.97493 14.8513C2.85787 14.7833 2.76069 14.6859 2.69312 14.5685C2.62556 14.4513 2.58997 14.3184 2.58994 14.183V2.5899C2.58994 2.17996 2.75278 1.78679 3.04267 1.4969C3.33255 1.20702 3.72572 1.04416 4.13568 1.04416H11.8644Z" fill="#C6CAD0"/>
<path d="M11.0885 2.44067C11.4162 2.44067 11.7304 2.57083 11.9621 2.80251C12.1938 3.0342 12.3239 3.34843 12.3239 3.67607V12.9416C12.3239 13.0498 12.2954 13.156 12.2414 13.2497C12.1874 13.3435 12.1098 13.4214 12.0162 13.4757C11.9227 13.53 11.8165 13.5587 11.7083 13.5591C11.6001 13.5594 11.4938 13.5314 11.3998 13.4777L8.61278 11.8853C8.42615 11.7787 8.21495 11.7226 8.00002 11.7226C7.78509 11.7226 7.57389 11.7787 7.38726 11.8853L4.6002 13.4777C4.50627 13.5314 4.3999 13.5594 4.29173 13.5591C4.18356 13.5587 4.07738 13.53 3.98382 13.4757C3.89026 13.4214 3.81259 13.3435 3.75859 13.2497C3.70459 13.156 3.67615 13.0498 3.67612 12.9416V3.67607C3.67612 3.34843 3.80627 3.0342 4.03796 2.80251C4.26964 2.57083 4.58387 2.44067 4.91152 2.44067H11.0885Z" stroke="#C6CAD0" stroke-width="1.11186" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.12" d="M11.0885 2.44067C11.4162 2.44067 11.7304 2.57083 11.9621 2.80251C12.1938 3.0342 12.3239 3.34843 12.3239 3.67607V12.9416C12.3239 13.0498 12.2954 13.156 12.2414 13.2497C12.1874 13.3435 12.1098 13.4214 12.0162 13.4757C11.9227 13.53 11.8165 13.5587 11.7083 13.5591C11.6001 13.5594 11.4938 13.5314 11.3998 13.4777L8.61278 11.8853C8.42615 11.7787 8.21495 11.7226 8.00002 11.7226C7.78509 11.7226 7.57389 11.7787 7.38726 11.8853L4.6002 13.4777C4.50627 13.5314 4.3999 13.5594 4.29173 13.5591C4.18356 13.5587 4.07738 13.53 3.98382 13.4757C3.89026 13.4214 3.81259 13.3435 3.75859 13.2497C3.70459 13.156 3.67615 13.0498 3.67612 12.9416V3.67607C3.67612 3.34843 3.80627 3.0342 4.03796 2.80251C4.26964 2.57083 4.58387 2.44067 4.91152 2.44067H11.0885Z" fill="#C6CAD0" stroke="#C6CAD0" stroke-width="1.11186" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -507,7 +507,6 @@
"shift-l": "pane::ActivateNextItem", // not a helix default
"g p": "pane::ActivatePreviousItem",
"shift-h": "pane::ActivatePreviousItem", // not a helix default
"g w": "vim::HelixJumpToWord",
"g .": "vim::HelixGotoLastModification",
"g o": "editor::ToggleSelectedDiffHunks", // Zed specific
"g shift-o": "git::ToggleStaged", // Zed specific
@ -546,6 +545,7 @@
"]": ["vim::PushHelixNext", { "around": true }],
"[": ["vim::PushHelixPrevious", { "around": true }],
"g q": "vim::PushRewrap",
"g w": "vim::PushRewrap", // not a helix default & clashes with helix `goto_word`
},
},
{

View file

@ -975,11 +975,6 @@
//
// Default: true
"diff_stats": true,
// Maximum length of the commit message title before a warning is shown.
// Set to 0 to disable.
//
// Default: 72
"commit_title_max_length": 72,
},
"message_editor": {
// Whether to automatically replace emoji shortcodes with emoji characters.
@ -1688,14 +1683,6 @@
"prompt_format": "infer",
"max_output_tokens": 64,
},
// Controls whether Zed may collect training data when using Zed's Edit Predictions.
// Data is only captured when the project is detected as open source.
// Possible values:
// - "default": use the preference previously set via the status-bar toggle,
// or false if no preference has been stored.
// - "yes": allow data collection for files in open-source projects.
// - "no": never allow data collection.
"allow_data_collection": "default",
},
// Settings specific to journaling
"journal": {

View file

@ -2125,7 +2125,7 @@ impl AcpThread {
let curr_status = mem::replace(&mut call.status, new_status);
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
respond_tx.send(outcome).ok();
respond_tx.send(outcome).log_err();
} else if cfg!(debug_assertions) {
panic!("tried to authorize an already authorized tool call");
}

View file

@ -96,18 +96,13 @@ impl MentionUri {
let path = url.path();
match url.scheme() {
"file" => {
let trimmed = if path_style.is_windows() {
let normalized = if path_style.is_windows() {
path.trim_start_matches("/")
} else {
path
};
let decoded = decode(trimmed).unwrap_or(Cow::Borrowed(trimmed));
let normalized: Cow<str> = if path_style.is_windows() {
Cow::Owned(decoded.replace('/', "\\"))
} else {
decoded
};
let path = normalized.as_ref();
let decoded = decode(normalized).unwrap_or(Cow::Borrowed(normalized));
let path = decoded.as_ref();
if let Some(fragment) = url.fragment() {
let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1);
@ -498,49 +493,6 @@ mod tests {
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_parse_file_uris_use_native_separators_on_windows() {
let parsed = MentionUri::parse("file:///C:/path/to/file.rs", PathStyle::Windows).unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs"));
}
other => panic!("Expected File variant, got {other:?}"),
}
let parsed = MentionUri::parse("file:///C:/path/to/dir/", PathStyle::Windows).unwrap();
match parsed {
MentionUri::Directory { abs_path } => {
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\dir\\"));
}
other => panic!("Expected Directory variant, got {other:?}"),
}
let parsed = MentionUri::parse(
"file:///C:/path/to/file.rs?symbol=MySymbol#L10:20",
PathStyle::Windows,
)
.unwrap();
match parsed {
MentionUri::Symbol { abs_path, .. } => {
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs"));
}
other => panic!("Expected Symbol variant, got {other:?}"),
}
let parsed =
MentionUri::parse("file:///C:/path/to/file.rs#L5:15", PathStyle::Windows).unwrap();
match parsed {
MentionUri::Selection {
abs_path: Some(abs_path),
..
} => {
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs"));
}
other => panic!("Expected Selection variant, got {other:?}"),
}
}
#[test]
fn test_to_directory_uri_without_slash() {
let uri = MentionUri::Directory {

View file

@ -1309,9 +1309,7 @@ impl NativeAgentConnection {
{
response
.send(outcome)
.map_err(|_| {
anyhow!("authorization receiver was dropped")
})
.map(|_| anyhow!("authorization receiver was dropped"))
.log_err();
}
})

View file

@ -189,15 +189,8 @@ impl AgentTool for GrepTool {
return Err("Search cancelled by user".to_string());
}
};
let (buffer, ranges) = match search_result {
Some(SearchResult::Buffer { buffer, ranges }) => (buffer, ranges),
Some(SearchResult::LimitReached) => {
has_more_matches = true;
break;
}
Some(SearchResult::WaitingForScan) => continue,
None => break,
let Some(SearchResult::Buffer { buffer, ranges }) = search_result else {
break;
};
if ranges.is_empty() {
continue;

View file

@ -7,7 +7,7 @@ use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, SplittableEditor, ToPoint,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
@ -40,7 +40,7 @@ use zed_actions::assistant::ToggleFocus;
pub struct AgentDiffPane {
multibuffer: Entity<MultiBuffer>,
editor: Entity<SplittableEditor>,
editor: Entity<Editor>,
thread: Entity<AcpThread>,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
@ -91,21 +91,15 @@ impl AgentDiffPane {
let project = thread.read(cx).project().clone();
let editor = cx.new(|cx| {
let workspace_entity = workspace.upgrade().expect("workspace must exist");
let diff_display_editor = SplittableEditor::new(
EditorSettings::get_global(cx).diff_view_style,
multibuffer.clone(),
project.clone(),
workspace_entity,
window,
cx,
);
diff_display_editor
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.disable_inline_diagnostics();
editor.set_expand_all_diff_hunks(cx);
editor
.set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx);
diff_display_editor.update_editors(cx, |editor, _cx| {
editor.register_addon(AgentDiffAddon);
});
diff_display_editor
editor.register_addon(AgentDiffAddon);
editor.disable_mouse_wheel_zoom();
editor
});
let action_log = thread.read(cx).action_log().clone();
@ -187,8 +181,7 @@ impl AgentDiffPane {
(was_empty, is_excerpt_newly_added)
});
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
rhs_editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, cx| {
if was_empty {
let first_hunk = editor
.diff_hunks_in_ranges(
@ -245,8 +238,7 @@ impl AgentDiffPane {
pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) {
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
rhs_editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, cx| {
let first_hunk = editor
.diff_hunks_in_ranges(
&[position..editor::Anchor::Max],
@ -265,16 +257,14 @@ impl AgentDiffPane {
}
fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context<Self>) {
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
rhs_editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
});
}
fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context<Self>) {
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
rhs_editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
reject_edits_in_selection(
editor,
@ -288,8 +278,7 @@ impl AgentDiffPane {
}
fn reject_all(&mut self, _: &RejectAll, window: &mut Window, cx: &mut Context<Self>) {
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
rhs_editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
reject_edits_in_ranges(
editor,
@ -562,10 +551,7 @@ impl Item for AgentDiffPane {
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor
.read(cx)
.rhs_editor()
.for_each_project_item(cx, f)
self.editor.for_each_project_item(cx, f)
}
fn set_nav_history(
@ -574,10 +560,8 @@ impl Item for AgentDiffPane {
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.rhs_editor().update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
@ -644,12 +628,14 @@ impl Item for AgentDiffPane {
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
cx: &'a App,
_: &'a App,
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.clone().into())
} else {
self.editor.act_as_type(type_id, cx)
None
}
}
@ -1813,7 +1799,7 @@ mod tests {
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use settings::{DiffViewStyle, SettingsStore};
use settings::SettingsStore;
use std::{path::Path, rc::Rc};
use util::path;
use workspace::{MultiWorkspace, PathList};
@ -1823,11 +1809,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
});
});
prompt_store::init(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
language_model::init(cx);
@ -1866,7 +1847,7 @@ mod tests {
let agent_diff = cx.new_window_entity(|window, cx| {
AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
});
let editor = agent_diff.read_with(cx, |diff, cx| diff.editor.read(cx).rhs_editor().clone());
let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone());
let buffer = project
.update(cx, |project, cx| project.open_buffer(buffer_path, cx))

View file

@ -33,7 +33,7 @@ use crate::DEFAULT_THREAD_TITLE;
use crate::ExpandMessageEditor;
use crate::ManageProfiles;
use crate::agent_connection_store::AgentConnectionStore;
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};
use crate::{
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
@ -708,7 +708,6 @@ pub struct AgentPanel {
show_trust_workspace_message: bool,
_base_view_observation: Option<Subscription>,
_draft_editor_observation: Option<Subscription>,
_thread_metadata_store_subscription: Subscription,
}
impl AgentPanel {
@ -1023,17 +1022,6 @@ impl AgentPanel {
}
_ => {}
});
let _thread_metadata_store_subscription = cx.subscribe(
&ThreadMetadataStore::global(cx),
|this, _store, event, cx| {
let ThreadMetadataStoreEvent::ThreadArchived(thread_id) = event;
if this.retained_threads.remove(thread_id).is_some() {
cx.notify();
}
},
);
let mut panel = Self {
workspace_id,
base_view,
@ -1067,7 +1055,6 @@ impl AgentPanel {
new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
_base_view_observation: None,
_draft_editor_observation: None,
_thread_metadata_store_subscription,
};
// Initial sync of agent servers from extensions
@ -1404,11 +1391,20 @@ impl AgentPanel {
) {
let session_id = action.from_session_id.clone();
let Some(content) = Self::initial_content_for_thread_summary(session_id.clone(), cx) else {
let Some(thread) = ThreadStore::global(cx)
.read(cx)
.entries()
.find(|t| t.id == session_id)
else {
log::error!("No session found for summarization with id {}", session_id);
return;
};
let Some(parent_session_id) = thread.parent_session_id else {
log::error!("Session {} has no parent session", session_id);
return;
};
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, window, cx| {
this.external_thread(
@ -1416,7 +1412,10 @@ impl AgentPanel {
None,
None,
None,
Some(content),
Some(AgentInitialContent::ThreadSummary {
session_id: parent_session_id,
title: Some(thread.title),
}),
true,
"agent_panel",
window,
@ -1428,21 +1427,6 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
fn initial_content_for_thread_summary(
session_id: acp::SessionId,
cx: &App,
) -> Option<AgentInitialContent> {
let thread = ThreadStore::global(cx)
.read(cx)
.entries()
.find(|t| t.id == session_id)?;
Some(AgentInitialContent::ThreadSummary {
session_id: thread.id,
title: Some(thread.title),
})
}
fn external_thread(
&mut self,
agent_choice: Option<crate::Agent>,
@ -5064,89 +5048,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_initial_content_for_thread_summary_uses_own_session_id(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
agent::ThreadStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
});
let source_session_id = acp::SessionId::new("source-thread-session");
let source_title: SharedString = "Source Thread Title".into();
let db_thread = agent::DbThread {
title: source_title.clone(),
messages: Vec::new(),
updated_at: Utc::now(),
detailed_summary: None,
initial_project_snapshot: None,
cumulative_token_usage: Default::default(),
request_token_usage: HashMap::default(),
model: None,
profile: None,
imported: false,
subagent_context: None,
speed: None,
thinking_enabled: false,
thinking_effort: None,
draft_prompt: None,
ui_scroll_position: None,
};
let thread_store = cx.update(|cx| ThreadStore::global(cx));
thread_store
.update(cx, |store, cx| {
store.save_thread(
source_session_id.clone(),
db_thread,
PathList::default(),
cx,
)
})
.await
.expect("saving source thread should succeed");
cx.run_until_parked();
thread_store.read_with(cx, |store, _cx| {
let entry = store
.thread_from_session_id(&source_session_id)
.expect("saved thread should be listed in the store");
assert!(
entry.parent_session_id.is_none(),
"saved thread is a root thread with no parent session"
);
});
let content = cx
.update(|cx| {
AgentPanel::initial_content_for_thread_summary(source_session_id.clone(), cx)
})
.expect("initial content should be produced for a root thread");
match content {
AgentInitialContent::ThreadSummary { session_id, title } => {
assert_eq!(
session_id, source_session_id,
"thread-summary mention should use the source thread's own session id"
);
assert_eq!(title, Some(source_title.clone()));
}
_ => panic!("expected AgentInitialContent::ThreadSummary"),
}
// Unknown session ids should still produce no content.
let missing = cx.update(|cx| {
AgentPanel::initial_content_for_thread_summary(
acp::SessionId::new("does-not-exist"),
cx,
)
});
assert!(
missing.is_none(),
"unknown session ids should not produce initial content"
);
}
#[gpui::test]
async fn test_cleanup_retained_threads_keeps_five_most_recent_idle_loadable_threads(
cx: &mut TestAppContext,

View file

@ -56,7 +56,7 @@ use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme_settings::{AgentBufferFontSize, AgentUiFontSize};
use theme_settings::AgentFontSize;
use ui::{
Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,
DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind,
@ -632,8 +632,7 @@ impl ConversationView {
let agent_server_store = project.read(cx).agent_server_store().clone();
let subscriptions = vec![
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
cx.observe_global_in::<AgentUiFontSize>(window, Self::agent_ui_font_size_changed),
cx.observe_global_in::<AgentBufferFontSize>(window, Self::agent_ui_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
cx.subscribe_in(
&agent_server_store,
window,

View file

@ -174,10 +174,6 @@ impl MentionSet {
self.mentions.values().map(|(uri, _)| uri.clone()).collect()
}
pub fn mention_uri_for_crease(&self, crease_id: &CreaseId) -> Option<MentionUri> {
self.mentions.get(crease_id).map(|(uri, _)| uri.clone())
}
pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {
self.mentions = mentions;
}

View file

@ -15,14 +15,12 @@ use anyhow::{Result, anyhow};
use editor::{
Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,
actions::{Copy, Paste},
code_context_menus::CodeContextMenu,
scroll::Autoscroll,
actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
};
use futures::{FutureExt as _, future::join_all};
use gpui::{
AppContext, ClipboardEntry, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, ImageFormat, KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
};
use language::{Buffer, language_settings::InlayHintKind};
use parking_lot::RwLock;
@ -1188,16 +1186,6 @@ impl MessageEditor {
cx.propagate();
}
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
let Some(text) = self.serialized_copy_text(cx) else {
cx.propagate();
return;
};
cx.stop_propagation();
cx.write_to_clipboard(ClipboardItem::new_string(text));
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
window.defer(cx, move |window, cx| {
@ -1777,76 +1765,6 @@ impl MessageEditor {
editor.set_text(text, window, cx);
});
}
fn serialized_copy_text(&self, cx: &mut App) -> Option<String> {
let display_snapshot = self
.editor
.update(cx, |editor, cx| editor.display_snapshot(cx));
let editor = self.editor.read(cx);
if !editor.has_non_empty_selection(&display_snapshot) {
return None;
}
let snapshot = editor.buffer().read(cx).snapshot(cx);
let mention_set = self.mention_set.read(cx);
let mention_ranges = display_snapshot
.crease_snapshot
.crease_items_with_offsets(&snapshot)
.into_iter()
.filter_map(|(crease_id, range)| {
mention_set.mention_uri_for_crease(&crease_id).map(|uri| {
(
range.start.to_offset(&snapshot),
range.end.to_offset(&snapshot),
uri,
)
})
})
.collect::<Vec<_>>();
let mut text = String::new();
let mut has_mentions = false;
let mut is_first = true;
for selection in editor
.selections
.all::<MultiBufferOffset>(&display_snapshot)
{
if is_first {
is_first = false;
} else {
text.push('\n');
}
let mut overlapping_mentions = mention_ranges
.iter()
.filter(|(start, end, _)| *start < selection.end && selection.start < *end)
.peekable();
if overlapping_mentions.peek().is_none() {
text.extend(snapshot.text_for_range(selection.start..selection.end));
continue;
}
has_mentions = true;
let mut cursor = selection.start;
for (start, end, uri) in overlapping_mentions {
if cursor < *start {
text.extend(snapshot.text_for_range(cursor..*start));
}
write!(text, "{}", uri.as_link()).unwrap();
cursor = *end;
}
if cursor < selection.end {
text.extend(snapshot.text_for_range(cursor..selection.end));
}
}
has_mentions.then_some(text)
}
}
impl Focusable for MessageEditor {
@ -1863,7 +1781,6 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::send_immediately))
.on_action(cx.listener(Self::chat_with_follow))
.on_action(cx.listener(Self::cancel))
.capture_action(cx.listener(Self::copy))
.on_action(cx.listener(Self::paste_raw))
.capture_action(cx.listener(Self::paste))
.flex_1()
@ -1998,10 +1915,10 @@ mod tests {
};
use fs::FakeFs;
use futures::{FutureExt as _, StreamExt as _};
use futures::StreamExt as _;
use gpui::{
AppContext, ClipboardEntry, ClipboardItem, Entity, EventEmitter, ExternalPaths,
FocusHandle, Focusable, Task, TestAppContext, VisualTestContext,
FocusHandle, Focusable, TestAppContext, VisualTestContext,
};
use language_model::LanguageModelRegistry;
use lsp::{CompletionContext, CompletionTriggerKind};
@ -2017,7 +1934,6 @@ mod tests {
use crate::completion_provider::PromptContextType;
use crate::{
conversation_view::tests::init_test,
mention_set::insert_crease_for_mention,
message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
};
@ -3932,318 +3848,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_copy_with_selection_mentions_serializes_links(cx: &mut TestAppContext) {
init_test(cx);
let (source_message_editor, _source_editor, mut cx) = setup_paste_test_message_editor(
json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}),
cx,
)
.await;
let workspace = source_message_editor.read_with(&cx, |message_editor, _| {
message_editor.workspace.upgrade().expect("workspace")
});
let project = workspace.read_with(&cx, |workspace, _| workspace.project().clone());
let source_text = "selection needs work\nselection looks fine";
let first_range = 0..9;
let second_start = "selection needs work\n".len();
let second_range = second_start..(second_start + "selection".len());
let first_uri = MentionUri::Selection {
abs_path: Some(path!("/project/file.rs").into()),
line_range: 0..=1,
};
let second_uri = MentionUri::Selection {
abs_path: Some(path!("/project/file.rs").into()),
line_range: 2..=3,
};
source_message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.set_text(source_text, window, cx);
let snapshot = message_editor
.editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx);
for (range, uri, content) in [
(
first_range.clone(),
first_uri.clone(),
"line 1\nline 2\n".to_string(),
),
(
second_range.clone(),
second_uri.clone(),
"line 3\nline 4\n".to_string(),
),
] {
let Some((crease_id, tx)) = insert_crease_for_mention(
snapshot
.anchor_to_buffer_anchor(
snapshot.anchor_before(MultiBufferOffset(range.start)),
)
.expect("selection mention anchor should map to a buffer")
.0,
range.len(),
uri.name().into(),
uri.icon_path(cx),
uri.tooltip_text(),
Some(uri.clone()),
Some(message_editor.workspace.clone()),
None,
message_editor.editor.clone(),
window,
cx,
) else {
panic!("expected mention crease insertion");
};
drop(tx);
message_editor.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(
crease_id,
uri,
Task::ready(Ok(Mention::Text {
content,
tracked_buffers: Vec::new(),
}))
.shared(),
);
});
}
let buffer_len = snapshot.len();
message_editor.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([MultiBufferOffset(0)..buffer_len]);
});
});
});
let copied_text = source_message_editor.update(&mut cx, |message_editor, cx| {
message_editor
.serialized_copy_text(cx)
.expect("selection mentions should serialize")
});
let expected_text = format!(
"{} needs work\n{} looks fine",
first_uri.as_link(),
second_uri.as_link()
);
assert_eq!(copied_text, expected_text);
let target_message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
let thread_store = cx.new(|cx| ThreadStore::new(cx));
let message_editor = cx.new(|cx| {
MessageEditor::new(
workspace_handle,
project.downgrade(),
Some(thread_store),
None,
Default::default(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
max_lines: None,
min_lines: 1,
},
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
true,
true,
None,
window,
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window, cx);
message_editor
});
cx.write_to_clipboard(ClipboardItem::new_string(copied_text));
target_message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.paste(&Paste, window, cx);
});
cx.run_until_parked();
let target_text = target_message_editor.read_with(&cx, |message_editor, cx| {
message_editor.editor.read(cx).text(cx)
});
assert_eq!(target_text, expected_text);
let contents = mention_contents(&target_message_editor, &mut cx).await;
assert_eq!(contents.len(), 2);
assert!(contents.iter().any(|(uri, _)| uri == &first_uri));
assert!(contents.iter().any(|(uri, _)| uri == &second_uri));
}
struct SelectionMentionFixture {
message_editor: Entity<MessageEditor>,
first_uri: MentionUri,
first_range: Range<usize>,
second_range: Range<usize>,
}
async fn setup_selection_mention_fixture(
cx: &mut TestAppContext,
) -> (SelectionMentionFixture, VisualTestContext) {
let (message_editor, _source_editor, mut cx) = setup_paste_test_message_editor(
json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}),
cx,
)
.await;
let source_text = "selection needs work\nselection looks fine";
let first_range = 0..9;
let second_start = "selection needs work\n".len();
let second_range = second_start..(second_start + "selection".len());
let first_uri = MentionUri::Selection {
abs_path: Some(path!("/project/file.rs").into()),
line_range: 0..=1,
};
let second_uri = MentionUri::Selection {
abs_path: Some(path!("/project/file.rs").into()),
line_range: 2..=3,
};
message_editor.update_in(&mut cx, |message_editor, window, cx| {
message_editor.set_text(source_text, window, cx);
let snapshot = message_editor
.editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx);
for (range, uri, content) in [
(
first_range.clone(),
first_uri.clone(),
"line 1\nline 2\n".to_string(),
),
(
second_range.clone(),
second_uri.clone(),
"line 3\nline 4\n".to_string(),
),
] {
let Some((crease_id, tx)) = insert_crease_for_mention(
snapshot
.anchor_to_buffer_anchor(
snapshot.anchor_before(MultiBufferOffset(range.start)),
)
.expect("selection mention anchor should map to a buffer")
.0,
range.len(),
uri.name().into(),
uri.icon_path(cx),
uri.tooltip_text(),
Some(uri.clone()),
Some(message_editor.workspace.clone()),
None,
message_editor.editor.clone(),
window,
cx,
) else {
panic!("expected mention crease insertion");
};
drop(tx);
message_editor.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(
crease_id,
uri,
Task::ready(Ok(Mention::Text {
content,
tracked_buffers: Vec::new(),
}))
.shared(),
);
});
}
});
(
SelectionMentionFixture {
message_editor,
first_uri,
first_range,
second_range,
},
cx,
)
}
#[gpui::test]
async fn test_serialized_copy_text_selection_covers_only_mention(cx: &mut TestAppContext) {
init_test(cx);
let (fixture, mut cx) = setup_selection_mention_fixture(cx).await;
fixture
.message_editor
.update_in(&mut cx, |message_editor, window, cx| {
let range = fixture.first_range.clone();
message_editor.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
]);
});
});
});
let copied = fixture
.message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.serialized_copy_text(cx)
});
assert_eq!(copied, Some(fixture.first_uri.as_link().to_string()));
}
#[gpui::test]
async fn test_serialized_copy_text_returns_none_when_mentions_outside_selection(
cx: &mut TestAppContext,
) {
init_test(cx);
let (fixture, mut cx) = setup_selection_mention_fixture(cx).await;
let between_start = fixture.first_range.end;
let between_end = fixture.second_range.start - 1;
fixture
.message_editor
.update_in(&mut cx, |message_editor, window, cx| {
message_editor.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([
MultiBufferOffset(between_start)..MultiBufferOffset(between_end)
]);
});
});
});
let copied = fixture
.message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.serialized_copy_text(cx)
});
assert_eq!(copied, None);
}
#[gpui::test]
async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
cx: &mut TestAppContext,

View file

@ -785,8 +785,6 @@ impl ThreadMetadataStore {
if let Some(job) = archive_job {
self.in_flight_archives.insert(thread_id, job);
}
cx.emit(ThreadMetadataStoreEvent::ThreadArchived(thread_id));
}
pub fn unarchive(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
@ -1230,13 +1228,6 @@ impl ThreadMetadataStore {
impl Global for ThreadMetadataStore {}
#[derive(Clone, Debug)]
pub enum ThreadMetadataStoreEvent {
ThreadArchived(ThreadId),
}
impl gpui::EventEmitter<ThreadMetadataStoreEvent> for ThreadMetadataStore {}
struct ThreadMetadataDb(ThreadSafeConnection);
impl Domain for ThreadMetadataDb {

View file

@ -764,20 +764,8 @@ pub enum ToolChoice {
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Thinking {
Enabled {
budget_tokens: Option<u32>,
},
Adaptive {
#[serde(default, skip_serializing_if = "Option::is_none")]
display: Option<AdaptiveThinkingDisplay>,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AdaptiveThinkingDisplay {
Omitted,
Summarized,
Enabled { budget_tokens: Option<u32> },
Adaptive,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, EnumString)]

View file

@ -11,9 +11,9 @@ use std::pin::Pin;
use std::str::FromStr;
use crate::{
AdaptiveThinkingDisplay, AnthropicError, AnthropicModelMode, CacheControl, CacheControlType,
ContentDelta, Event, ImageSource, Message, RequestContent, ResponseContent, StringOrContents,
Thinking, Tool, ToolChoice, ToolResultContent, ToolResultPart, Usage,
AnthropicError, AnthropicModelMode, CacheControl, CacheControlType, ContentDelta, Event,
ImageSource, Message, RequestContent, ResponseContent, StringOrContents, Thinking, Tool,
ToolChoice, ToolResultContent, ToolResultPart, Usage,
};
fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
@ -180,9 +180,7 @@ pub fn into_anthropic(
AnthropicModelMode::Thinking { budget_tokens } => {
Some(Thinking::Enabled { budget_tokens })
}
AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive {
display: Some(AdaptiveThinkingDisplay::Summarized),
}),
AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive),
AnthropicModelMode::Default => None,
}
} else {

View file

@ -124,7 +124,7 @@ impl AskPassSession {
ControlFlow::Continue(Ok(password))
} else {
if let Some(kill_tx) = kill_tx.lock().await.take() {
kill_tx.send(()).ok();
kill_tx.send(()).log_err();
}
ControlFlow::Break(())
}

View file

@ -17,17 +17,15 @@ anyhow.workspace = true
auto_update.workspace = true
client.workspace = true
db.workspace = true
editor.workspace = true
fs.workspace = true
editor.workspace = true
notifications.workspace = true
gpui.workspace = true
markdown_preview.workspace = true
notifications.workspace = true
project.workspace = true
release_channel.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
telemetry.workspace = true
ui.workspace = true

View file

@ -13,7 +13,6 @@ use notifications::status_toast::StatusToast;
use release_channel::{AppVersion, ReleaseChannel};
use semver::Version;
use serde::Deserialize;
use settings::Settings as _;
use smol::io::AsyncReadExt;
use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*};
use util::{ResultExt as _, maybe};
@ -205,10 +204,7 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
}
};
if *version >= version_with_parallel_agents
&& !ParallelAgentAnnouncement::dismissed(cx)
&& !project::DisableAiSettings::get_global(cx).disable_ai
{
if *version >= version_with_parallel_agents && !ParallelAgentAnnouncement::dismissed(cx) {
let fs = <dyn Fs>::global(cx);
Some(AnnouncementContent {
heading: "Introducing Parallel Agents".into(),

View file

@ -58,13 +58,8 @@ pub async fn stream_completion(
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
}
Some(Thinking::Adaptive { effort: _ }) => {
let thinking_config = HashMap::from([
("type".to_string(), Document::String("adaptive".to_string())),
(
"display".to_string(),
Document::String("summarized".to_string()),
),
]);
let thinking_config =
HashMap::from([("type".to_string(), Document::String("adaptive".to_string()))]);
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
}
_ => {}

View file

@ -740,21 +740,6 @@ impl UserStore {
.get(&current_organization.id)
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_current_organization_configuration_for_test(
&mut self,
organization: Arc<Organization>,
configuration: OrganizationConfiguration,
cx: &mut Context<Self>,
) {
self.current_organization = Some(organization.clone());
self.organizations = vec![organization.clone()];
self.configuration_by_organization
.insert(organization.id.clone(), configuration);
cx.emit(Event::OrganizationChanged);
cx.notify();
}
pub fn plan(&self) -> Option<Plan> {
#[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {

View file

@ -5295,7 +5295,6 @@ async fn test_project_search(
"Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call."
)
}
SearchResult::WaitingForScan => {}
};
}

View file

@ -336,28 +336,17 @@ fn is_missing_action(name: &str) -> bool {
actions_available() && find_action_by_name(name).is_none()
}
// Find the last binding (in keymap order) for the given action.
// Exact action matches are preferred over parameterized variants.
// Find the binding in reverse order, as the last binding takes precedence.
fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> {
let find = |predicate: &dyn Fn(&str) -> bool| {
keymap.sections().rev().find_map(|section| {
section.bindings().rev().find_map(|(keystroke, a)| {
if predicate(&a.to_string()) {
Some(keystroke.to_string())
} else {
None
}
})
keymap.sections().rev().find_map(|section| {
section.bindings().rev().find_map(|(keystroke, a)| {
if name_for_action(a.to_string()) == action {
Some(keystroke.to_string())
} else {
None
}
})
};
// Look for exact match
if let Some(binding) = find(&|a| a == action) {
return Some(binding);
}
// Look for parameterized match
find(&|a| name_for_action(a.to_string()) == action)
})
}
fn find_binding(os: Os, action: &str) -> Option<String> {
@ -883,68 +872,3 @@ fn keymap_schema_for_actions(
&deprecation_messages,
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_find_binding_prefers_exact_match_over_parameterized() {
let keymap: KeymapFile = serde_json::from_value(json!([
{
"bindings": {
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
}
}
]))
.unwrap();
let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher");
assert_eq!(binding.as_deref(), Some("ctrl-tab"));
}
#[test]
fn test_find_binding_falls_back_to_parameterized_match() {
let keymap: KeymapFile = serde_json::from_value(json!([
{
"bindings": {
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
}
}
]))
.unwrap();
let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher");
assert_eq!(binding.as_deref(), Some("ctrl-shift-tab"));
}
#[test]
fn test_find_binding_prefers_exact_match_regardless_of_order() {
let keymap: KeymapFile = serde_json::from_value(json!([
{
"bindings": {
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher"
}
}
]))
.unwrap();
let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher");
assert_eq!(binding.as_deref(), Some("ctrl-tab"));
}
#[test]
fn test_find_binding_later_section_overrides_earlier() {
let keymap: KeymapFile = serde_json::from_value(json!([
{ "bindings": { "ctrl-a": "some::Action" } },
{ "bindings": { "ctrl-b": "some::Action" } }
]))
.unwrap();
let binding = find_binding_in_keymap(&keymap, "some::Action");
assert_eq!(binding.as_deref(), Some("ctrl-b"));
}
}

View file

@ -40,8 +40,7 @@ use release_channel::AppVersion;
use semver::Version;
use serde::de::DeserializeOwned;
use settings::{
EditPredictionDataCollectionChoice, EditPredictionPromptFormat, EditPredictionProvider,
Settings as _, update_settings_file,
EditPredictionPromptFormat, EditPredictionProvider, Settings as _, update_settings_file,
};
use std::collections::{VecDeque, hash_map};
use std::env;
@ -152,7 +151,7 @@ pub struct EditPredictionStore {
preferred_experiment: Option<String>,
available_experiments: Vec<String>,
pub mercury: Mercury,
legacy_data_collection_enabled: bool,
data_collection_choice: DataCollectionChoice,
reject_predictions_tx: mpsc::UnboundedSender<EditPredictionRejectionPayload>,
settled_predictions_tx: mpsc::UnboundedSender<Instant>,
shown_predictions: VecDeque<EditPrediction>,
@ -758,8 +757,9 @@ impl EditPredictionStore {
}
pub fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
let data_collection_choice = Self::load_data_collection_choice(cx);
let llm_token = global_llm_token(cx);
let legacy_data_collection_enabled = Self::load_legacy_data_collection_enabled(cx);
let (reject_tx, reject_rx) = mpsc::unbounded();
cx.background_spawn({
@ -814,8 +814,8 @@ impl EditPredictionStore {
preferred_experiment: None,
available_experiments: Vec::new(),
mercury: Mercury::new(cx),
legacy_data_collection_enabled,
data_collection_choice,
reject_predictions_tx: reject_tx,
settled_predictions_tx,
rated_predictions: Default::default(),
@ -2770,45 +2770,38 @@ impl EditPredictionStore {
}
pub(crate) fn is_data_collection_enabled(&self, cx: &App) -> bool {
if !self.is_data_collection_allowed_by_organization(cx) {
return false;
}
if cx.is_staff() {
return true;
}
match all_language_settings(None, cx)
.edit_predictions
.allow_data_collection
{
EditPredictionDataCollectionChoice::Yes => true,
EditPredictionDataCollectionChoice::No => false,
// Fall back to the legacy KV entry captured when the store was
// created, preserving existing users' choices without per-request
// database reads.
EditPredictionDataCollectionChoice::Default => self.legacy_data_collection_enabled,
}
self.data_collection_choice.is_enabled(cx)
}
fn load_legacy_data_collection_enabled(cx: &App) -> bool {
KeyValueStore::global(cx)
fn load_data_collection_choice(cx: &App) -> DataCollectionChoice {
let choice = KeyValueStore::global(cx)
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
.log_err()
.flatten()
.as_deref()
== Some("true")
.flatten();
match choice.as_deref() {
Some("true") => DataCollectionChoice::Enabled,
Some("false") => DataCollectionChoice::Disabled,
Some(_) => {
log::error!("unknown value in '{ZED_PREDICT_DATA_COLLECTION_CHOICE}'");
DataCollectionChoice::NotAnswered
}
None => DataCollectionChoice::NotAnswered,
}
}
pub(crate) fn is_data_collection_allowed_by_organization(&self, cx: &App) -> bool {
self.user_store
.read(cx)
.current_organization_configuration()
.is_none_or(|organization_configuration| {
organization_configuration
.edit_prediction
.is_feedback_enabled
})
fn toggle_data_collection_choice(&mut self, cx: &mut Context<Self>) {
self.data_collection_choice = self.data_collection_choice.toggle();
let new_choice = self.data_collection_choice;
let is_enabled = new_choice.is_enabled(cx);
let kvp = KeyValueStore::global(cx);
db::write_and_log(cx, move || async move {
kvp.write_kvp(
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
is_enabled.to_string(),
)
.await
});
}
pub fn shown_predictions(&self) -> impl DoubleEndedIterator<Item = &EditPrediction> {
@ -3001,37 +2994,70 @@ pub struct ZedUpdateRequiredError {
minimum_version: Version,
}
struct ZedPredictUpsell;
#[derive(Debug, Clone, Copy)]
pub enum DataCollectionChoice {
NotAnswered,
Enabled,
Disabled,
}
fn is_upsell_dismissed(cx: &App) -> bool {
// To make this backwards compatible with older versions of Zed, we
// check if the user has seen the previous Edit Prediction Onboarding
// before, by checking the data collection choice which was written to
// the database once the user clicked on "Accept and Enable"
let kvp = KeyValueStore::global(cx);
if kvp
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
.log_err()
.is_some_and(|s| s.is_some())
{
return true;
impl DataCollectionChoice {
pub fn is_enabled(self, cx: &App) -> bool {
if cx.is_staff() {
return true;
}
match self {
Self::Enabled => true,
Self::NotAnswered | Self::Disabled => false,
}
}
kvp.read_kvp(ZedPredictUpsell::KEY)
.log_err()
.is_some_and(|s| s.is_some())
#[must_use]
pub fn toggle(&self) -> DataCollectionChoice {
match self {
Self::Enabled => Self::Disabled,
Self::Disabled => Self::Enabled,
Self::NotAnswered => Self::Enabled,
}
}
}
impl From<bool> for DataCollectionChoice {
fn from(value: bool) -> Self {
match value {
true => DataCollectionChoice::Enabled,
false => DataCollectionChoice::Disabled,
}
}
}
struct ZedPredictUpsell;
impl Dismissable for ZedPredictUpsell {
const KEY: &'static str = "dismissed-edit-predict-upsell";
fn dismissed(cx: &App) -> bool {
is_upsell_dismissed(cx)
// To make this backwards compatible with older versions of Zed, we
// check if the user has seen the previous Edit Prediction Onboarding
// before, by checking the data collection choice which was written to
// the database once the user clicked on "Accept and Enable"
let kvp = KeyValueStore::global(cx);
if kvp
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
.log_err()
.is_some_and(|s| s.is_some())
{
return true;
}
kvp.read_kvp(Self::KEY)
.log_err()
.is_some_and(|s| s.is_some())
}
}
pub fn should_show_upsell_modal(cx: &App) -> bool {
!is_upsell_dismissed(cx)
!ZedPredictUpsell::dismissed(cx)
}
pub fn init(cx: &mut App) {

View file

@ -3,16 +3,11 @@ use crate::udiff::apply_diff_to_string;
use client::{RefreshLlmTokenListener, UserStore, test::FakeServer};
use clock::FakeSystemClock;
use clock::ReplicaId;
use cloud_api_types::{
CreateLlmTokenResponse, LlmToken, Organization, OrganizationConfiguration,
OrganizationEditPredictionConfiguration, OrganizationId,
};
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
use cloud_llm_client::{
EditPredictionRejectReason, EditPredictionRejection, RejectEditPredictionsBody,
predict_edits_v3::{PredictEditsV3Request, PredictEditsV3Response},
};
use db::AppDatabase;
use settings::EditPredictionDataCollectionChoice;
use futures::{
AsyncReadExt, FutureExt, StreamExt,
@ -2395,31 +2390,12 @@ struct RequestChannels {
fn init_test_with_fake_client(
cx: &mut TestAppContext,
) -> (Entity<EditPredictionStore>, RequestChannels) {
init_test_with_fake_client_and_legacy_data_collection(cx, None)
}
fn init_test_with_fake_client_and_legacy_data_collection(
cx: &mut TestAppContext,
legacy_data_collection_choice: Option<&str>,
) -> (Entity<EditPredictionStore>, RequestChannels) {
cx.update(move |cx| {
cx.set_global(AppDatabase::test_new());
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
zlog::init_test();
if let Some(legacy_data_collection_choice) = legacy_data_collection_choice {
KeyValueStore::global(cx)
.write_kvp(
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
legacy_data_collection_choice.to_string(),
)
.now_or_never()
.expect("legacy data collection write should complete immediately")
.expect("legacy data collection write should succeed");
}
let (predict_req_tx, predict_req_rx) = mpsc::unbounded();
let (reject_req_tx, reject_req_rx) = mpsc::unbounded();
@ -2796,7 +2772,6 @@ async fn test_v3_prediction_strips_cursor_marker_from_edit_text(cx: &mut TestApp
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_global(AppDatabase::test_new());
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
});
@ -3417,252 +3392,6 @@ async fn test_edit_prediction_settled(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_data_collection_disabled_by_default(cx: &mut TestAppContext) {
let (ep_store, _channels) = init_test_with_fake_client(cx);
cx.update(|cx| {
assert!(!ep_store.read(cx).is_data_collection_enabled(cx));
});
}
#[gpui::test]
async fn test_data_collection_enabled_via_legacy_kv_store(cx: &mut TestAppContext) {
let (ep_store, _channels) =
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
cx.update(|cx| {
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
});
}
#[gpui::test]
async fn test_data_collection_default_uses_cached_legacy_value(cx: &mut TestAppContext) {
let (ep_store, _channels) =
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
cx.update(|cx| {
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
});
cx.update(|cx| KeyValueStore::global(cx))
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
.await
.unwrap();
cx.update(|cx| {
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
});
}
#[gpui::test]
async fn test_data_collection_setting_overrides_kv_store(cx: &mut TestAppContext) {
let (ep_store, _channels) =
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
// An explicit false in settings.json wins over the KV store.
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings(cx, |content| {
content
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.allow_data_collection = Some(EditPredictionDataCollectionChoice::No);
});
});
cx.update(|cx| {
assert!(!ep_store.read(cx).is_data_collection_enabled(cx));
});
}
#[gpui::test]
async fn test_data_collection_enabled_via_setting(cx: &mut TestAppContext) {
let (ep_store, _channels) = init_test_with_fake_client(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings(cx, |content| {
content
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.allow_data_collection = Some(EditPredictionDataCollectionChoice::Yes);
});
});
cx.update(|cx| {
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
});
}
#[gpui::test]
async fn test_data_collection_always_enabled_for_staff(cx: &mut TestAppContext) {
let (ep_store, _channels) = init_test_with_fake_client(cx);
cx.update(|cx| {
cx.set_staff(true);
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
});
}
#[gpui::test]
async fn test_data_collection_disabled_by_organization_configuration(cx: &mut TestAppContext) {
let (ep_store, _channels) = init_test_with_fake_client(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings(cx, |content| {
content
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.allow_data_collection = Some(EditPredictionDataCollectionChoice::Yes);
});
});
let user_store = cx.update(|cx| ep_store.read(cx).user_store.clone());
cx.update(|cx| {
user_store.update(cx, |user_store, cx| {
user_store.set_current_organization_configuration_for_test(
Arc::new(Organization {
id: OrganizationId("org-1".into()),
name: "Org 1".into(),
is_personal: false,
}),
OrganizationConfiguration {
is_zed_model_provider_enabled: true,
is_agent_thread_feedback_enabled: true,
is_collaboration_enabled: true,
edit_prediction: OrganizationEditPredictionConfiguration {
is_enabled: true,
is_feedback_enabled: false,
},
},
cx,
);
});
assert!(!ep_store.read(cx).is_data_collection_enabled(cx));
});
}
// When a user had data collection enabled via the legacy KV store (with no explicit
// setting in settings.json), toggle_data_collection must read the *resolved* state
// (true) and write Some(false).
#[gpui::test]
async fn test_toggle_data_collection_from_kv_enabled_state(cx: &mut TestAppContext) {
let (ep_store, _channels) =
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
cx.update(|cx| {
assert!(
ep_store.read(cx).is_data_collection_enabled(cx),
"data collection should be enabled via KV store before toggle"
);
});
// Simulate what toggle_data_collection does: capture the resolved current
// state, then write its inverse.
let is_currently_enabled = cx.update(|cx| ep_store.read(cx).is_data_collection_enabled(cx));
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings(cx, |content| {
content
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.allow_data_collection = Some(if is_currently_enabled {
EditPredictionDataCollectionChoice::No
} else {
EditPredictionDataCollectionChoice::Yes
});
});
});
cx.update(|cx| {
assert!(
!ep_store.read(cx).is_data_collection_enabled(cx),
"data collection should be disabled after toggling off from KV-enabled state"
);
});
}
#[gpui::test]
async fn test_upsell_shown_by_default(cx: &mut TestAppContext) {
init_test(cx);
let kvp = cx.update(|cx| KeyValueStore::global(cx));
kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
.await
.ok();
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.ok();
cx.update(|cx| assert!(should_show_upsell_modal(cx)));
}
#[gpui::test]
async fn test_upsell_dismissed_when_data_collection_choice_in_kv_store(cx: &mut TestAppContext) {
init_test(cx);
// Any value for the data collection key means the old upsell was already
// shown, regardless of whether data collection was accepted or declined.
for value in &["true", "false"] {
cx.update(|cx| KeyValueStore::global(cx))
.write_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), value.to_string())
.await
.unwrap();
cx.update(|cx| {
assert!(
!should_show_upsell_modal(cx),
"upsell should be suppressed when data collection choice is '{value}'"
);
});
}
cx.update(|cx| KeyValueStore::global(cx))
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
.await
.unwrap();
}
#[gpui::test]
async fn test_upsell_dismissed_when_dismissed_key_set(cx: &mut TestAppContext) {
init_test(cx);
let kvp = cx.update(|cx| KeyValueStore::global(cx));
kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
.await
.ok();
kvp.write_kvp(ZedPredictUpsell::KEY.into(), "1".into())
.await
.unwrap();
cx.update(|cx| assert!(!should_show_upsell_modal(cx)));
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.unwrap();
}
#[gpui::test]
async fn test_upsell_dismissed_via_dismissable_api(cx: &mut TestAppContext) {
init_test(cx);
let kvp = cx.update(|cx| KeyValueStore::global(cx));
kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
.await
.ok();
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.ok();
cx.update(|cx| {
assert!(should_show_upsell_modal(cx));
ZedPredictUpsell::set_dismissed(true, cx);
});
cx.run_until_parked();
cx.update(|cx| assert!(!should_show_upsell_modal(cx)));
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.unwrap();
}
#[ctor::ctor]
fn init_logger() {
zlog::init_test();

View file

@ -7,11 +7,9 @@ use edit_prediction_types::{
EditPredictionIconSet, SuggestionDisplayType,
};
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{App, Entity, prelude::*};
use language::{Buffer, ToPoint as _};
use project::Project;
use settings::{EditPredictionDataCollectionChoice, update_settings_file};
use crate::{BufferEditPrediction, EditPredictionStore};
@ -77,7 +75,24 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
.read(cx)
.is_file_open_source(&self.project, file, cx);
if self.store.read(cx).is_data_collection_enabled(cx) {
if let Some(organization_configuration) = self
.store
.read(cx)
.user_store
.read(cx)
.current_organization_configuration()
{
if !organization_configuration
.edit_prediction
.is_feedback_enabled
{
return DataCollectionState::Disabled {
is_project_open_source,
};
}
}
if self.store.read(cx).data_collection_choice.is_enabled(cx) {
DataCollectionState::Enabled {
is_project_open_source,
}
@ -87,9 +102,9 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
}
}
} else {
DataCollectionState::Disabled {
return DataCollectionState::Disabled {
is_project_open_source: false,
}
};
}
}
@ -98,26 +113,27 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
return false;
}
self.store
if let Some(organization_configuration) = self
.store
.read(cx)
.is_data_collection_allowed_by_organization(cx)
.user_store
.read(cx)
.current_organization_configuration()
{
if !organization_configuration
.edit_prediction
.is_feedback_enabled
{
return false;
}
}
true
}
fn toggle_data_collection(&mut self, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
let is_currently_enabled = self.store.read(cx).is_data_collection_enabled(cx);
update_settings_file(fs, cx, move |settings, _| {
let edit_predictions = settings
.project
.all_languages
.edit_predictions
.get_or_insert_default();
edit_predictions.allow_data_collection = Some(if is_currently_enabled {
EditPredictionDataCollectionChoice::No
} else {
EditPredictionDataCollectionChoice::Yes
});
self.store.update(cx, |store, cx| {
store.toggle_data_collection_choice(cx);
});
}

View file

@ -289,11 +289,6 @@ impl RelatedExcerptStore {
.ok()?;
let type_definitions = project
.update(cx, |project, cx| {
// tombi LSP for toml will open a scratch buffer with the JSON schema of
// the toml file when a goto type definition is requested
if is_tombi_lsp_in_toml(project, &buffer, cx) {
return Task::ready(Ok(None));
}
project.type_definitions(&buffer, identifier.range.start, cx)
})
.ok()?;
@ -568,7 +563,7 @@ impl RelatedBuffer {
})
.collect::<Vec<_>>();
self.cached_file = Some(CachedRelatedFile {
excerpts,
excerpts: excerpts,
buffer_version: buffer.version().clone(),
});
self.cached_file.as_ref().unwrap()
@ -674,7 +669,6 @@ fn identifiers_for_position(
if let Some(config) = config
&& config.identifier_capture_indices.contains(&capture.index)
&& range.contains_inclusive(&node_range)
&& !is_tsx_tag(&buffer, &capture.node)
&& Some(&node_range) != last_range.as_ref()
{
let name = buffer.text_for_range(node_range.clone()).collect();
@ -692,59 +686,3 @@ fn identifiers_for_position(
identifiers
}
fn is_tsx_tag(buffer: &BufferSnapshot, node: &tree_sitter::Node) -> bool {
let Some(language_config) = buffer
.language()
.and_then(|l| l.config().jsx_tag_auto_close.as_ref())
else {
return false;
};
let Some(parent_kind) = node.parent().map(|n| n.kind()) else {
return false;
};
if parent_kind != &language_config.open_tag_node_name
&& parent_kind != &language_config.close_tag_node_name
&& parent_kind != &language_config.tag_name_node_name
&& language_config
.erroneous_close_tag_name_node_name
.as_ref()
.is_some_and(|kind| parent_kind != kind)
&& language_config
.erroneous_close_tag_node_name
.as_ref()
.is_some_and(|kind| parent_kind == kind)
&& parent_kind != &language_config.jsx_element_node_name
{
return false;
}
// do fetch `<Component />`, model probably understands `<div>`, but needs info for user defined components
if !buffer
.text_for_range(node.byte_range())
.all(|str| str.chars().all(|c| c.is_lowercase()))
{
return false;
}
true
}
fn is_tombi_lsp_in_toml(
project: &Project,
buffer: &Entity<Buffer>,
cx: &mut Context<Project>,
) -> bool {
buffer.update(cx, |buffer, cx| {
if !buffer.language().is_some_and(|lang| lang.name() == "TOML") {
return false;
}
project.lsp_store().update(cx, |lsp_store, cx| {
for (_, lsp) in lsp_store.running_language_servers_for_local_buffer(buffer, cx) {
if "tombi".eq_ignore_ascii_case(lsp.name().as_ref()) {
return true;
}
}
false
})
})
}

View file

@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{iter, ops::Range, sync::Arc};
use collections::{HashMap, HashSet};
use futures::future::join_all;
@ -7,15 +7,17 @@ use itertools::Itertools;
use language::{BufferId, ClientCommand};
use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
use project::{CodeAction, TaskSourceKind};
use settings::Settings as _;
use task::TaskContext;
use text::Point;
use ui::{Context, Window, div, prelude::*};
use workspace::PreviewTabsSettings;
use crate::{
Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, SelectionEffects,
Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
actions::ToggleCodeLens,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
hover_links::HoverLink,
};
#[derive(Clone, Debug)]
@ -76,7 +78,6 @@ fn group_lenses_by_row(
}
fn render_code_lens_line(
buffer_id: BufferId,
line_number: usize,
lens: CodeLensLine,
editor: WeakEntity<Editor>,
@ -103,11 +104,11 @@ fn render_code_lens_line(
let action = item.action.clone();
let editor_handle = editor.clone();
let position = lens.position;
let id = SharedString::from(format!("{buffer_id}:{line_number}:{i}"));
let id = (line_number as u64) << 32 | (i as u64);
children.push(
div()
.id(ElementId::Name(id))
.id(ElementId::Integer(id))
.font(font.clone())
.text_size(font_size)
.text_color(cx.app.theme().colors().text_muted)
@ -205,7 +206,7 @@ pub(super) fn try_handle_client_command(
schedule_task(task_template, action, editor, workspace, window, cx)
}
Some(ClientCommand::ShowLocations) => {
try_show_references(arguments, action, editor, window, cx)
try_show_references(arguments, action, workspace, window, cx)
}
None => false,
}
@ -260,7 +261,7 @@ fn schedule_task(
fn try_show_references(
arguments: &[serde_json::Value],
action: &CodeAction,
editor: &mut Editor,
workspace: &gpui::Entity<workspace::Workspace>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> bool {
@ -275,18 +276,73 @@ fn try_show_references(
}
let server_id = action.server_id;
let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx);
let links = locations
.into_iter()
.map(|location| HoverLink::InlayHint(location, server_id))
.collect();
editor
.navigate_to_hover_links(None, links, nav_entry, false, window, cx)
.detach_and_log_err(cx);
let project = workspace.read(cx).project().clone();
let workspace = workspace.clone();
cx.spawn_in(window, async move |_editor, cx| {
let mut buffer_locations = std::collections::HashMap::default();
for location in &locations {
let open_task = cx.update(|_, cx| {
project.update(cx, |project, cx| {
let uri: lsp::Uri = location.uri.clone();
project.open_local_buffer_via_lsp(uri, server_id, cx)
})
})?;
let buffer = open_task.await?;
let range = range_from_lsp(location.range);
buffer_locations
.entry(buffer)
.or_insert_with(Vec::new)
.push(range);
}
workspace.update_in(cx, |workspace, window, cx| {
let target = buffer_locations
.iter()
.flat_map(|(k, v)| iter::repeat(k.clone()).zip(v))
.map(|(buffer, location)| {
buffer
.read(cx)
.text_for_range(location.clone())
.collect::<String>()
})
.filter(|text| !text.contains('\n'))
.unique()
.take(3)
.join(", ");
let title = if target.is_empty() {
"References".to_owned()
} else {
format!("References to {target}")
};
let allow_preview =
PreviewTabsSettings::get_global(cx).enable_preview_multibuffer_from_code_navigation;
Editor::open_locations_in_multibuffer(
workspace,
buffer_locations,
title,
false,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
);
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
true
}
fn range_from_lsp(range: lsp::Range) -> Range<Point> {
let start = Point::new(range.start.line, range.start.character);
let end = Point::new(range.end.line, range.end.character);
start..end
}
impl Editor {
pub(super) fn refresh_code_lenses(
&mut self,
@ -413,7 +469,6 @@ impl Editor {
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(render_code_lens_line(
buffer_id,
line_number,
lens_line,
editor_handle.clone(),
@ -554,7 +609,6 @@ impl Editor {
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(render_code_lens_line(
buffer_id,
line_number,
lens_line,
editor_handle.clone(),

View file

@ -3860,6 +3860,9 @@ impl Editor {
cx.emit(EditorEvent::SelectionsChanged { local });
let selections = &self.selections.disjoint_anchors_arc();
if selections.len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
}
if local && let Some(buffer_snapshot) = buffer.as_singleton() {
let inmemory_selections = selections
.iter()
@ -9255,9 +9258,9 @@ impl Editor {
}))
.tooltip(move |_window, cx| {
Tooltip::with_meta_in(
"Remove Bookmark",
"Remove bookmark",
Some(&ToggleBookmark),
SharedString::from("Right-click for more options"),
SharedString::from("Right-click for more options."),
&focus_handle,
cx,
)
@ -9543,16 +9546,15 @@ impl Editor {
};
let primary_action_text = "Unset breakpoint";
let focus_handle = self.focus_handle.clone();
let has_context_menu = self.has_mouse_context_menu();
let meta = if is_rejected {
SharedString::from("No executable code is associated with this line.")
} else if !breakpoint.is_disabled() {
SharedString::from(format!(
"{alt_as_text}-click to disable\nright-click for more options"
"{alt_as_text}click to disable,\nright-click for more options."
))
} else {
SharedString::from("Right-click for more options")
SharedString::from("Right-click for more options.")
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
@ -9582,16 +9584,14 @@ impl Editor {
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
}))
.when(!has_context_menu, |button| {
button.tooltip(move |_window, cx| {
Tooltip::with_meta_in(
primary_action_text,
Some(&ToggleBreakpoint),
meta.clone(),
&focus_handle,
cx,
)
})
.tooltip(move |_window, cx| {
Tooltip::with_meta_in(
primary_action_text,
Some(&ToggleBreakpoint),
meta.clone(),
&focus_handle,
cx,
)
})
}
@ -9637,10 +9637,10 @@ impl Editor {
};
match self {
Intent::SetBookmark => format!(
"{alt_as_text}-click to add a breakpoint\nright-click for more options"
"{alt_as_text}click to add a breakpoint,\nright-click for more options."
),
Intent::SetBreakpoint => format!(
"{alt_as_text}-click to add a bookmark\nright-click for more options"
"{alt_as_text}click to add a bookmark,\nright-click for more options."
),
}
}
@ -9667,7 +9667,6 @@ impl Editor {
};
let focus_handle = self.focus_handle.clone();
let has_context_menu = self.has_mouse_context_menu();
IconButton::new(("add_breakpoint_button", row.0 as usize), intent.icon())
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
@ -9696,16 +9695,14 @@ impl Editor {
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
}))
.when(!has_context_menu, |button| {
button.tooltip(move |_window, cx| {
Tooltip::with_meta_in(
intent.as_str(),
Some(&ToggleBreakpoint),
intent.secondary_and_options(),
&focus_handle,
cx,
)
})
.tooltip(move |_window, cx| {
Tooltip::with_meta_in(
intent.as_str(),
Some(&ToggleBreakpoint),
intent.secondary_and_options(),
&focus_handle,
cx,
)
})
}
@ -16013,7 +16010,7 @@ impl Editor {
}
}
pub(crate) fn navigation_entry(
fn navigation_entry(
&self,
cursor_anchor: Anchor,
cx: &mut Context<Self>,

View file

@ -3,7 +3,7 @@ use crate::{
JoinLines,
code_context_menus::CodeContextMenu,
edit_prediction_tests::FakeEditPredictionDelegate,
element::{StickyHeader, header_jump_data},
element::StickyHeader,
linked_editing_ranges::LinkedEditingRanges,
runnables::RunnableTasks,
scroll::scroll_amount::ScrollAmount,
@ -19335,112 +19335,6 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_header_jump_data_uses_selection_excerpt(cx: &mut TestAppContext) {
init_test(cx, |_| {});
// 25-line buffer so excerpts at rows 1, 10, and 20 (each a 1-line range,
// expanded by 2 context lines) can't merge into a single excerpt.
let buffer_text = (0..25)
.map(|row| format!("line {row}"))
.collect::<Vec<_>>()
.join("\n");
let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(ReadWrite);
multibuffer.set_excerpts_for_path(
PathKey::sorted(0),
buffer.clone(),
[
Point::new(1, 0)..Point::new(1, 0),
Point::new(10, 0)..Point::new(10, 0),
Point::new(20, 0)..Point::new(20, 0),
],
2,
cx,
);
multibuffer
});
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let display_snapshot = editor.display_snapshot(cx);
// Ensure the three ranges landed in three separate excerpts.
let excerpts: Vec<_> = snapshot
.buffer_snapshot()
.excerpts_for_buffer(buffer_id)
.collect();
assert_eq!(excerpts.len(), 3);
// Place the cursor at the start of the third excerpt, expressed in
// terms of the underlying buffer.
let selection_buffer_row = 20;
let buffer_entity = editor.buffer().read(cx).buffer(buffer_id).unwrap();
let selection_anchor = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer
.buffer_point_to_anchor(&buffer_entity, Point::new(selection_buffer_row, 0), cx)
.expect("buffer row 20 maps to a multibuffer anchor")
});
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchor_ranges([selection_anchor..selection_anchor])
});
let mut latest_selection_anchors: HashMap<BufferId, Anchor> = HashMap::default();
for selection in editor.selections.all_anchors(&display_snapshot).iter() {
let head = selection.head();
if let Some((text_anchor, _)) = snapshot.buffer_snapshot().anchor_to_buffer_anchor(head)
{
latest_selection_anchors.insert(text_anchor.buffer_id, head);
}
}
// The sticky buffer header represents the FIRST excerpt of its buffer,
// even when the cursor is in a later excerpt. That mismatch is the
// precondition for the regression.
let first_excerpt = snapshot
.buffer_snapshot()
.excerpt_boundaries_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
.next()
.expect("multibuffer has at least one excerpt")
.next;
let jump_data = header_jump_data(
&snapshot,
DisplayRow(0),
FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
&first_excerpt,
&latest_selection_anchors,
);
match jump_data {
JumpData::MultiBufferPoint {
position,
line_offset_from_top,
..
} => {
assert_eq!(
position.row, selection_buffer_row,
"jump should target the cursor's buffer row, not the first excerpt's row"
);
assert!(
line_offset_from_top < selection_buffer_row,
"line_offset_from_top ({line_offset_from_top}) should be measured from the \
selection's excerpt, not the first excerpt; expected less than \
selection_buffer_row ({selection_buffer_row})"
);
}
JumpData::MultiBufferRow { .. } => {
panic!("expected MultiBufferPoint jump data when a selection is present")
}
}
});
}
#[gpui::test]
async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@ -32821,78 +32715,6 @@ async fn test_sticky_scroll(cx: &mut TestAppContext) {
assert_eq!(sticky_headers(10.0), vec![]);
}
#[gpui::test]
async fn test_sticky_scroll_with_decoration_prefix_in_item(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(
Language::new(
LanguageConfig {
name: "TypeScript".into(),
..Default::default()
},
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
)
.with_outline_query(
r#"
(class_declaration
"class" @context
name: (_) @name) @item
"#,
)
.expect("TypeScript outline query"),
);
let buffer = indoc! {"
ˇ@Decorator
class Foo {
x = 1;
y = 2;
z = 3;
w = 4;
}
"};
cx.set_state(buffer);
cx.update_editor(|e, _, cx| {
e.buffer()
.read(cx)
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
})
});
let mut sticky_headers = |offset: ScrollOffset| {
cx.update_editor(|e, window, cx| {
e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
});
cx.run_until_parked();
cx.update_editor(|e, window, cx| {
EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
.into_iter()
.map(
|StickyHeader {
start_point,
offset,
..
}| { (start_point, offset) },
)
.collect::<Vec<_>>()
})
};
let class_foo = Point { row: 1, column: 0 };
assert_eq!(sticky_headers(0.0), vec![]);
assert_eq!(sticky_headers(1.5), vec![(class_foo, 0.0)]);
assert_eq!(sticky_headers(2.5), vec![(class_foo, 0.0)]);
assert_eq!(sticky_headers(5.5), vec![(class_foo, -0.5)]);
assert_eq!(sticky_headers(6.0), vec![]);
assert_eq!(sticky_headers(7.0), vec![]);
}
#[gpui::test]
async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
executor: BackgroundExecutor,

View file

@ -895,8 +895,7 @@ impl EditorElement {
let hitbox = &position_map.gutter_hitbox;
if event.position.x <= hitbox.bounds.right() - gutter_right_padding
// Don't show the gutter_context_menu in collab notes
&& editor.project.is_some()
&& editor.collaboration_hub.is_none()
{
let point_for_position = position_map.point_for_position(event.position);
editor.set_gutter_context_menu(
@ -1395,7 +1394,7 @@ impl EditorElement {
indicator.is_active && start_row == valid_point.row()
});
let gutter_hover_button = if gutter_hovered
let breakpoint_indicator = if gutter_hovered
&& !is_on_diff_review_button_row
&& split_side != Some(SplitSide::Left)
{
@ -1440,16 +1439,13 @@ impl EditorElement {
editor.gutter_hover_button.1 = None;
None
}
} else if editor.has_mouse_context_menu() {
editor.gutter_hover_button.1 = None;
editor.gutter_hover_button.0
} else {
editor.gutter_hover_button.1 = None;
None
};
if &gutter_hover_button != &editor.gutter_hover_button.0 {
editor.gutter_hover_button.0 = gutter_hover_button;
if &breakpoint_indicator != &editor.gutter_hover_button.0 {
editor.gutter_hover_button.0 = breakpoint_indicator;
cx.notify();
}
@ -4732,10 +4728,7 @@ impl EditorElement {
let mut rows = Vec::<StickyHeader>::new();
for item in editor.sticky_headers.iter().flatten() {
let start_point = item
.source_range_for_text
.start
.to_point(snapshot.buffer_snapshot());
let start_point = item.range.start.to_point(snapshot.buffer_snapshot());
let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
let sticky_row = snapshot
@ -8355,34 +8348,21 @@ pub(crate) fn header_jump_data(
) -> JumpData {
let multibuffer_snapshot = editor_snapshot.buffer_snapshot();
let buffer = first_excerpt.buffer(multibuffer_snapshot);
let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) =
let (jump_anchor, jump_buffer) = if let Some(anchor) =
latest_selection_anchors.get(&first_excerpt.buffer_id())
&& let Some((jump_anchor, selection_buffer)) =
multibuffer_snapshot.anchor_to_buffer_anchor(*anchor)
{
let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer);
let selection_excerpt_start = multibuffer_snapshot
.excerpts_for_buffer(jump_anchor.buffer_id)
.find(|excerpt| {
let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer);
let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer);
start <= jump_offset && jump_offset <= end
})
.map(|excerpt| excerpt.context.start)
.unwrap_or(first_excerpt.range.context.start);
(jump_anchor, selection_buffer, selection_excerpt_start)
(jump_anchor, selection_buffer)
} else {
(
first_excerpt.range.primary.start,
buffer,
first_excerpt.range.context.start,
)
(first_excerpt.range.primary.start, buffer)
};
let excerpt_start = first_excerpt.range.context.start;
let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer);
let rows_from_excerpt_start = if jump_anchor == excerpt_start {
0
} else {
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_buffer);
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
jump_position.row.saturating_sub(excerpt_start_point.row)
};

View file

@ -1799,10 +1799,6 @@ impl SearchableItem for Editor {
});
}
}
/// Takes the current cursor position and finds the next match in the
/// provided `direction`, the provide `count` number of times, wrapping
/// around if necessary.
fn match_index_for_direction(
&mut self,
matches: &[Range<Anchor>],
@ -1813,48 +1809,45 @@ impl SearchableItem for Editor {
_: &mut Window,
cx: &mut Context<Self>,
) -> usize {
if count == 0 {
return current_index;
}
let cursor = if self.selections.disjoint_anchors_arc().len() == 1 {
let buffer = self.buffer().read(cx).snapshot(cx);
let current_index_position = if self.selections.disjoint_anchors_arc().len() == 1 {
self.selections.newest_anchor().head()
} else {
matches[current_index].start
};
let buffer = self.buffer().read(cx).snapshot(cx);
let new_idx = match direction {
Direction::Next => matches
.iter()
.position(|m| m.start.cmp(&cursor, &buffer).is_gt())
.unwrap_or(0),
Direction::Prev => matches
.iter()
.rposition(|m| m.end.cmp(&cursor, &buffer).is_lt())
.unwrap_or(matches.len() - 1),
} as isize;
let mut count = count % matches.len();
if count == 0 {
return current_index;
}
match direction {
Direction::Next => {
if matches[current_index]
.start
.cmp(&current_index_position, &buffer)
.is_gt()
{
count -= 1
}
// We'll use `count - 1` because the first jump to the next or previous
// match already happens in the scenario above, when we find the next or
// previous match starting from the cursor position.
let count = count.saturating_sub(1);
let count = match direction {
Direction::Prev => -(count as isize),
Direction::Next => count as isize,
};
(current_index + count) % matches.len()
}
Direction::Prev => {
if matches[current_index]
.end
.cmp(&current_index_position, &buffer)
.is_lt()
{
count -= 1;
}
let new_idx = (new_idx + count) % matches.len() as isize;
let new_idx = if new_idx.is_negative() {
// We need a `matches.len() - 1` here in case `next_idx` has now been
// set to `0`, otherwise we'd end up returning `matches.len()`, which
// would be out of bounds.
new_idx + (matches.len() - 1) as isize
} else {
new_idx
};
assert!(new_idx < matches.len() as isize);
new_idx as usize
if current_index >= count {
current_index - count
} else {
matches.len() - (count - current_index)
}
}
}
}
fn find_matches(

View file

@ -433,17 +433,6 @@ impl SplittableEditor {
self.lhs.as_ref().map(|s| &s.editor)
}
pub fn update_editors(
&self,
cx: &mut Context<Self>,
f: impl Fn(&mut Editor, &mut Context<Editor>),
) {
if let Some(lhs) = &self.lhs {
lhs.editor.update(cx, &f);
}
self.rhs_editor.update(cx, &f);
}
pub fn diff_view_style(&self) -> DiffViewStyle {
self.diff_view_style
}
@ -457,18 +446,15 @@ impl SplittableEditor {
render_diff_hunk_controls: RenderDiffHunkControlsFn,
cx: &mut Context<Self>,
) {
self.update_editors(cx, |editor, cx| {
self.rhs_editor.update(cx, |editor, cx| {
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
});
}
pub fn disable_diff_hunk_controls(&self, cx: &mut Context<Self>) {
let empty_controls = Arc::new(|_, _: &_, _, _, _, _: &_, _: &mut _, _: &mut _| {
gpui::Empty.into_any_element()
});
self.update_editors(cx, |editor, cx| {
editor.set_render_diff_hunk_controls(empty_controls.clone(), cx);
});
if let Some(lhs) = &self.lhs {
lhs.editor.update(cx, |editor, cx| {
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
});
}
}
fn focused_side(&self) -> SplitSide {
@ -505,7 +491,6 @@ impl SplittableEditor {
editor.set_expand_all_diff_hunks(cx);
editor.disable_runnables();
editor.disable_inline_diagnostics();
editor.disable_mouse_wheel_zoom();
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
editor.start_temporary_diff_override();
editor
@ -601,7 +586,6 @@ impl SplittableEditor {
editor.disable_lsp_data();
editor.disable_runnables();
editor.disable_diagnostics(cx);
editor.disable_mouse_wheel_zoom();
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
editor
});
@ -2141,9 +2125,14 @@ mod tests {
window,
cx,
);
editor.update_editors(cx, |editor, cx| {
editor.rhs_editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(soft_wrap, cx);
});
if let Some(lhs) = &editor.lhs {
lhs.editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(soft_wrap, cx);
});
}
editor
});
(editor, cx)

View file

@ -31,7 +31,6 @@ settings.workspace = true
serde.workspace = true
theme.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View file

@ -13,8 +13,8 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render,
StatefulInteractiveElement, Styled, Task, WeakEntity, Window, actions, rems,
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
Window, actions, rems,
};
use open_path_prompt::{
OpenPathPrompt,
@ -37,10 +37,9 @@ use std::{
},
};
use ui::{
ButtonLike, CommonAnimationExt, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing,
PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
use ui_input::ErasedEditor;
use util::{
ResultExt, maybe,
paths::{PathStyle, PathWithPosition},
@ -1758,41 +1757,6 @@ impl PickerDelegate for FileFinderDelegate {
)
}
fn render_editor(
&self,
editor: &Arc<dyn ErasedEditor>,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Div {
let has_search_query = self.latest_search_query.is_some();
let is_project_scan_running = {
let worktree_store = self.project.read(cx).worktree_store();
!worktree_store.read(cx).initial_scan_completed()
};
h_flex()
.flex_none()
.h_9()
.px_2p5()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(editor.render(window, cx))
.when(is_project_scan_running && has_search_query, |this| {
this.child(
h_flex()
.id("project-scan-indicator")
.tooltip(Tooltip::text("Project Scan in Progress…"))
.child(
Icon::new(IconName::LoadCircle)
.color(Color::Accent)
.size(IconSize::Small)
.with_rotate_animation(2),
),
)
})
}
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();

View file

@ -36,7 +36,6 @@ thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
telemetry.workspace = true
tempfile.workspace = true
text.workspace = true
time.workspace = true

View file

@ -72,133 +72,6 @@ pub trait Watcher: Send + Sync {
fn remove(&self, path: &Path) -> Result<()>;
}
/// Detect whether a path requires polling instead of native file watching.
///
/// Returns `true` for filesystem types where inotify/FSEvents/ReadDirectoryChanges
/// silently fail to deliver events: 9P (WSL drvfs), NFS, CIFS/SMB, FUSE (sshfs), etc.
///
/// Can be overridden with the `ZED_FILE_WATCHER_MODE` environment variable:
/// - `native` — always use native OS watcher
/// - `poll` — always use polling
/// - `auto` (default) — auto-detect based on filesystem type
pub fn requires_poll_watcher(path: &Path) -> bool {
match std::env::var("ZED_FILE_WATCHER_MODE")
.as_deref()
.unwrap_or("auto")
{
"native" => return false,
"poll" => return true,
_ => {}
}
#[cfg(target_os = "linux")]
{
let path = effective_watch_path(path);
return detect_requires_poll_watcher_linux(&path);
}
#[cfg(not(target_os = "linux"))]
{
let _ = path;
false
}
}
pub fn effective_watch_path(path: &Path) -> PathBuf {
if path.exists() {
return path.to_path_buf();
}
for ancestor in path.ancestors() {
if ancestor.exists() {
return ancestor.to_path_buf();
}
}
path.to_path_buf()
}
#[cfg(target_os = "linux")]
fn detect_requires_poll_watcher_linux(path: &Path) -> bool {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
// Check filesystem type via statfs
let c_path = match CString::new(path.as_os_str().as_bytes()) {
Ok(p) => p,
Err(_) => return false,
};
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(c_path.as_ptr(), &mut stat) } != 0 {
return false;
}
// Filesystem magic numbers where inotify does not deliver events.
// These are defined in linux/magic.h and statfs(2).
const V9FS_MAGIC: u64 = 0x0102_1997; // Plan 9 / WSL2 interop (drvfs)
const NFS_SUPER_MAGIC: u64 = 0x0000_6969;
const CIFS_MAGIC: u64 = 0xFF53_4D42; // CIFS/SMB
const SMB_SUPER_MAGIC: u64 = 0x0000_517B;
const SMB2_MAGIC: u64 = 0xFE53_4D42;
const FUSE_SUPER_MAGIC: u64 = 0x6573_5546; // FUSE (includes sshfs)
let fs_type = (stat.f_type as u64) & 0xFFFF_FFFF;
if fs_type == V9FS_MAGIC
|| fs_type == NFS_SUPER_MAGIC
|| fs_type == CIFS_MAGIC
|| fs_type == SMB_SUPER_MAGIC
|| fs_type == SMB2_MAGIC
|| fs_type == FUSE_SUPER_MAGIC
{
log::info!(
"Detected network/virtual filesystem (type 0x{:x}) at {}, using poll watcher",
fs_type,
path.display()
);
return true;
}
// Also check for WSL + /mnt/<drive>/ pattern as a fallback
// in case statfs returns an unexpected type for drvfs
if is_wsl_drvfs_path(path) {
log::info!(
"Detected WSL drvfs mount at {}, using poll watcher",
path.display()
);
return true;
}
false
}
#[cfg(target_os = "linux")]
fn is_wsl_drvfs_path(path: &Path) -> bool {
// Only relevant inside WSL
if std::env::var_os("WSL_DISTRO_NAME").is_none() {
if let Ok(version) = std::fs::read_to_string("/proc/version") {
let v = version.to_lowercase();
if !v.contains("microsoft") && !v.contains("wsl") {
return false;
}
} else {
return false;
}
}
// Windows drives are mounted at /mnt/c, /mnt/d, etc.
let path_str = match path.to_str() {
Some(s) => s,
None => return false,
};
if !path_str.starts_with("/mnt/") || path_str.len() < 6 {
return false;
}
let after_mnt = &path_str[5..];
after_mnt.starts_with(|c: char| c.is_ascii_alphabetic())
&& (after_mnt.len() == 1 || after_mnt.as_bytes()[1] == b'/')
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum PathEventKind {
Removed,
@ -1197,34 +1070,19 @@ impl Fs for RealFs {
use util::{ResultExt as _, paths::SanitizedPath};
let executor = self.executor.clone();
let use_poll = requires_poll_watcher(path);
let watch_path = effective_watch_path(path);
let (tx, rx) = smol::channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
let mode = if use_poll {
log::info!(
"Using poll watcher ({}ms interval) for {}",
fs_watcher::poll_interval().as_millis(),
path.display()
);
telemetry::event!("fs_watcher_poll", path = path.display().to_string());
fs_watcher::WatcherMode::Poll
} else {
fs_watcher::WatcherMode::Native
};
let watcher: Arc<dyn Watcher> = Arc::new(fs_watcher::FsWatcher::new(
tx.clone(),
pending_paths.clone(),
mode,
));
if let Err(e) = watcher.add(&watch_path) {
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
if let Err(e) = watcher.add(path)
&& let Some(parent) = path.parent()
&& let Err(parent_e) = watcher.add(parent)
{
log::warn!(
"Failed to watch {} using {}:\n{e}",
"Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}",
path.display(),
watch_path.display()
parent.display()
);
}

View file

@ -4,38 +4,27 @@ use std::{
collections::{BTreeMap, HashMap},
ops::DerefMut,
path::Path,
sync::{Arc, LazyLock, OnceLock},
time::Duration,
sync::{Arc, OnceLock},
};
use util::{ResultExt, paths::SanitizedPath};
use crate::{PathEvent, PathEventKind, Watcher};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum WatcherMode {
#[default]
Native,
Poll,
}
pub struct FsWatcher {
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
registrations: Mutex<BTreeMap<Arc<std::path::Path>, WatcherRegistrationId>>,
mode: WatcherMode,
}
impl FsWatcher {
pub fn new(
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
mode: WatcherMode,
) -> Self {
Self {
tx,
pending_path_events,
registrations: Default::default(),
mode,
}
}
}
@ -48,10 +37,11 @@ impl Drop for FsWatcher {
std::mem::swap(old.deref_mut(), &mut registrations);
}
let global_watcher = global_watcher();
for (_, registration) in registrations {
global_watcher.remove(registration);
}
let _ = global(|g| {
for (_, registration) in registrations {
g.remove(registration);
}
});
}
}
@ -59,10 +49,13 @@ impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
log::trace!("watcher add: {path:?}");
let tx = self.tx.clone();
let pending_path_events = self.pending_path_events.clone();
let pending_paths = self.pending_path_events.clone();
if (self.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos")))
&& let Some((watched_path, _)) = self
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
// Return early if an ancestor of this path was already being watched.
// saves a huge amount of memory
if let Some((watched_path, _)) = self
.registrations
.lock()
.range::<std::path::Path, _>((
@ -70,36 +63,86 @@ impl Watcher for FsWatcher {
std::ops::Bound::Included(path),
))
.next_back()
&& path.starts_with(watched_path.as_ref())
{
log::trace!(
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
);
return Ok(());
&& path.starts_with(watched_path.as_ref())
{
log::trace!(
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
);
return Ok(());
}
}
if self.registrations.lock().contains_key(path) {
log::trace!("path to watch is already watched: {path:?}");
return Ok(());
#[cfg(target_os = "linux")]
{
if self.registrations.lock().contains_key(path) {
log::trace!("path to watch is already watched: {path:?}");
return Ok(());
}
}
let root_path = SanitizedPath::new_arc(path);
let path: Arc<std::path::Path> = path.into();
#[cfg(any(target_os = "windows", target_os = "macos"))]
let mode = notify::RecursiveMode::Recursive;
#[cfg(target_os = "linux")]
let mode = notify::RecursiveMode::NonRecursive;
let registration_path = path.clone();
let registration_id = global_watcher().add(
path.clone(),
self.mode,
move |result: Result<&notify::Event, &notify::Error>| match result {
Ok(event) => {
let registration_id = global({
let watch_path = path.clone();
let callback_path = path;
|g| {
g.add(watch_path, mode, move |event: &notify::Event| {
log::trace!("watcher received event: {event:?}");
push_notify_event(&tx, &pending_path_events, &root_path, path.as_ref(), event);
}
Err(error) => {
push_notify_error(&tx, &pending_path_events, path.as_ref(), error);
}
},
)?;
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut path_events = event
.paths
.iter()
.filter_map(|event_path| {
let event_path = SanitizedPath::new(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,
})
})
.collect::<Vec<_>>();
let is_rescan_event = event.need_rescan();
if is_rescan_event {
log::warn!(
"filesystem watcher lost sync for {callback_path:?}; scheduling rescan"
);
// we only keep the first event per path below, this ensures it will be the rescan event
// we'll remove any existing pending events for the same reason once we have the lock below
path_events.retain(|p| &p.path != callback_path.as_ref());
path_events.push(PathEvent {
path: callback_path.to_path_buf(),
kind: Some(PathEventKind::Rescan),
});
}
if !path_events.is_empty() {
path_events.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
coalesce_pending_rescans(&mut pending_paths, &mut path_events);
util::extend_sorted(
&mut *pending_paths,
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
}
})
}
})??;
self.registrations
.lock()
@ -114,85 +157,10 @@ impl Watcher for FsWatcher {
return Ok(());
};
global_watcher().remove(registration);
Ok(())
global(|w| w.remove(registration))
}
}
fn enqueue_path_events(
tx: &smol::channel::Sender<()>,
pending_path_events: &Arc<Mutex<Vec<PathEvent>>>,
mut path_events: Vec<PathEvent>,
) {
if path_events.is_empty() {
return;
}
path_events.sort();
let mut pending_paths = pending_path_events.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
coalesce_pending_rescans(&mut pending_paths, &mut path_events);
util::extend_sorted(&mut *pending_paths, path_events, usize::MAX, |a, b| {
a.path.cmp(&b.path)
});
}
fn push_notify_event(
tx: &smol::channel::Sender<()>,
pending_path_events: &Arc<Mutex<Vec<PathEvent>>>,
root_path: &SanitizedPath,
watched_root: &Path,
event: &notify::Event,
) {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut path_events = event
.paths
.iter()
.filter_map(|event_path| {
let event_path = SanitizedPath::new(event_path);
event_path.starts_with(root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,
})
})
.collect::<Vec<_>>();
if event.need_rescan() {
log::warn!("filesystem watcher lost sync for {watched_root:?}; scheduling rescan");
path_events.retain(|path_event| path_event.path != watched_root);
path_events.push(PathEvent {
path: watched_root.to_path_buf(),
kind: Some(PathEventKind::Rescan),
});
}
enqueue_path_events(tx, pending_path_events, path_events);
}
fn push_notify_error(
tx: &smol::channel::Sender<()>,
pending_path_events: &Arc<Mutex<Vec<PathEvent>>>,
watched_root: &Path,
error: &notify::Error,
) {
log::warn!("watcher error for {watched_root:?}: {error}");
enqueue_path_events(
tx,
pending_path_events,
vec![PathEvent {
path: watched_root.to_path_buf(),
kind: Some(PathEventKind::Rescan),
}],
);
}
fn coalesce_pending_rescans(pending_paths: &mut Vec<PathEvent>, path_events: &mut Vec<PathEvent>) {
if !path_events
.iter()
@ -247,34 +215,29 @@ fn is_covered_rescan(kind: Option<PathEventKind>, path: &Path, ancestor: &Path)
pub struct WatcherRegistrationId(u32);
struct WatcherRegistrationState {
callback: Arc<dyn for<'a> Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync>,
callback: Arc<dyn Fn(&notify::Event) + Send + Sync>,
path: Arc<std::path::Path>,
mode: WatcherMode,
}
struct WatcherState {
watchers: HashMap<WatcherRegistrationId, WatcherRegistrationState>,
native_path_registrations: HashMap<Arc<std::path::Path>, u32>,
poll_path_registrations: HashMap<Arc<std::path::Path>, u32>,
path_registrations: HashMap<Arc<std::path::Path>, u32>,
last_registration: WatcherRegistrationId,
}
impl WatcherState {
fn path_registrations(&mut self, mode: WatcherMode) -> &mut HashMap<Arc<std::path::Path>, u32> {
match mode {
WatcherMode::Native => &mut self.native_path_registrations,
WatcherMode::Poll => &mut self.poll_path_registrations,
}
}
}
pub struct GlobalWatcher {
state: Mutex<WatcherState>,
// DANGER: never keep state lock while holding watcher lock
// two mutexes because calling watcher.add triggers watcher.event, which needs watchers.
native_watcher: Mutex<Option<notify::RecommendedWatcher>>,
poll_watcher: Mutex<Option<notify::PollWatcher>>,
// DANGER: never keep the state lock while holding the watcher lock
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
#[cfg(target_os = "linux")]
watcher: Mutex<notify::INotifyWatcher>,
#[cfg(target_os = "freebsd")]
watcher: Mutex<notify::KqueueWatcher>,
#[cfg(target_os = "windows")]
watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
#[cfg(target_os = "macos")]
watcher: Mutex<notify::FsEventWatcher>,
}
impl GlobalWatcher {
@ -282,17 +245,27 @@ impl GlobalWatcher {
fn add(
&self,
path: Arc<std::path::Path>,
mode: WatcherMode,
cb: impl for<'a> Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync + 'static,
mode: notify::RecursiveMode,
cb: impl Fn(&notify::Event) + Send + Sync + 'static,
) -> anyhow::Result<WatcherRegistrationId> {
let mut state = self.state.lock();
let registrations_for_mode = state.path_registrations(mode);
let path_already_covered =
path_already_covered(path.as_ref(), registrations_for_mode, mode);
use notify::Watcher;
if !path_already_covered && !registrations_for_mode.contains_key(&path) {
let mut state = self.state.lock();
// Check if this path is already covered by an existing watched ancestor path.
// On macOS and Windows, watching is recursive, so we don't need to watch
// child paths if an ancestor is already being watched.
#[cfg(any(target_os = "windows", target_os = "macos"))]
let path_already_covered = state.path_registrations.keys().any(|existing| {
path.starts_with(existing.as_ref()) && path.as_ref() != existing.as_ref()
});
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
let path_already_covered = false;
if !path_already_covered && !state.path_registrations.contains_key(&path) {
drop(state);
self.watch(&path, mode)?;
self.watcher.lock().watch(&path, mode)?;
state = self.state.lock();
}
@ -302,191 +275,39 @@ impl GlobalWatcher {
let registration_state = WatcherRegistrationState {
callback: Arc::new(cb),
path: path.clone(),
mode,
};
state.watchers.insert(id, registration_state);
*state.path_registrations(mode).entry(path).or_insert(0) += 1;
*state.path_registrations.entry(path).or_insert(0) += 1;
Ok(id)
}
pub fn remove(&self, id: WatcherRegistrationId) {
use notify::Watcher;
let mut state = self.state.lock();
let Some(registration_state) = state.watchers.remove(&id) else {
return;
};
let path_registrations = state.path_registrations(registration_state.mode);
let Some(count) = path_registrations.get_mut(&registration_state.path) else {
let Some(count) = state.path_registrations.get_mut(&registration_state.path) else {
return;
};
*count -= 1;
if *count == 0 {
path_registrations.remove(&registration_state.path);
let path_still_covered = path_already_covered(
registration_state.path.as_ref(),
path_registrations,
registration_state.mode,
);
state.path_registrations.remove(&registration_state.path);
if !path_still_covered {
drop(state);
self.unwatch(&registration_state.path, registration_state.mode)
.log_err();
}
}
}
fn watch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> {
use notify::Watcher;
match mode {
WatcherMode::Native => {
self.ensure_native_watcher()?;
self.native_watcher
.lock()
.as_mut()
.expect("native watcher initialized")
.watch(
path,
if cfg!(any(target_os = "windows", target_os = "macos")) {
notify::RecursiveMode::Recursive
} else {
notify::RecursiveMode::NonRecursive
},
)?;
}
WatcherMode::Poll => {
self.ensure_poll_watcher()?;
self.poll_watcher
.lock()
.as_mut()
.expect("poll watcher initialized")
.watch(path, notify::RecursiveMode::Recursive)?;
}
}
Ok(())
}
fn unwatch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> {
use notify::Watcher;
match mode {
WatcherMode::Native => {
if let Some(watcher) = self.native_watcher.lock().as_mut() {
watcher.unwatch(path)?;
}
}
WatcherMode::Poll => {
if let Some(watcher) = self.poll_watcher.lock().as_mut() {
watcher.unwatch(path)?;
}
}
}
Ok(())
}
fn ensure_native_watcher(&self) -> anyhow::Result<()> {
if self.native_watcher.lock().is_some() {
return Ok(());
}
let watcher = notify::recommended_watcher(handle_native_event)?;
*self.native_watcher.lock() = Some(watcher);
Ok(())
}
fn ensure_poll_watcher(&self) -> anyhow::Result<()> {
if self.poll_watcher.lock().is_some() {
return Ok(());
}
let config = notify::Config::default().with_poll_interval(*POLL_INTERVAL);
let watcher = notify::PollWatcher::new(handle_poll_event, config)?;
*self.poll_watcher.lock() = Some(watcher);
Ok(())
}
}
fn path_already_covered(
path: &Path,
path_registrations: &HashMap<Arc<std::path::Path>, u32>,
mode: WatcherMode,
) -> bool {
(mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos")))
&& path_registrations
.keys()
.any(|existing| path.starts_with(existing.as_ref()) && path != existing.as_ref())
}
static POLL_INTERVAL: LazyLock<Duration> = LazyLock::new(|| {
let poll_ms: u64 = std::env::var("ZED_FILE_WATCHER_POLL_MS")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(2000)
.clamp(500, 30000);
Duration::from_millis(poll_ms)
});
pub fn poll_interval() -> Duration {
*POLL_INTERVAL
}
static FS_WATCHER_INSTANCE: OnceLock<GlobalWatcher> = OnceLock::new();
fn global_watcher() -> &'static GlobalWatcher {
FS_WATCHER_INSTANCE.get_or_init(|| GlobalWatcher {
state: Mutex::new(WatcherState {
watchers: Default::default(),
native_path_registrations: Default::default(),
poll_path_registrations: Default::default(),
last_registration: Default::default(),
}),
native_watcher: Mutex::new(None),
poll_watcher: Mutex::new(None),
})
}
fn handle_native_event(event: Result<notify::Event, notify::Error>) {
handle_event(WatcherMode::Native, event);
}
fn handle_poll_event(event: Result<notify::Event, notify::Error>) {
handle_event(WatcherMode::Poll, event);
}
fn handle_event(mode: WatcherMode, event: Result<notify::Event, notify::Error>) {
log::trace!("global handle event for {mode:?}: {event:?}");
let callbacks = {
let state = global_watcher().state.lock();
state
.watchers
.values()
.filter(|registration| registration.mode == mode)
.map(|registration| registration.callback.clone())
.collect::<Vec<_>>()
};
match event {
Ok(event) => {
if matches!(event.kind, EventKind::Access(_)) {
return;
}
for callback in callbacks {
callback(Ok(&event));
}
}
Err(error) => {
for callback in callbacks {
callback(Err(&error));
}
drop(state);
self.watcher
.lock()
.unwatch(&registration_state.path)
.log_err();
}
}
}
static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
OnceLock::new();
#[cfg(test)]
mod tests {
use super::*;
@ -578,8 +399,45 @@ mod tests {
}
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let global_watcher = global_watcher();
global_watcher.ensure_native_watcher()?;
Ok(f(global_watcher))
fn handle_event(event: Result<notify::Event, notify::Error>) {
log::trace!("global handle event: {event:?}");
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
let Some(event) = event
.log_err()
.filter(|event| !matches!(event.kind, EventKind::Access(_)))
else {
return;
};
global::<()>(move |watcher| {
let callbacks = {
let state = watcher.state.lock();
state
.watchers
.values()
.map(|r| r.callback.clone())
.collect::<Vec<_>>()
};
for callback in callbacks {
callback(&event);
}
})
.log_err();
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
state: Mutex::new(WatcherState {
watchers: Default::default(),
path_registrations: Default::default(),
last_registration: Default::default(),
}),
watcher: Mutex::new(file_watcher),
})
});
match result {
Ok(g) => Ok(f(g)),
Err(e) => Err(anyhow::anyhow!("{e}")),
}
}

View file

@ -83,7 +83,6 @@ pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
task.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
zlog.workspace = true

View file

@ -627,6 +627,9 @@ impl PickerDelegate for BranchListDelegate {
let focus_handle = self.focus_handle.clone();
let editor = editor.as_any().downcast_ref::<Entity<Editor>>().unwrap();
let show_inline_filter =
self.editor_position() == PickerEditorPosition::End || !self.show_footer;
v_flex()
.when(
self.editor_position() == PickerEditorPosition::End,
@ -639,34 +642,32 @@ impl PickerDelegate for BranchListDelegate {
.h_9()
.px_2p5()
.child(editor.clone())
.when(
self.editor_position() == PickerEditorPosition::End,
|this| {
let tooltip_label = match self.branch_filter {
BranchFilter::All => "Filter Remote Branches",
BranchFilter::Remote => "Show All Branches",
};
.when(show_inline_filter, |this| {
let tooltip_label = match self.branch_filter {
BranchFilter::All => "Filter Remote Branches",
BranchFilter::Remote => "Show All Branches",
};
this.gap_1().justify_between().child({
IconButton::new("filter-remotes", IconName::Filter)
.toggle_state(self.branch_filter == BranchFilter::Remote)
.tooltip(move |_, cx| {
Tooltip::for_action_in(
tooltip_label,
&branch_picker::FilterRemotes,
&focus_handle,
cx,
)
})
.on_click(|_click, window, cx| {
window.dispatch_action(
branch_picker::FilterRemotes.boxed_clone(),
cx,
);
})
})
},
),
this.gap_1().justify_between().child({
IconButton::new("filter-remotes", IconName::Filter)
.toggle_state(self.branch_filter == BranchFilter::Remote)
.icon_size(IconSize::Small)
.tooltip(move |_, cx| {
Tooltip::for_action_in(
tooltip_label,
&branch_picker::FilterRemotes,
&focus_handle,
cx,
)
})
.on_click(|_click, window, cx| {
window.dispatch_action(
branch_picker::FilterRemotes.boxed_clone(),
cx,
);
})
})
}),
)
.when(
self.editor_position() == PickerEditorPosition::Start,

View file

@ -1,6 +1,5 @@
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style};
use crate::git_panel_settings::GitPanelSettings;
use git::repository::CommitOptions;
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
use panel::panel_button;
@ -540,18 +539,6 @@ impl Render for CommitModal {
let border_radius = properties.modal_border_radius;
let editor_focus_handle = self.commit_editor.focus_handle(cx);
let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length;
let title_exceeds_limit = if max_title_length > 0 {
self.commit_editor
.read(cx)
.text(cx)
.lines()
.next()
.is_some_and(|title| title.len() > max_title_length)
} else {
false
};
v_flex()
.id("commit-modal")
.key_context("GitCommit")
@ -580,9 +567,6 @@ impl Render for CommitModal {
this.toggle_branch_selector(window, cx);
}),
)
.w(width)
.h_112()
.p(container_padding)
.elevation_3(cx)
.overflow_hidden()
.flex_none()
@ -591,50 +575,30 @@ impl Render for CommitModal {
.rounded(px(border_radius))
.border_1()
.border_color(cx.theme().colors().border)
.w(width)
.p(container_padding)
.child(
v_flex()
.id("editor-container")
.cursor_text()
.justify_between()
.p_2()
.size_full()
.gap_2()
.justify_between()
.rounded(properties.editor_border_radius())
.overflow_hidden()
.cursor_text()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(if title_exceeds_limit {
cx.theme().status().warning_border
} else {
cx.theme().colors().border_variant
})
.border_color(cx.theme().colors().border_variant)
.on_click(cx.listener(move |_, _: &ClickEvent, window, cx| {
window.focus(&editor_focus_handle, cx);
}))
.child(self.render_commit_editor(window, cx))
.when(title_exceeds_limit, |this| {
this.child(
h_flex()
.absolute()
.bottom_12()
.w_full()
.py_1()
.px_2()
.gap_1()
.justify_center()
.child(
Icon::new(IconName::Warning)
.size(IconSize::XSmall)
.color(Color::Warning),
)
.child(
Label::new(format!(
"Commit message title exceeds {max_title_length}-character limit."
))
.size(LabelSize::Small),
),
)
})
.child(
div()
.flex_1()
.size_full()
.child(self.render_commit_editor(window, cx)),
)
.child(self.render_footer(window, cx)),
)
}

View file

@ -2,7 +2,7 @@
use anyhow::Result;
use buffer_diff::BufferDiff;
use editor::{Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor};
use editor::{Editor, EditorEvent, MultiBuffer};
use futures::{FutureExt, select_biased};
use gpui::{
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
@ -10,7 +10,6 @@ use gpui::{
};
use language::{Buffer, HighlightedText, LanguageRegistry};
use project::Project;
use settings::Settings;
use std::{
any::{Any, TypeId},
path::PathBuf,
@ -27,7 +26,7 @@ use workspace::{
};
pub struct FileDiffView {
editor: Entity<SplittableEditor>,
editor: Entity<Editor>,
old_buffer: Entity<Buffer>,
new_buffer: Entity<Buffer>,
buffer_changes_tx: watch::Sender<()>,
@ -58,14 +57,12 @@ impl FileDiffView {
let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?;
workspace.update_in(cx, |workspace, window, cx| {
let workspace_entity = cx.entity();
let diff_view = cx.new(|cx| {
FileDiffView::new(
old_buffer,
new_buffer,
buffer_diff,
project.clone(),
workspace_entity,
window,
cx,
)
@ -86,7 +83,6 @@ impl FileDiffView {
new_buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -96,19 +92,16 @@ impl FileDiffView {
multibuffer
});
let editor = cx.new(|cx| {
let splittable = SplittableEditor::new(
EditorSettings::get_global(cx).diff_view_style,
multibuffer.clone(),
project.clone(),
workspace,
window,
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.start_temporary_diff_override();
editor.disable_diagnostics(cx);
editor.set_expand_all_diff_hunks(cx);
editor.set_render_diff_hunk_controls(
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
cx,
);
splittable.rhs_editor().update(cx, |editor, _| {
editor.start_temporary_diff_override();
});
splittable.disable_diff_hunk_controls(cx);
splittable
editor
});
let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(());
@ -275,19 +268,22 @@ impl Item for FileDiffView {
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor.deactivated(window, cx);
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
cx: &'a App,
_: &'a App,
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.clone().into())
} else {
self.editor.act_as_type(type_id, cx)
None
}
}
@ -309,10 +305,8 @@ impl Item for FileDiffView {
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.rhs_editor().update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
})
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
@ -322,11 +316,8 @@ impl Item for FileDiffView {
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor.update(cx, |editor, cx| {
editor
.rhs_editor()
.update(cx, |editor, cx| editor.navigate(data, window, cx))
})
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
@ -344,14 +335,13 @@ impl Item for FileDiffView {
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.rhs_editor().update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
})
editor.added_to_workspace(workspace, window, cx)
});
}
fn can_save(&self, cx: &App) -> bool {
self.editor.read(cx).rhs_editor().read(cx).can_save(cx)
// The editor handles the new buffer, so delegate to it
self.editor.read(cx).can_save(cx)
}
fn save(
@ -361,7 +351,9 @@ impl Item for FileDiffView {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(options, project, window, cx)
// Delegate saving to the editor, which manages the new buffer
self.editor
.update(cx, |editor, cx| editor.save(options, project, window, cx))
}
}
@ -375,10 +367,9 @@ impl Render for FileDiffView {
mod tests {
use super::*;
use editor::test::editor_test_context::assert_state_with_diff;
use gpui::BorrowAppContext;
use gpui::TestAppContext;
use project::{FakeFs, Fs, Project};
use settings::{DiffViewStyle, SettingsStore};
use settings::SettingsStore;
use std::path::PathBuf;
use unindent::unindent;
use util::path;
@ -388,11 +379,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
});
});
theme_settings::init(theme::LoadThemes::JustBase, cx);
});
}
@ -432,9 +418,7 @@ mod tests {
// Verify initial diff
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, cx| {
diff_view.editor.read(cx).rhs_editor().clone()
}),
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
cx,
&unindent(
"
@ -469,9 +453,7 @@ mod tests {
// The diff now reflects the changes to the new file
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, cx| {
diff_view.editor.read(cx).rhs_editor().clone()
}),
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
cx,
&unindent(
"
@ -506,9 +488,7 @@ mod tests {
// The diff now reflects the changes to the new file
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, cx| {
diff_view.editor.read(cx).rhs_editor().clone()
}),
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
cx,
&unindent(
"
@ -572,10 +552,8 @@ mod tests {
.unwrap();
diff_view.update_in(cx, |diff_view, window, cx| {
diff_view.editor.update(cx, |splittable, cx| {
splittable.rhs_editor().update(cx, |editor, cx| {
editor.insert("modified ", window, cx);
});
diff_view.editor.update(cx, |editor, cx| {
editor.insert("modified ", window, cx);
});
});

View file

@ -4343,18 +4343,6 @@ impl GitPanel {
editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32
});
let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length;
let title_exceeds_limit = if max_title_length > 0 {
self.commit_editor
.read(cx)
.text(cx)
.lines()
.next()
.is_some_and(|title| title.len() > max_title_length)
} else {
false
};
let footer = v_flex()
.child(PanelRepoFooter::new(
display_name,
@ -4362,41 +4350,15 @@ impl GitPanel {
head_commit,
Some(git_panel),
))
.when(title_exceeds_limit, |this| {
this.child(
h_flex()
.px_2()
.py_1()
.gap_1()
.border_t_1()
.border_color(cx.theme().status().warning_border)
.bg(cx.theme().status().warning_background.opacity(0.5))
.child(
Icon::new(IconName::Warning)
.size(IconSize::XSmall)
.color(Color::Warning),
)
.child(
Label::new(format!(
"Commit message title exceeds {max_title_length}-character limit."
))
.size(LabelSize::Small),
),
)
})
.child(
panel_editor_container(window, cx)
.id("commit-editor-container")
.cursor_text()
.relative()
.w_full()
.h(max_height + footer_size)
.border_t_1()
.border_color(if title_exceeds_limit {
cx.theme().status().warning_border
} else {
cx.theme().colors().border
})
.border_color(cx.theme().colors().border)
.cursor_text()
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
window.focus(&this.commit_editor.focus_handle(cx), cx);
}))
@ -4977,46 +4939,57 @@ impl GitPanel {
let toggle_state = self.header_state(header.header);
let section = header.header;
let weak = cx.weak_entity();
let show_checkbox_persistently = !matches!(&toggle_state, ToggleState::Unselected);
h_flex()
.id(id)
.cursor_pointer()
.group(group_name)
.group(group_name.clone())
.h(self.list_item_height())
.w_full()
.items_center()
.pl_3()
.pr_1()
.gap_2()
.justify_between()
.hover(|s| s.bg(cx.theme().colors().ghost_element_hover))
.gap_1p5()
.border_1()
.border_r_2()
.child(
Label::new(header.title())
.color(Color::Muted)
.size(LabelSize::Small),
h_flex().flex_1().child(
Label::new(header.title())
.color(Color::Muted)
.size(LabelSize::Small)
.line_height_style(LineHeightStyle::UiLabel)
.single_line(),
),
)
.child(
Checkbox::new(checkbox_id, toggle_state)
.disabled(!has_write_access)
.fill()
.elevation(ElevationIndex::Surface),
)
.on_click(move |_, window, cx| {
if !has_write_access {
return;
}
div()
.flex_none()
.cursor_pointer()
.child(
Checkbox::new(checkbox_id, toggle_state)
.disabled(!has_write_access)
.fill()
.elevation(ElevationIndex::Surface)
.on_click_ext(move |_, _, window, cx| {
if !has_write_access {
return;
}
weak.update(cx, |this, cx| {
this.toggle_staged_for_entry(
&GitListEntry::Header(GitHeaderEntry { header: section }),
window,
cx,
);
cx.stop_propagation();
})
.ok();
})
weak.update(cx, |this, cx| {
this.toggle_staged_for_entry(
&GitListEntry::Header(GitHeaderEntry { header: section }),
window,
cx,
);
cx.stop_propagation();
})
.ok();
}),
)
.when(!show_checkbox_persistently, |this| {
this.visible_on_hover(group_name)
}),
)
.into_any_element()
}
@ -6150,6 +6123,10 @@ impl RenderOnce for PanelRepoFooter {
util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
};
let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name)
.size(ButtonSize::None)
.label_size(LabelSize::Small);
let repo_selector = PopoverMenu::new("repository-switcher")
.menu({
let project = project;
@ -6159,9 +6136,8 @@ impl RenderOnce for PanelRepoFooter {
}
})
.trigger_with_tooltip(
Button::new("repo-selector", truncated_repo_name)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
repo_selector_trigger
.when(single_repo, |this| this.disabled(true).color(Color::Muted))
.truncate(true),
move |_, cx| {
if single_repo {
@ -6203,7 +6179,7 @@ impl RenderOnce for PanelRepoFooter {
});
h_flex()
.h_9()
.h(px(36.))
.w_full()
.px_2()
.justify_between()
@ -6220,14 +6196,14 @@ impl RenderOnce for PanelRepoFooter {
Color::Muted
},
))
.when(!single_repo, |this| {
this.child(repo_selector).when(show_separator, |this| {
this.child(
Label::new("/").size(LabelSize::Small).color(Color::Custom(
cx.theme().colors().text_muted.opacity(0.4),
)),
)
})
.child(repo_selector)
.when(show_separator, |this| {
this.child(
div()
.text_sm()
.text_color(cx.theme().colors().icon_muted.opacity(0.5))
.child("/"),
)
})
.child(branch_selector),
)

View file

@ -30,7 +30,6 @@ pub struct GitPanelSettings {
pub diff_stats: bool,
pub show_count_badge: bool,
pub starts_open: bool,
pub commit_title_max_length: usize,
}
#[derive(Default)]
@ -77,7 +76,6 @@ impl Settings for GitPanelSettings {
diff_stats: git_panel.diff_stats.unwrap(),
show_count_badge: git_panel.show_count_badge.unwrap(),
starts_open: git_panel.starts_open.unwrap(),
commit_title_max_length: git_panel.commit_title_max_length.unwrap(),
}
}
}

View file

@ -359,9 +359,13 @@ impl ProjectDiff {
);
match branch_diff.read(cx).diff_base() {
DiffBase::Head => {}
DiffBase::Merge { .. } => diff_display_editor.disable_diff_hunk_controls(cx),
DiffBase::Merge { .. } => diff_display_editor.set_render_diff_hunk_controls(
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
cx,
),
}
diff_display_editor.rhs_editor().update(cx, |editor, cx| {
editor.disable_diagnostics(cx);
editor.set_show_diff_review_button(true, cx);
match branch_diff.read(cx).diff_base() {

View file

@ -178,9 +178,14 @@ impl TextDiffView {
window,
cx,
);
splittable.disable_diff_hunk_controls(cx);
splittable.rhs_editor().update(cx, |editor, _cx| {
splittable.set_render_diff_hunk_controls(
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
cx,
);
splittable.rhs_editor().update(cx, |editor, cx| {
editor.start_temporary_diff_override();
editor.disable_diagnostics(cx);
editor.set_expand_all_diff_hunks(cx);
});
splittable
});

View file

@ -787,232 +787,23 @@ async fn open_worktree_workspace(
window_handle.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
if is_creating_new_worktree {
new_workspace.update(cx, |workspace, cx| {
workspace.run_create_worktree_tasks(window, cx);
new_workspace.update(cx, |workspace, cx| {
workspace.run_create_worktree_tasks(window, cx);
});
})?;
if let Some(dock_position) = focused_dock {
if is_creating_new_worktree {
if let Some(dock_position) = focused_dock {
window_handle.update(cx, |_multi_workspace, window, cx| {
new_workspace.update(cx, |workspace, cx| {
let dock = workspace.dock_at_position(dock_position);
if let Some(panel) = dock.read(cx).active_panel() {
panel.panel_focus_handle(cx).focus(window, cx);
}
}
});
});
})?;
}
})?;
}
anyhow::Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use fs::Fs;
use gpui::{App, Task, TestAppContext};
use language::language_settings::AllLanguageSettings;
use project::project_settings::ProjectSettings;
use project::task_store::{TaskSettingsLocation, TaskStore};
use project::{FakeFs, WorktreeSettings};
use serde_json::json;
use settings::{SettingsLocation, SettingsStore};
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::sync::Mutex;
use task::SpawnInTerminal;
use theme::LoadThemes;
use util::path;
use util::rel_path::rel_path;
use workspace::{TerminalProvider, WorkspaceSettings};
struct CountingTerminalProvider {
spawned_task_labels: Arc<Mutex<Vec<String>>>,
}
impl TerminalProvider for CountingTerminalProvider {
fn spawn(
&self,
task: SpawnInTerminal,
_window: &mut ui::Window,
_cx: &mut App,
) -> Task<Option<anyhow::Result<ExitStatus>>> {
self.spawned_task_labels
.lock()
.expect("terminal spawn mutex should not be poisoned")
.push(task.label);
Task::ready(Some(Ok(ExitStatus::default())))
}
}
fn init_test(cx: &mut TestAppContext) {
zlog::init_test();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme_settings::init(LoadThemes::JustBase, cx);
AllLanguageSettings::register(cx);
editor::init(cx);
ProjectSettings::register(cx);
WorktreeSettings::register(cx);
WorkspaceSettings::register(cx);
TaskStore::init(None);
});
}
fn install_counting_provider_and_worktree_hook(
workspace: &Entity<Workspace>,
spawned_task_labels: &Arc<Mutex<Vec<String>>>,
main_project_root: &Path,
hook_tasks_json: &str,
cx: &mut App,
) {
workspace.update(cx, |workspace, cx| {
workspace.set_terminal_provider(CountingTerminalProvider {
spawned_task_labels: spawned_task_labels.clone(),
});
let project = workspace.project().clone();
let Some(worktree) = project.read(cx).worktrees(cx).next() else {
return;
};
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let worktree_root = worktree.abs_path().to_path_buf();
if worktree_root == main_project_root {
return;
}
let Some(task_inventory) = project
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.cloned()
else {
return;
};
task_inventory.update(cx, |inventory, _| {
inventory
.update_file_based_tasks(
TaskSettingsLocation::Worktree(SettingsLocation {
worktree_id,
path: rel_path(".zed"),
}),
Some(hook_tasks_json),
)
.expect("should inject create_worktree hook tasks for linked worktree");
});
});
}
#[gpui::test]
async fn test_create_worktree_hook_does_not_run_when_switching_back_to_main_worktree(
cx: &mut TestAppContext,
) {
init_test(cx);
let hook_tasks_json = r#"[{"label":"setup worktree","command":"echo","hide":"never","hooks":["create_worktree"]}]"#;
let fs = FakeFs::new(cx.background_executor.clone());
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
fs.insert_tree(
"/root",
json!({
"project": {
".git": {},
".zed": {
"tasks.json": hook_tasks_json,
},
"src": {
"main.rs": "fn main() {}",
},
},
}),
)
.await;
let main_project_root = PathBuf::from(path!("/root/project"));
let project = Project::test(fs.clone(), [main_project_root.as_path()], cx).await;
project
.update(cx, |project, cx| project.git_scans_complete(cx))
.await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let spawned_task_labels = Arc::new(Mutex::new(Vec::new()));
multi_workspace.update(cx, |multi_workspace, cx| {
multi_workspace.retain_active_workspace(cx);
let active_workspace = multi_workspace.workspace().clone();
install_counting_provider_and_worktree_hook(
&active_workspace,
&spawned_task_labels,
&main_project_root,
hook_tasks_json,
cx,
);
});
let main_workspace =
multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
main_workspace.update_in(cx, |workspace, window, cx| {
handle_create_worktree(
workspace,
&zed_actions::CreateWorktree {
worktree_name: Some("feature".to_string()),
branch_target: NewWorktreeBranchTarget::CurrentBranch,
},
window,
None,
cx,
);
});
cx.run_until_parked();
let active_workspace =
multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
cx.update(|_, cx| {
install_counting_provider_and_worktree_hook(
&active_workspace,
&spawned_task_labels,
&main_project_root,
hook_tasks_json,
cx,
);
});
active_workspace.update_in(cx, |workspace, window, cx| {
workspace.run_create_worktree_tasks(window, cx);
});
cx.run_until_parked();
assert_eq!(
spawned_task_labels
.lock()
.expect("terminal spawn mutex should not be poisoned")
.as_slice(),
["setup worktree"],
"create_worktree hook should run once for the created linked worktree"
);
active_workspace.update_in(cx, |workspace, window, cx| {
handle_switch_worktree(
workspace,
&zed_actions::SwitchWorktree {
path: main_project_root.clone(),
display_name: "project".to_string(),
},
window,
None,
cx,
);
});
cx.run_until_parked();
assert_eq!(
spawned_task_labels
.lock()
.expect("terminal spawn mutex should not be poisoned")
.as_slice(),
["setup worktree"],
"switching back to the main worktree should not rerun create_worktree hooks"
);
}
}

View file

@ -1680,7 +1680,8 @@ impl App {
self.globals_by_type
.get(&TypeId::of::<G>())
.map(|any_state| any_state.downcast_ref::<G>().unwrap())
.unwrap_or_else(|| panic!("no state of type {} exists", type_name::<G>()))
.with_context(|| format!("no state of type {} exists", type_name::<G>()))
.unwrap()
}
/// Access the global of the given type if a value has been assigned.
@ -1698,7 +1699,8 @@ impl App {
self.globals_by_type
.get_mut(&global_type)
.and_then(|any_state| any_state.downcast_mut::<G>())
.unwrap_or_else(|| panic!("no state of type {} exists", type_name::<G>()))
.with_context(|| format!("no state of type {} exists", type_name::<G>()))
.unwrap()
}
/// Access the global of the given type mutably. A default value is assigned if a global of this type has not
@ -1733,7 +1735,7 @@ impl App {
*self
.globals_by_type
.remove(&global_type)
.unwrap_or_else(|| panic!("no global added for {}", type_name::<G>()))
.unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::<G>()))
.downcast()
.unwrap()
}

View file

@ -738,7 +738,7 @@ pub trait InteractiveElement: Sized {
fn key_context<C, E>(mut self, key_context: C) -> Self
where
C: TryInto<KeyContext, Error = E>,
E: std::fmt::Display,
E: Debug,
{
if let Some(key_context) = key_context.try_into().log_err() {
self.interactivity().key_context = Some(key_context);

View file

@ -1,7 +1,7 @@
use crate::{App, PlatformDispatcher, PlatformScheduler};
use futures::channel::mpsc;
use futures::prelude::*;
use gpui_util::{TryFutureExt, TryFutureExtBacktrace};
use gpui_util::TryFutureExt;
use scheduler::Instant;
use scheduler::Scheduler;
use std::{
@ -84,7 +84,7 @@ impl<T> Task<T> {
impl<T, E> Task<Result<T, E>>
where
T: 'static,
E: 'static + std::fmt::Display,
E: 'static + Debug,
{
/// Run the task to completion in the background and log any errors that occur.
#[track_caller]
@ -96,22 +96,6 @@ where
}
}
impl<T, E> Task<Result<T, E>>
where
T: 'static,
E: 'static + std::fmt::Debug,
{
/// Like [`Self::detach_and_log_err`], but uses `{:?}` formatting on failure so `anyhow::Error`
/// values emit their full backtrace. Prefer `detach_and_log_err` unless a backtrace is wanted.
#[track_caller]
pub fn detach_and_log_err_with_backtrace(self, cx: &App) {
let location = *core::panic::Location::caller();
cx.foreground_executor()
.spawn(self.log_tracked_err_with_backtrace(location))
.detach();
}
}
impl<T> std::future::Future for Task<T> {
type Output = T;

View file

@ -208,16 +208,8 @@ impl ShapedLine {
}
// Split text
let left_text = if byte_index == self.text.len() {
self.text.clone()
} else {
SharedString::new(&self.text[..byte_index])
};
let right_text = if byte_index == 0 {
self.text.clone()
} else {
SharedString::new(&self.text[byte_index..])
};
let left_text = SharedString::new(self.text[..byte_index].to_string());
let right_text = SharedString::new(self.text[byte_index..].to_string());
let left_width = x_offset;
let right_width = self.layout.width - left_width;

View file

@ -2237,13 +2237,7 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
let paths: SmallVec<[_; 2]> = file_list
.lines()
.filter_map(|path| Url::parse(path).log_err())
.filter_map(|url| match url.to_file_path() {
Ok(url) => Some(url),
Err(()) => {
log::error!("Failed turn {url:?} into a file path");
None
}
})
.filter_map(|url| url.to_file_path().log_err())
.collect();
let position = Point::new(x.into(), y.into());

View file

@ -908,13 +908,7 @@ impl X11Client {
let paths: SmallVec<[_; 2]> = file_list
.lines()
.filter_map(|path| Url::parse(path).log_err())
.filter_map(|url| match url.to_file_path() {
Ok(url) => Some(url),
Err(()) => {
log::error!("Failed turn {url:?} into a file path");
None
}
})
.filter_map(|url| url.to_file_path().log_err())
.collect();
let input = PlatformInput::FileDrop(FileDropEvent::Entered {
position: state.xdnd_state.position,

View file

@ -79,12 +79,6 @@ pub trait ResultExt<E> {
type Ok;
fn log_err(self) -> Option<Self::Ok>;
/// Like [`ResultExt::log_err`], but uses `{:?}` formatting so `anyhow::Error` values emit their
/// full backtrace. Reach for this only when a backtrace is genuinely wanted — most call sites
/// should stick with `log_err` / `warn_on_err`, whose output is a single chained error message.
fn log_err_with_backtrace(self) -> Option<Self::Ok>
where
E: std::fmt::Debug;
/// Assert that this result should never be an error in development or tests.
fn debug_assert_ok(self, reason: &str) -> Self;
fn warn_on_err(self) -> Option<Self::Ok>;
@ -96,7 +90,7 @@ pub trait ResultExt<E> {
impl<T, E> ResultExt<E> for Result<T, E>
where
E: std::fmt::Display,
E: std::fmt::Debug,
{
type Ok = T;
@ -105,28 +99,10 @@ where
self.log_with_level(log::Level::Error)
}
#[track_caller]
fn log_err_with_backtrace(self) -> Option<T>
where
E: std::fmt::Debug,
{
match self {
Ok(value) => Some(value),
Err(error) => {
log_error_with_caller(
*Location::caller(),
DebugAsDisplay(&error),
log::Level::Error,
);
None
}
}
}
#[track_caller]
fn debug_assert_ok(self, reason: &str) -> Self {
if let Err(error) = &self {
debug_panic!("{reason} - {error:#}");
debug_panic!("{reason} - {error:?}");
}
self
}
@ -157,7 +133,7 @@ where
fn log_error_with_caller<E>(caller: core::panic::Location<'_>, error: E, level: log::Level)
where
E: std::fmt::Display,
E: std::fmt::Debug,
{
#[cfg(not(windows))]
let file = caller.file();
@ -180,7 +156,7 @@ where
&log::Record::builder()
.target(module_path.as_deref().unwrap_or(""))
.module_path(file.as_deref())
.args(format_args!("{:#}", error))
.args(format_args!("{:?}", error))
.file(Some(caller.file()))
.line(Some(caller.line()))
.level(level)
@ -188,20 +164,10 @@ where
);
}
pub fn log_err<E: std::fmt::Display>(error: &E) {
pub fn log_err<E: std::fmt::Debug>(error: &E) {
log_error_with_caller(*Location::caller(), error, log::Level::Error);
}
// Forces `{:?}` formatting through a `Display`-bounded logging helper so `anyhow::Error` emits a
// backtrace instead of the single-line chained message produced by its `Display`/`{:#}` forms.
struct DebugAsDisplay<'a, E>(&'a E);
impl<E: std::fmt::Debug> std::fmt::Display for DebugAsDisplay<'_, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
pub trait TryFutureExt {
fn log_err(self) -> LogErrorFuture<Self>
where
@ -219,25 +185,10 @@ pub trait TryFutureExt {
Self: Sized;
}
/// `{:?}`-formatting companion to [`TryFutureExt`]; emits a backtrace for `anyhow::Error`. Prefer
/// [`TryFutureExt`] unless a backtrace is genuinely wanted.
pub trait TryFutureExtBacktrace {
fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture<Self>
where
Self: Sized;
fn log_tracked_err_with_backtrace(
self,
location: core::panic::Location<'static>,
) -> LogErrorWithBacktraceFuture<Self>
where
Self: Sized;
}
impl<F, T, E> TryFutureExt for F
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Display,
E: std::fmt::Debug,
{
#[track_caller]
fn log_err(self) -> LogErrorFuture<Self>
@ -272,38 +223,13 @@ where
}
}
impl<F, T, E> TryFutureExtBacktrace for F
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
#[track_caller]
fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture<Self>
where
Self: Sized,
{
let location = Location::caller();
LogErrorWithBacktraceFuture(self, log::Level::Error, *location)
}
fn log_tracked_err_with_backtrace(
self,
location: core::panic::Location<'static>,
) -> LogErrorWithBacktraceFuture<Self>
where
Self: Sized,
{
LogErrorWithBacktraceFuture(self, log::Level::Error, location)
}
}
#[must_use]
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
impl<F, T, E> Future for LogErrorFuture<F>
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Display,
E: std::fmt::Debug,
{
type Output = Option<T>;
@ -324,33 +250,6 @@ where
}
}
#[must_use]
pub struct LogErrorWithBacktraceFuture<F>(F, log::Level, core::panic::Location<'static>);
impl<F, T, E> Future for LogErrorWithBacktraceFuture<F>
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
type Output = Option<T>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let level = self.1;
let location = self.2;
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
match inner.poll(cx) {
Poll::Ready(output) => Poll::Ready(match output {
Ok(output) => Some(output),
Err(error) => {
log_error_with_caller(location, DebugAsDisplay(&error), level);
None
}
}),
Poll::Pending => Poll::Pending,
}
}
}
pub struct UnwrapFuture<F>(F);
impl<F, T, E> Future for UnwrapFuture<F>

View file

@ -13,14 +13,6 @@
"{"
"}" @end) @indent
(field_declaration_list
(access_specifier) @start
"}" @end) @indent
(field_declaration_list
(access_specifier)
(access_specifier) @outdent)
(_
"("
")" @end) @indent

View file

@ -1,8 +1,8 @@
pub mod row_chunk;
use crate::{
ByteContent, DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig,
PLAIN_TEXT, RunnableCapture, RunnableTag, TextObject, TreeSitterOptions, analyze_byte_content,
DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig, PLAIN_TEXT,
RunnableCapture, RunnableTag, TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
language_settings::{AutoIndentMode, LanguageSettings},
outline::OutlineItem,
@ -1579,21 +1579,16 @@ impl Buffer {
let target_encoding = force_encoding.unwrap_or(current_encoding);
let bytes = load_bytes_task.await?;
anyhow::ensure!(
analyze_byte_content(&bytes) != ByteContent::Binary,
"Binary files are not supported"
);
let is_unicode = target_encoding == encoding_rs::UTF_8
|| target_encoding == encoding_rs::UTF_16LE
|| target_encoding == encoding_rs::UTF_16BE;
let (new_text, has_bom, encoding_used) = if force_encoding.is_some() && !is_unicode {
let bytes = load_bytes_task.await?;
let (cow, _had_errors) = target_encoding.decode_without_bom_handling(&bytes);
(cow.into_owned(), false, target_encoding)
} else {
let bytes = load_bytes_task.await?;
let (cow, used_enc, _had_errors) = target_encoding.decode(&bytes);
let actual_has_bom = if used_enc == encoding_rs::UTF_8 {

View file

@ -1,158 +0,0 @@
pub const FILE_ANALYSIS_BYTES: usize = 1024;
#[derive(Debug, PartialEq)]
pub enum ByteContent {
Utf16Le,
Utf16Be,
Binary,
Unknown,
}
// Heuristic check using null byte distribution plus a generic text-likeness
// heuristic. This prefers UTF-16 when many bytes are NUL and otherwise
// distinguishes between text-like and binary-like content.
pub fn analyze_byte_content(bytes: &[u8]) -> ByteContent {
if bytes.len() < 2 {
return ByteContent::Unknown;
}
if is_known_binary_header(bytes) {
return ByteContent::Binary;
}
let limit = bytes.len().min(FILE_ANALYSIS_BYTES);
let mut even_null_count = 0usize;
let mut odd_null_count = 0usize;
let mut non_text_like_count = 0usize;
for (i, &byte) in bytes[..limit].iter().enumerate() {
if byte == 0 {
if i % 2 == 0 {
even_null_count += 1;
} else {
odd_null_count += 1;
}
non_text_like_count += 1;
continue;
}
let is_text_like = match byte {
b'\t' | b'\n' | b'\r' | 0x0C => true,
0x20..=0x7E => true,
// Treat bytes that are likely part of UTF-8 or single-byte encodings as text-like.
0x80..=0xBF | 0xC2..=0xF4 => true,
_ => false,
};
if !is_text_like {
non_text_like_count += 1;
}
}
let total_null_count = even_null_count + odd_null_count;
// If there are no NUL bytes at all, this is overwhelmingly likely to be text.
if total_null_count == 0 {
return ByteContent::Unknown;
}
let has_significant_nulls = total_null_count >= limit / 16;
let nulls_skew_to_even = even_null_count > odd_null_count * 4;
let nulls_skew_to_odd = odd_null_count > even_null_count * 4;
if has_significant_nulls {
let sample = &bytes[..limit];
// UTF-16BE ASCII: [0x00, char] — nulls at even positions (high byte first)
// UTF-16LE ASCII: [char, 0x00] — nulls at odd positions (low byte first)
if nulls_skew_to_even && is_plausible_utf16_text(sample, false) {
return ByteContent::Utf16Be;
}
if nulls_skew_to_odd && is_plausible_utf16_text(sample, true) {
return ByteContent::Utf16Le;
}
return ByteContent::Binary;
}
if non_text_like_count * 100 < limit * 8 {
ByteContent::Unknown
} else {
ByteContent::Binary
}
}
fn is_known_binary_header(bytes: &[u8]) -> bool {
bytes.starts_with(b"%PDF-") // PDF
|| bytes.starts_with(b"PK\x03\x04") // ZIP local header
|| bytes.starts_with(b"PK\x05\x06") // ZIP end of central directory
|| bytes.starts_with(b"PK\x07\x08") // ZIP spanning/splitting
|| bytes.starts_with(b"\x89PNG\r\n\x1a\n") // PNG
|| bytes.starts_with(b"\xFF\xD8\xFF") // JPEG
|| bytes.starts_with(b"GIF87a") // GIF87a
|| bytes.starts_with(b"GIF89a") // GIF89a
|| bytes.starts_with(b"IWAD") // Doom IWAD archive
|| bytes.starts_with(b"PWAD") // Doom PWAD archive
|| bytes.starts_with(b"RIFF") // WAV, AVI, WebP
|| bytes.starts_with(b"OggS") // OGG (Vorbis, Opus, FLAC)
|| bytes.starts_with(b"fLaC") // FLAC
|| bytes.starts_with(b"ID3") // MP3 with ID3v2 tag
|| bytes.starts_with(b"\xFF\xFB") // MP3 frame sync (MPEG1 Layer3)
|| bytes.starts_with(b"\xFF\xFA") // MP3 frame sync (MPEG1 Layer3)
|| bytes.starts_with(b"\xFF\xF3") // MP3 frame sync (MPEG2 Layer3)
|| bytes.starts_with(b"\xFF\xF2") // MP3 frame sync (MPEG2 Layer3)
}
// Null byte skew alone is not enough to identify UTF-16 -- binary formats with
// small 16-bit values (like PCM audio) produce the same pattern. Decode the
// bytes as UTF-16 and reject if too many code units land in control character
// ranges or form unpaired surrogates, which real text almost never contains.
fn is_plausible_utf16_text(bytes: &[u8], little_endian: bool) -> bool {
let mut suspicious_count = 0usize;
let mut total = 0usize;
let mut i = 0;
while let Some(code_unit) = read_u16(bytes, i, little_endian) {
total += 1;
match code_unit {
0x0009 | 0x000A | 0x000C | 0x000D => {}
// C0/C1 control characters and non-characters
0x0000..=0x001F | 0x007F..=0x009F | 0xFFFE | 0xFFFF => suspicious_count += 1,
0xD800..=0xDBFF => {
let next_offset = i + 2;
let has_low_surrogate = read_u16(bytes, next_offset, little_endian)
.is_some_and(|next| (0xDC00..=0xDFFF).contains(&next));
if has_low_surrogate {
total += 1;
i += 2;
} else {
suspicious_count += 1;
}
}
// Lone low surrogate without a preceding high surrogate
0xDC00..=0xDFFF => suspicious_count += 1,
_ => {}
}
i += 2;
}
if total == 0 {
return false;
}
// Real UTF-16 text has near-zero control characters; binary data with
// small 16-bit values typically exceeds 5%. 2% provides a safe margin.
suspicious_count * 100 < total * 2
}
fn read_u16(bytes: &[u8], offset: usize, little_endian: bool) -> Option<u16> {
let pair = [*bytes.get(offset)?, *bytes.get(offset + 1)?];
if little_endian {
return Some(u16::from_le_bytes(pair));
}
Some(u16::from_be_bytes(pair))
}

View file

@ -9,7 +9,6 @@
mod buffer;
mod diagnostic;
mod diagnostic_set;
mod file_content;
mod language_registry;
pub mod language_settings;
@ -92,7 +91,6 @@ pub use buffer::Operation;
pub use buffer::*;
pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup};
pub use file_content::{ByteContent, FILE_ANALYSIS_BYTES, analyze_byte_content};
pub use language_registry::{
AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
QUERY_FILENAME_PREFIXES,

View file

@ -16,10 +16,10 @@ use itertools::{Either, Itertools};
use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens};
pub use settings::{
AutoIndentMode, CompletionSettingsContent, EditPredictionDataCollectionChoice,
EditPredictionPromptFormat, EditPredictionProvider, EditPredictionsMode, FormatOnSave,
Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LineEndingSetting,
LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
AutoIndentMode, CompletionSettingsContent, EditPredictionPromptFormat, EditPredictionProvider,
EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind,
LanguageSettingsContent, LineEndingSetting, LspInsertMode, RewrapBehavior,
ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
};
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
use shellexpand;
@ -478,11 +478,6 @@ pub struct EditPredictionSettings {
pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
pub examples_dir: Option<Arc<Path>>,
/// Controls whether training data collection is enabled.
///
/// `Default` means the value stored in the legacy KV store is used as a fallback,
/// preserving existing users' choices without a migration.
pub allow_data_collection: EditPredictionDataCollectionChoice,
}
impl EditPredictionSettings {
@ -872,7 +867,6 @@ impl settings::Settings for AllLanguageSettings {
ollama: ollama_settings,
open_ai_compatible_api: openai_compatible_settings,
examples_dir: edit_predictions.examples_dir,
allow_data_collection: edit_predictions.allow_data_collection.unwrap_or_default(),
},
defaults: default_language_settings,
languages,

View file

@ -365,9 +365,7 @@ impl LanguageModel for CopilotChatLanguageModel {
if model.supports_adaptive_thinking() {
if anthropic_request.thinking.is_some() {
anthropic_request.thinking = Some(anthropic::Thinking::Adaptive {
display: Some(anthropic::AdaptiveThinkingDisplay::Summarized),
});
anthropic_request.thinking = Some(anthropic::Thinking::Adaptive);
anthropic_request.output_config =
effort.map(|effort| anthropic::OutputConfig {
effort: Some(effort),

View file

@ -408,9 +408,7 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
);
if enable_thinking && effort.is_some() {
request.thinking = Some(anthropic::Thinking::Adaptive {
display: Some(anthropic::AdaptiveThinkingDisplay::Summarized),
});
request.thinking = Some(anthropic::Thinking::Adaptive);
request.output_config = Some(anthropic::OutputConfig { effort });
}

View file

@ -67,7 +67,6 @@ util.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-bash.workspace = true
tree-sitter-c.workspace = true

View file

@ -16,332 +16,6 @@ mod tests {
use std::num::NonZeroU32;
use unindent::Unindent;
#[gpui::test]
async fn test_cpp_autoindent_access_specifier(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
class Foo {
public:
void bar();
private:
int x;
};
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
class Foo {
public:
void bar();
private:
int x;
};
"#
.unindent(),
"members after access specifiers should be indented one level deeper than the specifier"
);
buffer
});
}
#[gpui::test]
async fn test_cpp_autoindent_access_specifier_next_line(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
class Foo {
public:
void bar();
void baz();
private:
int x;
};
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
class Foo {
public:
void bar();
void baz();
private:
int x;
};
"#
.unindent(),
"members after access specifiers should be indented one level deeper than the specifier"
);
buffer
});
}
#[gpui::test]
async fn test_cpp_autoindent_nested_class_access_specifiers(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
class Outer {
public:
class Inner {
public:
void inner_pub();
private:
int inner_priv;
};
private:
int outer_priv;
};
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
class Outer {
public:
class Inner {
public:
void inner_pub();
private:
int inner_priv;
};
private:
int outer_priv;
};
"#
.unindent(),
"nested class access specifiers should indent independently at each nesting level"
);
buffer
});
}
#[gpui::test]
async fn test_cpp_autoindent_consecutive_access_specifiers(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
class Foo {
public:
protected:
private:
int x;
};
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
class Foo {
public:
protected:
private:
int x;
};
"#
.unindent(),
"consecutive access specifiers with no members between them should all align at class level"
);
buffer
});
}
#[gpui::test]
async fn test_cpp_autoindent_indented_access_specifiers(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
class Foo {
int default_member;
public:
void pub_method();
private:
int priv_member;
};
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
class Foo {
int default_member;
public:
void pub_method();
private:
int priv_member;
};
"#
.unindent(),
"access specifiers should be indented one level inside class braces"
);
buffer
});
}
#[gpui::test]
async fn test_cpp_autoindent_access_specifier_with_method_bodies(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
class Foo {
public:
void bar() {
if (x)
y++;
}
private:
int get_x() {
return x;
}
int x;
};
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
class Foo {
public:
void bar() {
if (x)
y++;
}
private:
int get_x() {
return x;
}
int x;
};
"#
.unindent(),
"method bodies inside access specifier sections should compose brace and specifier indent"
);
buffer
});
}
#[gpui::test]
async fn test_cpp_autoindent_basic(cx: &mut TestAppContext) {
cx.update(|cx| {

View file

@ -999,6 +999,7 @@ impl SearchableItem for MarkdownPreviewView {
markdown.set_active_search_highlight(Some(index), cx);
markdown.request_autoscroll_to_source_index(start, cx);
});
cx.emit(SearchEvent::ActiveMatchChanged);
}
}

View file

@ -838,7 +838,7 @@ impl<D: PickerDelegate> Picker<D> {
el.with_width_from_item(Some(widest_item))
})
.flex_grow()
.py(DynamicSpacing::Base04.rems(cx))
.py_1()
.track_scroll(&scroll_handle)
.into_any_element(),
ElementContainer::List(state) => list(
@ -849,7 +849,7 @@ impl<D: PickerDelegate> Picker<D> {
)
.with_sizing_behavior(sizing_behavior)
.flex_grow()
.py(DynamicSpacing::Base04.rems(cx))
.py_2()
.into_any_element(),
}
}
@ -1123,16 +1123,13 @@ impl<D: PickerDelegate> Render for Picker<D> {
.when(self.delegate.match_count() == 0, |el| {
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
el.child(
v_flex()
.flex_grow()
.py(DynamicSpacing::Base04.rems(cx))
.child(
ListItem::new("empty_state")
.inset(true)
.spacing(ListItemSpacing::Sparse)
.disabled(true)
.child(Label::new(text).color(Color::Muted)),
),
v_flex().flex_grow().py_2().child(
ListItem::new("empty_state")
.inset(true)
.spacing(ListItemSpacing::Sparse)
.disabled(true)
.child(Label::new(text).color(Color::Muted)),
),
)
})
})

View file

@ -6688,6 +6688,9 @@ impl LspStore {
.into_response()
.context("resolve completion")?;
// We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve.
// Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
if let CompletionSource::Lsp {
@ -6706,40 +6709,6 @@ impl LspStore {
);
**lsp_completion = resolved_completion;
*resolved = true;
// We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not supposed to change during resolve.
// Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
//
// We still re-derive new_text here as a workaround for the specific
// VS Code TypeScript completion resolve flow that vtsls wraps:
// https://github.com/microsoft/vscode/blob/838b48504cd9a2338e2ca9e854da9cec990c4d57/extensions/typescript-language-features/src/languageFeatures/completions.ts#L218
//
// Some servers (e.g. vtsls with completeFunctionCalls) update
// insertText/textEdit during resolve to add snippet content like
// function call parentheses.
//
// vtsls resolve flow:
// https://github.com/yioneko/vtsls/blob/fecf52324a30e72dfab1537047556076720c1a5f/packages/service/src/service/completion.ts#L228-L244
// vtsls converter (isSnippet / insertTextFormat):
// https://github.com/yioneko/vtsls/blob/28e075105d7711d635ebf8aefc971bb8e1d2fe65/packages/service/src/utils/converter.ts#L149-L200
//
// NB: We only update the text content here, NOT the replace/insert
// ranges on `Completion`. Those ranges were converted to anchors from
// the original response and stay valid across buffer edits. The LSP
// ranges in the resolved text_edit are stale when completions are
// cached across keystrokes (see #34094).
let resolved_new_text = lsp_completion
.text_edit
.as_ref()
.map(|edit| match edit {
lsp::CompletionTextEdit::Edit(e) => e.new_text.clone(),
lsp::CompletionTextEdit::InsertAndReplace(e) => e.new_text.clone(),
})
.or_else(|| lsp_completion.insert_text.clone());
if let Some(mut resolved_new_text) = resolved_new_text {
LineEnding::normalize(&mut resolved_new_text);
completion.new_text = resolved_new_text;
}
}
Ok(())
}

View file

@ -221,7 +221,6 @@ impl Search {
query.clone(),
input_paths_tx,
sorted_search_results_tx,
tx.clone(),
))
.boxed_local(),
Self::open_buffers(
@ -413,21 +412,12 @@ impl Search {
query: Arc<SearchQuery>,
tx: Sender<InputPath>,
results: Sender<oneshot::Receiver<ProjectPath>>,
results_tx: Sender<SearchResult>,
) -> impl AsyncFnOnce(&mut AsyncApp) {
async move |cx| {
_ = maybe!(async move {
let gitignored_tracker = PathInclusionMatcher::new(query.clone());
let include_ignored = query.include_ignored();
for worktree in worktrees {
let scan_complete = worktree.read_with(cx, |worktree, _| {
worktree.as_local().map(|local| local.scan_complete())
});
if let Some(scan_complete) = scan_complete {
_ = results_tx.send(SearchResult::WaitingForScan).await;
scan_complete.await;
}
let (mut snapshot, worktree_settings) = worktree
.read_with(cx, |this, _| {
Some((this.snapshot(), this.as_local()?.settings()))

View file

@ -25,7 +25,6 @@ pub enum SearchResult {
ranges: Vec<Range<Anchor>>,
},
LimitReached,
WaitingForScan,
}
#[derive(Clone, Copy, PartialEq)]

View file

@ -130,7 +130,7 @@ impl TaskStore {
.payload
.task_variables
.into_iter()
.filter_map(|(k, v)| Some((k.parse().ok()?, v))),
.filter_map(|(k, v)| Some((k.parse().log_err()?, v))),
);
let snapshot = location.buffer.read(cx).snapshot();

View file

@ -6278,39 +6278,6 @@ async fn test_dirty_buffer_reloads_after_undo(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_buffer_file_change_to_binary_fails(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"file.txt": "",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let buffer = project
.update(cx, |p, cx| p.open_local_buffer(path!("/dir/file.txt"), cx))
.await
.unwrap();
fs.write(
path!("/dir/file.txt").as_ref(),
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01",
)
.await
.unwrap();
cx.executor().run_until_parked();
// Test that existing buffer is left untouched
buffer.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "");
});
}
#[gpui::test]
async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
init_test(cx);
@ -12313,7 +12280,7 @@ async fn search(
SearchResult::Buffer { buffer, ranges } => {
results.entry(buffer).or_insert(ranges);
}
SearchResult::LimitReached | SearchResult::WaitingForScan => {}
SearchResult::LimitReached => {}
}
}
Ok(results

View file

@ -1020,11 +1020,8 @@ impl PickerDelegate for RecentProjectsDelegate {
.collect()
};
if !matched_folders.is_empty() {
entries.push(ProjectPickerEntry::Header("Current Folders".into()));
for (index, positions) in matched_folders {
entries.push(ProjectPickerEntry::OpenFolder { index, positions });
}
for (index, positions) in matched_folders {
entries.push(ProjectPickerEntry::OpenFolder { index, positions });
}
}
@ -1285,7 +1282,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.child(
IconButton::new(("remove-folder", worktree_id.to_usize()), IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Remove Folder from Project"))
.tooltip(Tooltip::text("Remove Folder from Workspace"))
.on_click(cx.listener(move |picker, _, window, cx| {
let Some(workspace) = picker.delegate.workspace.upgrade() else {
return;
@ -1317,16 +1314,18 @@ impl PickerDelegate for RecentProjectsDelegate {
.child(
h_flex()
.id("open_folder_item")
.gap_3()
.w_full()
.gap_2p5()
.overflow_hidden()
.when(self.has_any_non_local_projects, |this| {
this.child(Icon::new(icon).color(Color::Muted))
})
.child(
v_flex()
.min_w_0()
.flex_1()
.child(
h_flex()
.min_w_0()
.gap_1()
.child(HighlightedLabel::new(
name.to_string(),
@ -1336,7 +1335,8 @@ impl PickerDelegate for RecentProjectsDelegate {
this.child(
Label::new(branch)
.color(Color::Muted)
.truncate(),
.truncate()
.flex_1(),
)
})
.when(is_active, |this| {
@ -1359,7 +1359,7 @@ impl PickerDelegate for RecentProjectsDelegate {
this.tooltip(move |_, cx| {
if let Some(branch) = tooltip_branch.clone() {
Tooltip::with_meta(
format!("{}/{}", name, branch),
branch,
None,
tooltip_path.clone(),
cx,
@ -1384,7 +1384,6 @@ impl PickerDelegate for RecentProjectsDelegate {
.map(|p| p.compact().to_string_lossy().to_string())
.collect();
let tooltip_path: SharedString = ordered_paths.join("\n").into();
let icon = icon_for_remote_connection(self.project_connection_options.as_ref());
let mut path_start_offset = 0;
let (match_labels, path_highlights): (Vec<_>, Vec<_>) = paths
@ -1439,10 +1438,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.child(
h_flex()
.id("open_project_info_container")
.gap_2p5()
.when(self.has_any_non_local_projects, |this| {
this.child(Icon::new(icon).color(Color::Muted))
})
.gap_3()
.child({
let mut highlighted = highlighted_match;
if !self.render_paths {
@ -1489,12 +1485,6 @@ impl PickerDelegate for RecentProjectsDelegate {
})
.unzip();
let tooltip_title = if paths.len() > 1 {
"Add Folders to this Project"
} else {
"Add Folder to this Project"
};
let prefix = match &location {
SerializedWorkspaceLocation::Remote(options) => {
Some(SharedString::from(options.display_name()))
@ -1519,9 +1509,9 @@ impl PickerDelegate for RecentProjectsDelegate {
.icon_size(IconSize::Small)
.tooltip(move |_, cx| {
Tooltip::with_meta(
tooltip_title,
"Add Folders to this Project",
None,
"As a multi-root folder",
"As a multi-root folder project",
cx,
)
})
@ -1584,7 +1574,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.child(
h_flex()
.id("project_info_container")
.gap_2p5()
.gap_3()
.flex_grow()
.when(self.has_any_non_local_projects, |this| {
this.child(Icon::new(icon).color(Color::Muted))
@ -1832,7 +1822,7 @@ impl PickerDelegate for RecentProjectsDelegate {
menu.context(focus_handle)
.when(show_add_to_workspace, |menu| {
menu.action(
"Add Folder to this Project",
"Add to this Workspace",
AddToWorkspace.boxed_clone(),
)
.separator()

View file

@ -1399,7 +1399,6 @@ impl BufferSearchBar {
}
let new_match_index = searchable_item
.match_index_for_direction(matches, index, direction, count, *token, window, cx);
self.active_match_index = Some(new_match_index);
searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
searchable_item.activate_match(new_match_index, matches, *token, window, cx);
@ -2258,8 +2257,8 @@ mod tests {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor in between matches and ensure that going to the previous match
// selects the closest match to the left of the cursor.
// Park the cursor in between matches and ensure that going to the previous match selects
// the closest match to the left.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
@ -2268,6 +2267,7 @@ mod tests {
});
});
search_bar.update_in(cx, |search_bar, window, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor
@ -2280,8 +2280,8 @@ mod tests {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor in between matches and ensure that going to the next match
// selects the closest match to the right of the cursor.
// Park the cursor in between matches and ensure that going to the next match selects the
// closest match to the right.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
@ -2290,6 +2290,7 @@ mod tests {
});
});
search_bar.update_in(cx, |search_bar, window, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_next_match(&SelectNextMatch, window, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor
@ -2302,8 +2303,8 @@ mod tests {
assert_eq!(search_bar.active_match_index, Some(1));
});
// Park the cursor after the last match and ensure that going to the previous match
// selects the last match.
// Park the cursor after the last match and ensure that going to the previous match selects
// the last match.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
@ -2312,6 +2313,7 @@ mod tests {
});
});
search_bar.update_in(cx, |search_bar, window, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor
@ -2324,8 +2326,8 @@ mod tests {
assert_eq!(search_bar.active_match_index, Some(2));
});
// Park the cursor after the last match and ensure that going to the next match
// wraps around and selects the first match.
// Park the cursor after the last match and ensure that going to the next match selects the
// first match.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
@ -2334,6 +2336,7 @@ mod tests {
});
});
search_bar.update_in(cx, |search_bar, window, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_next_match(&SelectNextMatch, window, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor
@ -2347,7 +2350,7 @@ mod tests {
});
// Park the cursor before the first match and ensure that going to the previous match
// wraps around and selects the last match.
// selects the last match.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([

View file

@ -237,7 +237,6 @@ pub struct ProjectSearch {
search_id: usize,
no_results: Option<bool>,
limit_reached: bool,
waiting_for_scan: bool,
search_history_cursor: SearchHistoryCursor,
search_included_history_cursor: SearchHistoryCursor,
search_excluded_history_cursor: SearchHistoryCursor,
@ -300,7 +299,6 @@ impl ProjectSearch {
search_id: 0,
no_results: None,
limit_reached: false,
waiting_for_scan: false,
search_history_cursor: Default::default(),
search_included_history_cursor: Default::default(),
search_excluded_history_cursor: Default::default(),
@ -325,7 +323,6 @@ impl ProjectSearch {
search_id: self.search_id,
no_results: self.no_results,
limit_reached: self.limit_reached,
waiting_for_scan: false,
search_history_cursor: self.search_history_cursor.clone(),
search_included_history_cursor: self.search_included_history_cursor.clone(),
search_excluded_history_cursor: self.search_excluded_history_cursor.clone(),
@ -425,17 +422,15 @@ impl ProjectSearch {
.update(cx, |excerpts, cx| excerpts.clear(cx));
project_search.no_results = Some(true);
project_search.limit_reached = false;
project_search.waiting_for_scan = false;
})
.ok()?;
let mut limit_reached = false;
while let Some(results) = matches.next().await {
let (buffers_with_ranges, has_reached_limit, is_waiting_for_scan) = cx
let (buffers_with_ranges, has_reached_limit) = cx
.background_executor()
.spawn(async move {
let mut limit_reached = false;
let mut waiting_for_scan = false;
let mut buffers_with_ranges = Vec::with_capacity(results.len());
for result in results {
match result {
@ -445,23 +440,12 @@ impl ProjectSearch {
project::search::SearchResult::LimitReached => {
limit_reached = true;
}
project::search::SearchResult::WaitingForScan => {
waiting_for_scan = true;
}
}
}
(buffers_with_ranges, limit_reached, waiting_for_scan)
(buffers_with_ranges, limit_reached)
})
.await;
limit_reached |= has_reached_limit;
if is_waiting_for_scan {
project_search
.update(cx, |project_search, cx| {
project_search.waiting_for_scan = true;
cx.notify();
})
.ok()?;
}
let mut new_ranges = project_search
.update(cx, |project_search, cx| {
project_search.excerpts.update(cx, |excerpts, cx| {
@ -499,7 +483,6 @@ impl ProjectSearch {
project_search.no_results = Some(false);
}
project_search.limit_reached = limit_reached;
project_search.waiting_for_scan = false;
project_search.pending_search.take();
cx.notify();
})
@ -533,11 +516,8 @@ impl Render for ProjectSearchView {
let model = self.entity.read(cx);
let has_no_results = model.no_results.unwrap_or(false);
let is_search_underway = model.pending_search.is_some();
let is_waiting_for_scan = model.waiting_for_scan;
let heading_text = if is_waiting_for_scan {
"Loading project…"
} else if is_search_underway {
let heading_text = if is_search_underway {
"Searching…"
} else if has_no_results {
"No Results"

View file

@ -182,14 +182,6 @@ pub struct EditPredictionSettingsContent {
pub open_ai_compatible_api: Option<CustomEditPredictionProviderSettingsContent>,
/// The directory where manually captured edit prediction examples are stored.
pub examples_dir: Option<Arc<Path>>,
/// Controls whether Zed may collect training data when using Zed's Edit Predictions.
/// Data is only ever captured for files in projects that are detected as open source.
///
/// - `"default"`: use the preference previously set via the status-bar toggle,
/// or false if no preference has been stored.
/// - `"yes"`: allow data collection for files in open-source projects.
/// - `"no"`: never allow data collection.
pub allow_data_collection: Option<EditPredictionDataCollectionChoice>,
}
#[with_fallible_options]
@ -326,33 +318,6 @@ pub struct OllamaEditPredictionSettingsContent {
pub prompt_format: Option<EditPredictionPromptFormat>,
}
/// Controls whether Zed collects training data when using Zed's Edit Predictions.
#[derive(
Copy,
Clone,
Debug,
Default,
Eq,
PartialEq,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionDataCollectionChoice {
/// Use the preference previously set via the status-bar toggle, or false
/// if no preference has been stored.
#[default]
Default,
/// Allow Zed to collect training data from open-source projects.
Yes,
/// Never allow training data collection.
No,
}
/// The mode in which edit predictions should be displayed.
#[derive(
Copy,

View file

@ -647,12 +647,6 @@ pub struct GitPanelSettingsContent {
///
/// Default: false
pub starts_open: Option<bool>,
/// Maximum length of the commit message title before a warning is shown.
/// Set to 0 to disable.
///
/// Default: 72
pub commit_title_max_length: Option<usize>,
}
#[derive(

View file

@ -1036,9 +1036,6 @@ pub struct ThemeColorsContent {
/// Background color for Vim yank highlight.
#[serde(rename = "vim.yank.background")]
pub vim_yank_background: Option<String>,
/// Foreground color for Helix jump labels.
#[serde(rename = "vim.helix_jump_label.foreground")]
pub vim_helix_jump_label_foreground: Option<String>,
/// Background color for Vim Helix Normal mode indicator.
#[serde(rename = "vim.helix_normal.background")]
pub vim_helix_normal_background: Option<String>,

View file

@ -185,11 +185,10 @@ pub fn render_ollama_model_picker(
field.json_path,
window,
cx,
move |settings, app| {
move |settings, _cx| {
(field.write)(
settings,
Some(settings::OllamaModelName(model_name.to_string())),
app,
);
},
)

File diff suppressed because it is too large Load diff

View file

@ -121,8 +121,8 @@ fn render_settings_audio_device_dropdown<T: AsRef<Option<String>> + From<Option<
field.json_path,
window,
cx,
move |settings, app| {
(field.write)(settings, value, app);
move |settings, _cx| {
(field.write)(settings, value);
},
)
.log_err();

View file

@ -5,7 +5,7 @@ use edit_prediction::{
open_ai_compatible::{open_ai_compatible_api_token, open_ai_compatible_api_url},
};
use edit_prediction_ui::{get_available_providers, set_completion_provider};
use gpui::{App, Entity, ScrollHandle, prelude::*};
use gpui::{Entity, ScrollHandle, prelude::*};
use language::language_settings::AllLanguageSettings;
use settings::Settings as _;
@ -373,7 +373,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
.api_url
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -406,7 +406,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
.model
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -439,7 +439,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
.prompt_format
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -469,7 +469,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
.max_output_tokens
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -504,7 +504,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
.api_url
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -537,7 +537,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
.model
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -570,7 +570,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
.prompt_format
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -600,7 +600,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
.max_output_tokens
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -635,7 +635,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
.api_url
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -668,7 +668,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
.max_tokens
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages
@ -698,7 +698,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
.model
.as_ref()
},
write: |settings, value, _app: &App| {
write: |settings, value| {
settings
.project
.all_languages

View file

@ -100,7 +100,7 @@ struct FocusFile(pub u32);
struct SettingField<T: 'static> {
pick: fn(&SettingsContent) -> Option<&T>,
write: fn(&mut SettingsContent, Option<T>, &App),
write: fn(&mut SettingsContent, Option<T>),
/// A json-path-like string that gives a unique-ish string that identifies
/// where in the JSON the setting is defined.
@ -149,7 +149,7 @@ impl<T: 'static> SettingField<T> {
fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
SettingField {
pick: |_| Some(&UnimplementedSettingField),
write: |_, _, _| unreachable!(),
write: |_, _| unreachable!(),
json_path: self.json_path,
}
}
@ -232,8 +232,8 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
None,
window,
cx,
move |settings, app| {
(this.write)(settings, value_to_set, app);
move |settings, _| {
(this.write)(settings, value_to_set);
},
)
// todo(settings_ui): Don't log err
@ -504,7 +504,6 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::TerminalBlink>(render_dropdown)
.add_basic_renderer::<settings::CursorShapeContent>(render_dropdown)
.add_basic_renderer::<settings::EditPredictionPromptFormat>(render_dropdown)
.add_basic_renderer::<settings::EditPredictionDataCollectionChoice>(render_dropdown)
.add_basic_renderer::<f32>(render_editable_number_field)
.add_basic_renderer::<u32>(render_editable_number_field)
.add_basic_renderer::<u64>(render_editable_number_field)
@ -4091,8 +4090,8 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
field.json_path,
window,
cx,
move |settings, app| {
(field.write)(settings, new_text.map(Into::into), app);
move |settings, _cx| {
(field.write)(settings, new_text.map(Into::into));
},
)
.log_err(); // todo(settings_ui) don't log err
@ -4123,8 +4122,8 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
let state = *state == ui::ToggleState::Selected;
update_settings_file(file.clone(), field.json_path, window, cx, move |settings, app| {
(field.write)(settings, Some(state.into()), app);
update_settings_file(file.clone(), field.json_path, window, cx, move |settings, _cx| {
(field.write)(settings, Some(state.into()));
})
.log_err(); // todo(settings_ui) don't log err
}
@ -4158,8 +4157,8 @@ fn render_editable_number_field<T: NumberFieldType + Send + Sync>(
field.json_path,
window,
cx,
move |settings, app| {
(field.write)(settings, Some(value), app);
move |settings, _cx| {
(field.write)(settings, Some(value));
},
)
.log_err(); // todo(settings_ui) don't log err
@ -4198,8 +4197,8 @@ where
field.json_path,
window,
cx,
move |settings, app| {
(field.write)(settings, Some(value), app);
move |settings, _cx| {
(field.write)(settings, Some(value));
},
)
.log_err(); // todo(settings_ui) don't log err
@ -4253,8 +4252,8 @@ fn render_font_picker(
field.json_path,
window,
cx,
move |settings, app| {
(field.write)(settings, Some(font_name.to_string().into()), app);
move |settings, _cx| {
(field.write)(settings, Some(font_name.to_string().into()));
},
)
.log_err(); // todo(settings_ui) don't log err
@ -4303,11 +4302,10 @@ fn render_theme_picker(
field.json_path,
window,
cx,
move |settings, app| {
move |settings, _cx| {
(field.write)(
settings,
Some(settings::ThemeName(theme_name.into())),
app,
);
},
)
@ -4357,11 +4355,10 @@ fn render_icon_theme_picker(
field.json_path,
window,
cx,
move |settings, app| {
move |settings, _cx| {
(field.write)(
settings,
Some(settings::IconThemeName(theme_name.into())),
app,
);
},
)

View file

@ -6141,45 +6141,6 @@ async fn test_archive_thread_keeps_metadata_but_hides_from_sidebar(cx: &mut Test
});
}
#[gpui::test]
async fn test_archive_thread_drops_retained_conversation_view(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/project-a", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
cx.run_until_parked();
let connection = acp_thread::StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Done".into()),
)]);
open_thread_with_connection(&panel, connection, cx);
send_message(&panel, cx);
let session_id = active_session_id(&panel, cx);
let thread_id = active_thread_id(&panel, cx);
cx.run_until_parked();
sidebar.read_with(cx, |sidebar, _| {
assert!(
is_active_session(sidebar, &session_id),
"expected the newly created thread to be active before archiving",
);
});
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.archive_thread(&session_id, window, cx);
});
cx.run_until_parked();
panel.read_with(cx, |panel, _| {
assert!(
!panel.is_retained_thread(&thread_id),
"archiving a thread must drop its ConversationView from retained_threads, \
but the archived thread id {thread_id:?} is still retained",
);
});
}
#[gpui::test]
async fn test_archive_thread_active_entry_management(cx: &mut TestAppContext) {
// Tests two archive scenarios:

View file

@ -43,7 +43,6 @@ url.workspace = true
util.workspace = true
urlencoding.workspace = true
parking_lot.workspace = true
percent-encoding.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View file

@ -138,10 +138,6 @@ pub(super) fn find_from_grid_point<T: EventListener>(
if let Ok(url) = Url::parse(&maybe_url_or_path) {
if let Ok(path) = url.to_file_path_ext(path_style) {
return (path.to_string_lossy().into_owned(), false, word_match);
} else if let Some(path) = try_osc8_url_to_path(url)
&& path_style.is_posix()
{
return (path, false, word_match);
}
}
// Fallback: strip file:// prefix if URL parsing fails
@ -158,22 +154,6 @@ pub(super) fn find_from_grid_point<T: EventListener>(
})
}
// OSC 8 mandates that file:// URIs must be encoded as file://{host}{path}
// We need to skip the {host} part if it's set
fn try_osc8_url_to_path(url: url::Url) -> Option<String> {
use percent_encoding::percent_decode;
if url.scheme() != "file" {
return None;
}
let bytes = url
.path_segments()?
.skip(1)
.flat_map(|segment| percent_decode(segment.as_bytes()))
.collect::<Vec<u8>>();
bytes.try_into().ok()
}
fn sanitize_url_punctuation<T: EventListener>(
url: String,
url_match: Match,

View file

@ -489,7 +489,6 @@ impl TerminalView {
pub fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
has_selection: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -498,6 +497,13 @@ impl TerminalView {
.upgrade()
.and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
.is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled());
let has_selection = self
.terminal
.read(cx)
.last_content
.selection_text
.as_ref()
.is_some_and(|text| !text.is_empty());
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.context(self.focus_handle.clone())
.action("New Terminal", Box::new(NewTerminal::default()))
@ -1243,21 +1249,12 @@ impl Render for TerminalView {
MouseButton::Right,
cx.listener(|this, event: &MouseDownEvent, window, cx| {
if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
let had_selection = this.terminal.read(cx).last_content.selection.is_some();
if !had_selection {
if this.terminal.read(cx).last_content.selection.is_none() {
this.terminal.update(cx, |terminal, _| {
terminal.select_word_at_event_position(event);
});
}
let has_selection = !had_selection
|| this
.terminal
.read(cx)
.last_content
.selection_text
.as_ref()
.is_some_and(|text| !text.is_empty());
this.deploy_context_menu(event.position, has_selection, window, cx);
};
this.deploy_context_menu(event.position, window, cx);
cx.notify();
}
}),

View file

@ -176,7 +176,6 @@ impl ThemeColors {
vim_visual_line_background: system.transparent,
vim_visual_block_background: system.transparent,
vim_yank_background: neutral().light_alpha().step_3(),
vim_helix_jump_label_foreground: red().light().step_9(),
vim_helix_normal_background: system.transparent,
vim_helix_select_background: system.transparent,
vim_normal_foreground: system.transparent,
@ -323,7 +322,6 @@ impl ThemeColors {
vim_visual_line_background: system.transparent,
vim_visual_block_background: system.transparent,
vim_yank_background: neutral().dark_alpha().step_4(),
vim_helix_jump_label_foreground: red().dark().step_9(),
vim_helix_normal_background: system.transparent,
vim_helix_select_background: system.transparent,
vim_normal_foreground: system.transparent,

View file

@ -260,7 +260,6 @@ pub(crate) fn zed_default_dark() -> Theme {
vim_visual_line_background: SystemColors::default().transparent,
vim_visual_block_background: SystemColors::default().transparent,
vim_yank_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.2),
vim_helix_jump_label_foreground: red,
vim_helix_normal_background: SystemColors::default().transparent,
vim_helix_select_background: SystemColors::default().transparent,
vim_normal_foreground: SystemColors::default().transparent,

View file

@ -177,8 +177,6 @@ pub struct ThemeColors {
pub vim_visual_block_background: Hsla,
/// Background color for Vim yank highlight.
pub vim_yank_background: Hsla,
/// Foreground color for Helix jump labels.
pub vim_helix_jump_label_foreground: Hsla,
/// Background color for Vim Helix Normal mode indicator.
pub vim_helix_normal_background: Hsla,
/// Background color for Vim Helix Select mode indicator.

View file

@ -26,18 +26,7 @@ impl Focusable for IconThemeSelector {
}
}
impl ModalView for IconThemeSelector {
fn on_before_dismiss(
&mut self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> workspace::DismissDecision {
self.picker.update(cx, |picker, cx| {
picker.delegate.revert_theme(cx);
});
workspace::DismissDecision::Dismiss(true)
}
}
impl ModalView for IconThemeSelector {}
impl IconThemeSelector {
pub fn new(
@ -143,13 +132,6 @@ impl IconThemeSelectorDelegate {
.unwrap_or(self.selected_index);
}
fn revert_theme(&mut self, cx: &mut App) {
if !self.selection_completed {
Self::set_icon_theme(self.original_theme.clone(), cx);
self.selection_completed = true;
}
}
fn set_icon_theme(name: IconThemeName, cx: &mut App) {
SettingsStore::update_global(cx, |store, _| {
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
@ -203,7 +185,10 @@ impl PickerDelegate for IconThemeSelectorDelegate {
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
self.revert_theme(cx);
if !self.selection_completed {
Self::set_icon_theme(self.original_theme.clone(), cx);
self.selection_completed = true;
}
self.selector
.update(cx, |_, cx| cx.emit(DismissEvent))

View file

@ -79,18 +79,7 @@ fn toggle_icon_theme_selector(
});
}
impl ModalView for ThemeSelector {
fn on_before_dismiss(
&mut self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> workspace::DismissDecision {
self.picker.update(cx, |picker, cx| {
picker.delegate.revert_theme(cx);
});
workspace::DismissDecision::Dismiss(true)
}
}
impl ModalView for ThemeSelector {}
struct ThemeSelector {
picker: Entity<Picker<ThemeSelectorDelegate>>,
@ -226,15 +215,6 @@ impl ThemeSelectorDelegate {
}
}
fn revert_theme(&mut self, cx: &mut App) {
if !self.selection_completed {
SettingsStore::update_global(cx, |store, _| {
store.override_global(self.original_theme_settings.clone());
});
self.selection_completed = true;
}
}
fn set_theme(&mut self, new_theme: Arc<Theme>, cx: &mut App) {
// Update the global (in-memory) theme settings.
SettingsStore::update_global(cx, |store, _| {
@ -391,7 +371,12 @@ impl PickerDelegate for ThemeSelectorDelegate {
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
self.revert_theme(cx);
if !self.selection_completed {
SettingsStore::update_global(cx, |store, _| {
store.override_global(self.original_theme_settings.clone());
});
self.selection_completed = true;
}
self.selector
.update(cx, |_, cx| cx.emit(DismissEvent))

View file

@ -775,11 +775,6 @@ pub fn theme_colors_refinement(
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(editor_document_highlight_read_background),
vim_helix_jump_label_foreground: this
.vim_helix_jump_label_foreground
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(status_colors.error),
vim_helix_normal_background: this
.vim_helix_normal_background
.as_ref()
@ -853,39 +848,3 @@ fn try_parse_color(color: &str) -> anyhow::Result<Hsla> {
Ok(hsla)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn helix_jump_label_color_uses_theme_color_or_status_error() {
let status_error = try_parse_color("#e63333").expect("valid color");
let override_color = try_parse_color("#00ff00").expect("valid color");
let status_colors = StatusColorsRefinement {
error: Some(status_error),
..Default::default()
};
let fallback_refinement =
theme_colors_refinement(&ThemeColorsContent::default(), &status_colors);
assert_eq!(
fallback_refinement.vim_helix_jump_label_foreground,
Some(status_error)
);
let override_refinement = theme_colors_refinement(
&ThemeColorsContent {
vim_helix_jump_label_foreground: Some("#00ff00".to_string()),
..Default::default()
},
&status_colors,
);
assert_eq!(
override_refinement.vim_helix_jump_label_foreground,
Some(override_color)
);
}
}

View file

@ -96,17 +96,11 @@ pub(crate) struct UiFontSize(Pixels);
impl Global for UiFontSize {}
/// In-memory override for the UI font size in the agent panel.
/// In-memory override for the font size in the agent panel.
#[derive(Default)]
pub struct AgentUiFontSize(Pixels);
pub struct AgentFontSize(Pixels);
impl Global for AgentUiFontSize {}
/// In-memory override for the buffer font size in the agent panel.
#[derive(Default)]
pub struct AgentBufferFontSize(Pixels);
impl Global for AgentBufferFontSize {}
impl Global for AgentFontSize {}
/// Represents the selection of a theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -384,7 +378,7 @@ impl ThemeSettings {
/// Returns the agent panel font size. Falls back to the UI font size if unset.
pub fn agent_ui_font_size(&self, cx: &App) -> Pixels {
cx.try_global::<AgentUiFontSize>()
cx.try_global::<AgentFontSize>()
.map(|size| size.0)
.or(self.agent_ui_font_size)
.map(clamp_font_size)
@ -393,7 +387,7 @@ impl ThemeSettings {
/// Returns the agent panel buffer font size.
pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels {
cx.try_global::<AgentBufferFontSize>()
cx.try_global::<AgentFontSize>()
.map(|size| size.0)
.or(self.agent_buffer_font_size)
.map(clamp_font_size)
@ -549,16 +543,16 @@ pub fn reset_ui_font_size(cx: &mut App) {
pub fn adjust_agent_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
let agent_ui_font_size = ThemeSettings::get_global(cx).agent_ui_font_size(cx);
let adjusted_size = cx
.try_global::<AgentUiFontSize>()
.try_global::<AgentFontSize>()
.map_or(agent_ui_font_size, |adjusted_size| adjusted_size.0);
cx.set_global(AgentUiFontSize(clamp_font_size(f(adjusted_size))));
cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size))));
cx.refresh_windows();
}
/// Resets the agent response font size in the agent panel to the default value.
pub fn reset_agent_ui_font_size(cx: &mut App) {
if cx.has_global::<AgentUiFontSize>() {
cx.remove_global::<AgentUiFontSize>();
if cx.has_global::<AgentFontSize>() {
cx.remove_global::<AgentFontSize>();
cx.refresh_windows();
}
}
@ -567,16 +561,16 @@ pub fn reset_agent_ui_font_size(cx: &mut App) {
pub fn adjust_agent_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
let agent_buffer_font_size = ThemeSettings::get_global(cx).agent_buffer_font_size(cx);
let adjusted_size = cx
.try_global::<AgentBufferFontSize>()
.try_global::<AgentFontSize>()
.map_or(agent_buffer_font_size, |adjusted_size| adjusted_size.0);
cx.set_global(AgentBufferFontSize(clamp_font_size(f(adjusted_size))));
cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size))));
cx.refresh_windows();
}
/// Resets the user message font size in the agent panel to the default value.
pub fn reset_agent_buffer_font_size(cx: &mut App) {
if cx.has_global::<AgentBufferFontSize>() {
cx.remove_global::<AgentBufferFontSize>();
if cx.has_global::<AgentFontSize>() {
cx.remove_global::<AgentFontSize>();
cx.refresh_windows();
}
}

View file

@ -28,12 +28,12 @@ pub use crate::schema::{
};
use crate::settings::adjust_buffer_font_size;
pub use crate::settings::{
AgentBufferFontSize, AgentUiFontSize, BufferLineHeight, FontFamilyName, IconThemeName,
IconThemeSelection, ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings,
adjust_agent_buffer_font_size, adjust_agent_ui_font_size, adjust_ui_font_size,
adjusted_font_size, appearance_to_mode, clamp_font_size, default_theme,
observe_buffer_font_size_adjustment, reset_agent_buffer_font_size, reset_agent_ui_font_size,
reset_buffer_font_size, reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font,
AgentFontSize, BufferLineHeight, FontFamilyName, IconThemeName, IconThemeSelection,
ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, adjust_agent_buffer_font_size,
adjust_agent_ui_font_size, adjust_ui_font_size, adjusted_font_size, appearance_to_mode,
clamp_font_size, default_theme, observe_buffer_font_size_adjustment,
reset_agent_buffer_font_size, reset_agent_ui_font_size, reset_buffer_font_size,
reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font,
};
pub use theme::UiDensity;

View file

@ -32,12 +32,9 @@ use gpui::{
pulsating_between,
};
use onboarding_banner::OnboardingBanner;
use project::{
Project, git_store::GitStoreEvent, project_settings::ProjectSettings,
trusted_worktrees::TrustedWorktrees,
};
use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
use remote::RemoteConnectionOptions;
use settings::Settings as _;
use settings::Settings;
use std::sync::Arc;
use std::time::Duration;
@ -178,7 +175,6 @@ impl Render for TitleBar {
let title_bar_settings = *TitleBarSettings::get_global(cx);
let button_layout = title_bar_settings.button_layout;
let is_git_enabled = ProjectSettings::get_global(cx).git.enabled.status;
let show_menus = show_menus(cx);
@ -235,9 +231,9 @@ impl Render for TitleBar {
.child(self.render_project_name(project_name, window, cx))
})
.when_some(
repository.filter(|_| is_git_enabled),
repository.filter(|_| title_bar_settings.show_branch_name),
|title_bar, repository| {
title_bar.children(self.render_worktree_and_branch(
title_bar.children(self.render_project_branch(
repository,
linked_worktree_name,
cx,
@ -833,7 +829,7 @@ impl TitleBar {
.anchor(gpui::Anchor::TopLeft)
}
fn render_worktree_and_branch(
fn render_project_branch(
&self,
repository: Entity<project::git_store::Repository>,
linked_worktree_name: Option<SharedString>,
@ -878,6 +874,7 @@ impl TitleBar {
(branch_name, icon_info, is_detached_head)
};
let branch_name = branch_name?;
let settings = TitleBarSettings::get_global(cx);
let effective_repository = Some(repository);
@ -938,77 +935,66 @@ impl TitleBar {
.anchor(gpui::Anchor::TopLeft)
};
let branch_picker = branch_name.and_then(|branch_name| {
settings.show_branch_name.then(|| {
let branch_tooltip_label = branch_name.clone();
let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon {
icon_info
} else {
(IconName::GitBranch, Color::Muted)
};
let branch_tooltip_label = branch_name.clone();
let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon {
icon_info
} else {
(IconName::GitBranch, Color::Muted)
};
let trigger = if is_detached_head {
Button::new("project_branch_trigger", "Create Branch")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.label_size(LabelSize::Small)
.start_icon(
Icon::new(IconName::GitBranchPlus)
.size(IconSize::XSmall)
.color(Color::Muted),
)
} else {
Button::new("project_branch_trigger", branch_name)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.label_size(LabelSize::Small)
.color(Color::Muted)
.start_icon(
Icon::new(branch_icon)
.size(IconSize::XSmall)
.color(branch_icon_color),
)
};
let trigger = if is_detached_head {
Button::new("project_branch_trigger", "Create Branch")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.label_size(LabelSize::Small)
.start_icon(
Icon::new(IconName::GitBranchPlus)
.size(IconSize::XSmall)
.color(Color::Muted),
)
} else {
Button::new("project_branch_trigger", branch_name)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.label_size(LabelSize::Small)
.color(Color::Muted)
.start_icon(
Icon::new(branch_icon)
.size(IconSize::XSmall)
.color(branch_icon_color),
)
};
PopoverMenu::new("branch-menu")
.menu(move |window, cx| {
Some(git_ui::git_picker::popover(
workspace.downgrade(),
effective_repository.clone(),
git_ui::git_picker::GitPickerTab::Branches,
gpui::rems(34.),
window,
cx,
))
})
.trigger_with_tooltip(trigger, move |_window, cx| {
let meta = if is_detached_head {
format!("Detached HEAD: {}", branch_tooltip_label)
} else {
format!("Currently Checked Out: {}", branch_tooltip_label)
};
Tooltip::with_meta(
"Branch & Stash",
Some(&zed_actions::git::Branch),
meta,
cx,
)
})
.anchor(gpui::Anchor::TopLeft)
let git_picker_button = PopoverMenu::new("branch-menu")
.menu(move |window, cx| {
Some(git_ui::git_picker::popover(
workspace.downgrade(),
effective_repository.clone(),
git_ui::git_picker::GitPickerTab::Branches,
gpui::rems(34.),
window,
cx,
))
})
});
.trigger_with_tooltip(trigger, move |_window, cx| {
let meta = if is_detached_head {
format!("Detached HEAD: {}", branch_tooltip_label)
} else {
format!("Currently Checked Out: {}", branch_tooltip_label)
};
Tooltip::with_meta("Branch & Stash", Some(&zed_actions::git::Branch), meta, cx)
})
.anchor(gpui::Anchor::TopLeft);
Some(
h_flex()
.gap_px()
.child(worktree_button)
.when_some(branch_picker, |this, branch_picker| {
this.child(
Label::new("/")
.size(LabelSize::Small)
.color(Color::Muted)
.alpha(0.25),
)
.child(branch_picker)
})
.child(
Label::new("/")
.size(LabelSize::Small)
.color(Color::Muted)
.alpha(0.25),
)
.child(git_picker_button)
.into_any_element(),
)
}

View file

@ -254,14 +254,6 @@ impl RelPath {
#[derive(Debug)]
pub struct StripPrefixError;
impl std::fmt::Display for StripPrefixError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("prefix not found")
}
}
impl std::error::Error for StripPrefixError {}
impl ToOwned for RelPath {
type Owned = RelPathBuf;

File diff suppressed because it is too large Load diff

View file

@ -155,23 +155,6 @@ pub enum Operator {
replaced_char: Option<char>,
},
HelixSurroundDelete,
HelixJump {
behaviour: HelixJumpBehaviour,
first_char: Option<char>,
labels: Vec<HelixJumpLabel>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct HelixJumpLabel {
pub label: [char; 2],
pub range: Range<Anchor>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HelixJumpBehaviour {
Move,
Extend,
}
#[derive(Default, Clone, Debug)]
@ -1102,7 +1085,6 @@ impl Operator {
Operator::HelixMatch => "helix_m",
Operator::HelixNext { .. } => "helix_next",
Operator::HelixPrevious { .. } => "helix_previous",
Operator::HelixJump { .. } => "gw",
Operator::HelixSurroundAdd => "helix_ms",
Operator::HelixSurroundReplace { .. } => "helix_mr",
Operator::HelixSurroundDelete => "helix_md",
@ -1130,7 +1112,6 @@ impl Operator {
Operator::HelixMatch => "m".to_string(),
Operator::HelixNext { .. } => "]".to_string(),
Operator::HelixPrevious { .. } => "[".to_string(),
Operator::HelixJump { .. } => "gw".to_string(),
Operator::HelixSurroundAdd => "ms".to_string(),
Operator::HelixSurroundReplace {
replaced_char: None,
@ -1161,8 +1142,7 @@ impl Operator {
| Operator::ChangeSurrounds {
target: Some(_), ..
}
| Operator::DeleteSurrounds
| Operator::HelixJump { .. } => true,
| Operator::DeleteSurrounds => true,
Operator::Change
| Operator::Delete
| Operator::Yank
@ -1233,8 +1213,7 @@ impl Operator {
| Operator::Register
| Operator::RecordRegister
| Operator::ReplayRegister
| Operator::HelixMatch
| Operator::HelixJump { .. } => false,
| Operator::HelixMatch => false,
}
}
}

Some files were not shown because too many files have changed in this diff Show more