Compare commits

...

5 commits

Author SHA1 Message Date
Danilo Leal
48a0cbcfd8
git_ui: Improve the staged & unstaged grouping in the git panel (#60976)
Some checks are pending
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Follow up to https://github.com/zed-industries/zed/pull/59884

This PR refines how the git panel behaves when grouping changes by
staging state, with a focus on making staging workflows predictable and
conflict resolution safe.

- Renamed the "Group By" menu options to describe what you actually see:
**"Tracked & Untracked"** (was "Status") and **"Staged & Unstaged"**
(was "Staging").
- When grouping by staged & unstaged, both sections now stay visible
even when empty, showing a placeholder message ("No staged changes yet"
/ "No unstaged changes"). Previously an empty section disappeared
entirely, which made the panel layout jump around as you staged and
unstaged files, and made it harder to tell at a glance that nothing was
staged yet.
- Section header controls are now consistent checkboxes everywhere
(previously a mix of checkboxes and +/− icon buttons), with "Stage All"
/ "Unstage All" tooltips. Headers of empty sections render no checkbox
and don't react to clicks or hover. I appreciate the debate that
happened in the PR linked above about this but I was personally having a
hard time understanding what was the difference in interaction given the
action was exactly the same, we were just having different UIs, which
looked inconsistent.
- Arrow-key navigation now skips over section headers and empty-section
placeholder rows in both flat and tree view, instead of getting stuck or
selecting non-interactive rows — including when jumping to the first or
last entry.
- In the staged & unstaged grouping, a partially staged file appears in
both sections. Each row's checkbox and tooltip now follow the section
it's rendered in: rows in Staged always unstage, rows in Unstaged always
stage — including via shift-click range operations, which now also
support bulk *unstaging* within the Staged section (previously ranges
could only stage). The context menu's Stage/Unstage label follows the
same rule and stays in sync with in-flight staging operations.
- Conflicted files are grouped by whether the current merge marked them
conflicted (rather than raw status), so a conflict you've resolved stays
visible under "Conflicts" until the merge concludes. On top of that,
resolution is now one-way in the UI:
- Ticking a conflicted file's checkbox marks it resolved (stages it).
Once resolved, the checkbox is disabled with a "Conflict marked as
resolved" tooltip — unticking it would silently discard git's record of
the unmerged base/ours/theirs versions, a round-trip git can't actually
perform.
- The same lock applies everywhere the file can be reached: the keyboard
toggle, folder checkboxes in tree view, the Conflicts section header
(disabled once all conflicts are resolved), "Stage All"/"Unstage All" on
the Staged/Unstaged headers, and shift-click range sweeps — none of them
will resolve or un-resolve a conflict as a side effect.
- The explicit `git: unstage file` action still works as a deliberate
escape hatch.

---

Here's a video, where you can see I stage and unstage whole files, make
a partial staging, and get into a merge-conflict state, where the
conflicted files are tagged as resolved:


https://github.com/user-attachments/assets/d27ef9c6-5691-45c1-91ab-c3b152aaaa60

---

Release Notes:

- N/A _(given the feature hasn't been released yet_)
2026-07-14 18:05:11 +00:00
Yara 🏳️‍⚧️
3e76813825
pickers: Remove picker::ToMultiBuffer action we did not land (#60979)
Missed this when shipping the pickers. The feature is still planned and
tracked in the tracking issue.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
2026-07-14 17:18:23 +00:00
Smit Barmase
ba0eea3f49
settings_content: Fix globally excluded language servers starting (#60984)
Closes https://github.com/zed-industries/zed/issues/60763

Follow-up to #60000. Global language server exclusions were added to
per-language settings without removing an explicit enable for the same
server, allowing it to still start.

For example, this setting should disable `vtsls` for every language:

```json
{
  "language_servers": ["!vtsls", "..."]
}
```

However, TypeScript’s default configuration explicitly enables `vtsls`,
causing it to still start.

Release Notes:

- Fixed language servers starting despite being disabled globally.
2026-07-14 17:15:05 +00:00
Kirill Bulatov
ef2430c8f3
Properly notify on inline diagnostics toggle (#60978)
Closes https://github.com/zed-industries/zed/issues/60959

Release Notes:

- Fixed inline diagnostics not instantly [dis]appearing after toggling
2026-07-14 16:57:03 +00:00
John Tur
72656afa6d
Use more efficient thread pool API on Windows (#60917)
This interface uses fewer syscalls and has less internal synchronization
(since the `TP_WORK` object is not exposed).

Release Notes:

- N/A
2026-07-14 16:40:22 +00:00
6 changed files with 873 additions and 339 deletions

View file

@ -331,13 +331,12 @@ impl Editor {
self.set_max_diagnostics_severity(new_severity, cx);
if self.diagnostics_enabled {
self.active_diagnostics = ActiveDiagnostic::None;
self.refresh_inline_diagnostics(false, window, cx);
} else {
self.inline_diagnostics_update = Task::ready(());
self.inline_diagnostics.clear();
} else {
self.refresh_inline_diagnostics(false, window, cx);
cx.notify();
}
cx.notify();
}
pub(super) fn all_diagnostics_active(&self) -> bool {
@ -464,6 +463,7 @@ impl Editor {
{
self.inline_diagnostics_update = Task::ready(());
self.inline_diagnostics.clear();
cx.notify();
return;
}
@ -596,3 +596,158 @@ impl Editor {
}
}
}
#[cfg(test)]
mod tests {
use crate::{
actions::{ToggleDiagnostics, ToggleInlineDiagnostics},
editor_tests::init_test,
test::editor_test_context::EditorTestContext,
};
use gpui::{TestAppContext, UpdateGlobal};
use indoc::indoc;
use language::DiagnosticSourceKind;
use lsp::LanguageServerId;
use settings::{DelayMs, SettingsStore};
use std::sync::{
Arc,
atomic::{self, AtomicUsize},
};
use util::path;
fn setup_inline_diagnostics(cx: &mut EditorTestContext) {
cx.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
let inline = settings
.diagnostics
.get_or_insert_default()
.inline
.get_or_insert_default();
inline.enabled = Some(true);
inline.update_debounce_ms = Some(DelayMs(0));
});
});
});
cx.set_state(indoc! {"
fn func(abc dˇef: i32) -> u32 {
}
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 12),
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "cannot find value `def`".to_string(),
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
1,
"inline diagnostics should appear after the language server publishes them"
);
});
}
#[gpui::test]
async fn test_toggle_diagnostics_refreshes_inline_diagnostics(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
setup_inline_diagnostics(&mut cx);
cx.update_editor(|editor, window, cx| {
editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
0,
"inline diagnostics should be cleared after disabling diagnostics"
);
});
cx.update_editor(|editor, window, cx| {
editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
1,
"inline diagnostics should reappear after re-enabling diagnostics, without further editor events"
);
});
}
#[gpui::test]
async fn test_toggle_inline_diagnostics_notifies_on_hide(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
setup_inline_diagnostics(&mut cx);
let notify_count = Arc::new(AtomicUsize::new(0));
let editor = cx.editor.clone();
let _subscription = cx.update({
let notify_count = notify_count.clone();
move |_, cx| {
cx.observe(&editor, move |_, _| {
notify_count.fetch_add(1, atomic::Ordering::SeqCst);
})
}
});
cx.update_editor(|editor, window, cx| {
editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
});
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
0,
"inline diagnostics should be cleared after toggling them off"
);
});
assert_eq!(
notify_count.load(atomic::Ordering::SeqCst),
1,
"toggling inline diagnostics off should notify to repaint the editor"
);
cx.update_editor(|editor, window, cx| {
editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
1,
"inline diagnostics should reappear after toggling them back on"
);
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,10 @@ use windows::Win32::{
Foundation::{FILETIME, LPARAM, WPARAM},
Media::{timeBeginPeriod, timeEndPeriod},
System::Threading::{
CloseThreadpoolTimer, CloseThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWork,
GetCurrentThread, PTP_CALLBACK_INSTANCE, PTP_TIMER, PTP_WORK, SetThreadPriority,
SetThreadpoolTimer, SubmitThreadpoolWork, THREAD_PRIORITY_TIME_CRITICAL,
CloseThreadpoolTimer, CreateThreadpoolTimer, GetCurrentThread, PTP_CALLBACK_INSTANCE,
PTP_TIMER, SetThreadPriority, SetThreadpoolTimer, THREAD_PRIORITY_TIME_CRITICAL,
TP_CALLBACK_ENVIRON_V3, TP_CALLBACK_PRIORITY, TP_CALLBACK_PRIORITY_HIGH,
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL,
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL, TrySubmitThreadpoolCallback,
},
UI::WindowsAndMessaging::PostMessageW,
};
@ -66,11 +65,8 @@ impl WindowsDispatcher {
let context = runnable.into_raw().as_ptr() as *mut c_void;
unsafe {
if let Ok(work) =
CreateThreadpoolWork(Some(run_work_callback), Some(context), Some(&environ))
{
SubmitThreadpoolWork(work);
}
TrySubmitThreadpoolCallback(Some(run_work_callback), Some(context), Some(&environ))
.log_err();
}
}
@ -179,11 +175,9 @@ impl PlatformDispatcher for WindowsDispatcher {
unsafe extern "system" fn run_work_callback(
_instance: PTP_CALLBACK_INSTANCE,
context: *mut c_void,
work: PTP_WORK,
) {
let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) };
WindowsDispatcher::execute_runnable(runnable);
unsafe { CloseThreadpoolWork(work) };
}
unsafe extern "system" fn run_timer_callback(

View file

@ -76,8 +76,6 @@ actions!(
SetPreviewHidden,
/// Opens the footer's actions menu.
ToggleActionsMenu,
/// Take the picker's content and open it in a multibuffer
ToMultiBuffer,
/// Toggles multi-select mode, in which clicking items adds them to
/// the selection instead of opening them
ToggleMultiSelect,

View file

@ -3,13 +3,11 @@
use std::sync::Arc;
use gpui::{
Action, AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText,
Task, Window, px,
AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText, Task,
Window, px,
};
use language::{Bias, Buffer, HighlightedText, HighlightedTextBuilder, ToPoint};
use picker::{
MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate, ToMultiBuffer,
};
use picker::{MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate};
use project::{Project, Symbol};
use rope::Point;
use settings::Settings;
@ -335,7 +333,7 @@ impl EditorPreview {
div()
.flex_1()
.overflow_hidden()
.child(self.editor_as_giant_button())
.child(self.occluded_editor())
.into_any_element()
}
}
@ -357,7 +355,7 @@ impl EditorPreview {
.child(content)
}
fn editor_as_giant_button(&self) -> impl IntoElement {
fn occluded_editor(&self) -> impl IntoElement {
div()
.relative()
.size_full()
@ -367,10 +365,7 @@ impl EditorPreview {
.id("picker-preview-editor")
.absolute()
.inset_0()
.occlude()
.on_click(|_, window, cx| {
window.dispatch_action(ToMultiBuffer.boxed_clone(), cx);
}),
.occlude(),
)
}
}

View file

@ -67,6 +67,12 @@ impl merge_from::MergeFrom for AllLanguageSettingsContent {
language_settings.merge_from(&other.defaults);
if let Some(mut language_server_overrides) = language_server_overrides {
if let Some(disabled) = &globally_disabled_servers {
for disabled_server in disabled {
if let Some(enabled_server) = disabled_server.strip_prefix('!') {
language_server_overrides.retain(|entry| entry != enabled_server);
}
}
let insert_before = language_server_overrides
.iter()
.position(|entry| entry == REST_OF_LANGUAGE_SERVERS)
@ -1327,6 +1333,55 @@ mod test {
);
}
#[test]
fn test_global_language_server_disable_overrides_per_language_enable() {
let mut base = AllLanguageSettingsContent {
defaults: LanguageSettingsContent {
language_servers: Some(vec![REST_OF_LANGUAGE_SERVERS.into()]),
..LanguageSettingsContent::default()
},
languages: LanguageToSettingsMap(
[(
"TypeScript".into(),
LanguageSettingsContent {
language_servers: Some(vec![
"!typescript-language-server".into(),
"vtsls".into(),
REST_OF_LANGUAGE_SERVERS.into(),
]),
..LanguageSettingsContent::default()
},
)]
.into_iter()
.collect(),
),
..AllLanguageSettingsContent::default()
};
let user = AllLanguageSettingsContent {
defaults: LanguageSettingsContent {
language_servers: Some(vec!["!vtsls".into(), REST_OF_LANGUAGE_SERVERS.into()]),
..LanguageSettingsContent::default()
},
..AllLanguageSettingsContent::default()
};
base.merge_from(&user);
let expected = vec![
"!typescript-language-server".to_string(),
"!vtsls".to_string(),
REST_OF_LANGUAGE_SERVERS.to_string(),
];
assert_eq!(
base.languages
.0
.get("TypeScript")
.and_then(|settings| settings.language_servers.as_deref()),
Some(expected.as_slice()),
);
}
#[test]
fn test_language_servers_merge_no_per_language_config_uses_global() {
let mut base = AllLanguageSettingsContent {