From a17a1c10c87519676a65de7a46106520f13f22f3 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 30 Mar 2026 07:50:20 +0200 Subject: [PATCH 1/7] nix: Fix screen-casting when building Zed with nix (#52690) Release Notes: - Fixed screen-casting when building Zed with Nix. --- nix/build.nix | 8 ++++---- nix/livekit-libwebrtc/package.nix | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/nix/build.nix b/nix/build.nix index 02ed6235e54daa27a9af9b86da79618a21e3cc7e..9270abbe6f747e0ed78400d13561eadd97edd184 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -77,6 +77,7 @@ let builtins.elem firstComp topLevelIncludes; craneLib = crane.overrideToolchain rustToolchain; + gpu-lib = if withGLES then libglvnd else vulkan-loader; commonArgs = let zedCargoLock = builtins.fromTOML (builtins.readFile ../crates/zed/Cargo.toml); @@ -178,8 +179,8 @@ let libva libxkbcommon wayland + gpu-lib libglvnd - vulkan-loader xorg.libX11 xorg.libxcb libdrm @@ -236,8 +237,7 @@ let # about them that's special is that they're manually dlopened at runtime NIX_LDFLAGS = lib.optionalString stdenv'.hostPlatform.isLinux "-rpath ${ lib.makeLibraryPath [ - libglvnd - vulkan-loader + gpu-lib wayland libva ] @@ -246,7 +246,7 @@ let NIX_OUTPATH_USED_AS_RANDOM_SEED = "norebuilds"; }; - # prevent nix from removing the "unused" wayland rpaths + # prevent nix from removing the "unused" wayland/gpu-lib rpaths dontPatchELF = stdenv'.hostPlatform.isLinux; # TODO: try craneLib.cargoNextest separate output diff --git a/nix/livekit-libwebrtc/package.nix b/nix/livekit-libwebrtc/package.nix index dd7b5808ac65ab07d1293683905b694910ee503a..4c0d99926200e619b567cf7a90549f4f882eda42 100644 --- a/nix/livekit-libwebrtc/package.nix +++ b/nix/livekit-libwebrtc/package.nix @@ -81,6 +81,15 @@ stdenv.mkDerivation { pname = "livekit-libwebrtc"; version = "137-unstable-2025-11-24"; + # libwebrtc loads libEGL/libGL at runtime via dlopen() in the Wayland + # screencast path, so they are not visible as ordinary DT_NEEDED edges. + # Keep an explicit rpath so the shared object can resolve them at runtime. + NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isLinux + "-rpath ${lib.makeLibraryPath [ libGL ]}"; + + # Prevent fixup from stripping the rpath above as "unused". + dontPatchELF = stdenv.hostPlatform.isLinux; + gclientDeps = gclient2nix.importGclientDeps ./sources.json; sourceRoot = "src"; From 8cbcadf1962aafbf429f116222e172b2ffec94fa Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 30 Mar 2026 09:17:02 +0200 Subject: [PATCH 2/7] Make paste a separate undo transaction from preceding edits (#52003) When undoing a paste, it is really confusing when that actually also removes what was type right before the paste if the paste happened fast enough after. Release Notes: - Fixed undoing a paste sometimes also undoing edits right before the paste --- crates/editor/src/editor.rs | 2 ++ crates/editor/src/editor_tests.rs | 30 +++++++++++++++++++ crates/language/src/buffer.rs | 8 ++--- crates/multi_buffer/src/multi_buffer.rs | 9 +++++- crates/multi_buffer/src/multi_buffer_tests.rs | 4 +-- crates/text/src/text.rs | 17 ++++++----- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 405924edb227e4c561caafeee2f8cd3e51567023..ed3fb5ccf8fe915718c012d208deb8fb32a4615e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14018,6 +14018,8 @@ impl Editor { return; } + self.finalize_last_transaction(cx); + let clipboard_text = Cow::Borrowed(text.as_str()); self.transact(window, cx, |this, window, cx| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2a0d2fbfe0199126cd8b86c016e5ffbbdbdb9ae3..2ba12a2ff18a4d6a01d52f7c2a61c682942f9a4d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8839,6 +8839,36 @@ async fn test_paste_multiline(cx: &mut TestAppContext) { )ˇ"}); } +#[gpui::test] +async fn test_paste_undo_does_not_include_preceding_edits(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.update_editor(|e, _, cx| { + e.buffer().update(cx, |buffer, cx| { + buffer.set_group_interval(Duration::from_secs(10), cx) + }) + }); + // Type some text + cx.set_state("ˇ"); + cx.update_editor(|e, window, cx| e.insert("hello", window, cx)); + // cx.assert_editor_state("helloˇ"); + + // Paste some text immediately after typing + cx.write_to_clipboard(ClipboardItem::new_string(" world".into())); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); + cx.assert_editor_state("hello worldˇ"); + + // Undo should only undo the paste, not the preceding typing + cx.update_editor(|e, window, cx| e.undo(&Undo, window, cx)); + cx.assert_editor_state("helloˇ"); + + // Undo again should undo the typing + cx.update_editor(|e, window, cx| e.undo(&Undo, window, cx)); + cx.assert_editor_state("ˇ"); +} + #[gpui::test] async fn test_paste_content_from_other_app(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 013025de87ad3957f9ac8d8c58f638baeac1448c..b2ab420312249f809599d06315e706627b76570b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3274,6 +3274,10 @@ impl Buffer { pub fn preserve_preview(&self) -> bool { !self.has_edits_since(&self.preview_version) } + + pub fn set_group_interval(&mut self, group_interval: Duration) { + self.text.set_group_interval(group_interval); + } } #[doc(hidden)] @@ -3289,10 +3293,6 @@ impl Buffer { self.edit(edits, autoindent_mode, cx); } - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.text.set_group_interval(group_interval); - } - pub fn randomly_edit(&mut self, rng: &mut T, old_range_count: usize, cx: &mut Context) where T: rand::Rng, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 7b5f0135f57269b7c787031120f6eb22b0caf549..7e721e8249cfb30366fc8f5198c2348d980aa6bd 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1234,8 +1234,15 @@ impl MultiBuffer { } } - pub fn set_group_interval(&mut self, group_interval: Duration) { + pub fn set_group_interval(&mut self, group_interval: Duration, cx: &mut Context) { self.history.set_group_interval(group_interval); + if self.singleton { + for BufferState { buffer, .. } in self.buffers.values() { + buffer.update(cx, |buffer, _| { + buffer.set_group_interval(group_interval); + }); + } + } } pub fn with_title(mut self, title: String) -> Self { diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 8b708968f21b103ee3c7882c01cd1edf6884af03..e44a38e4abed8438bcdcbf1f2c8c55c465d98e2d 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -3513,8 +3513,8 @@ fn test_history(cx: &mut App) { buf }); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |this, _| { - this.set_group_interval(group_interval); + multibuffer.update(cx, |this, cx| { + this.set_group_interval(group_interval, cx); }); multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index ee095a7f19fd1acf8b1b4a1526fb16b00e3fd43f..b8f2ce6ce9b66040b4e633d28bfb42e1791a38ca 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -223,10 +223,11 @@ impl History { redo_stack: Vec::new(), transaction_depth: 0, // Don't group transactions in tests unless we opt in, because it's a footgun. - #[cfg(any(test, feature = "test-support"))] - group_interval: Duration::ZERO, - #[cfg(not(any(test, feature = "test-support")))] - group_interval: Duration::from_millis(300), + group_interval: if cfg!(any(test, feature = "test-support")) { + Duration::ZERO + } else { + Duration::from_millis(300) + }, } } @@ -1825,6 +1826,10 @@ impl Buffer { tx.try_send(()).ok(); } } + + pub fn set_group_interval(&mut self, group_interval: Duration) { + self.history.group_interval = group_interval; + } } #[cfg(any(test, feature = "test-support"))] @@ -1929,10 +1934,6 @@ impl Buffer { assert!(!self.text().contains("\r\n")); } - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.history.group_interval = group_interval; - } - pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); From ce7512b115504a38f988dc7cc8306acacc1ca3e2 Mon Sep 17 00:00:00 2001 From: Mohammad Razeghi Date: Mon, 30 Mar 2026 09:24:26 +0200 Subject: [PATCH 3/7] Fix terminal rename not working from context menu on inactive tabs (#50031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #49939 Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing Screenshot 2026-02-24 at 11 53 57 PM - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Fix terminal rename not working from context menu on inactive tabs --- crates/terminal_view/src/terminal_view.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3f38ee2f0fd7f64fd996d9011d28ec942d02c86d..f3188f0f3ab4e7288d406fb43b0d3a416a76771f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1350,9 +1350,16 @@ impl Item for TerminalView { None => (IconName::Terminal, Color::Muted, None), }; + let self_handle = self.self_handle.clone(); h_flex() .gap_1() .group("term-tab-icon") + .track_focus(&self.focus_handle) + .on_action(move |action: &RenameTerminal, window, cx| { + self_handle + .update(cx, |this, cx| this.rename_terminal(action, window, cx)) + .ok(); + }) .child( h_flex() .group("term-tab-icon") From 086060558f9c6cdb802deac64f2fbde243986bb6 Mon Sep 17 00:00:00 2001 From: MostlyK <135974627+MostlyKIGuess@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:01:22 +0530 Subject: [PATCH 4/7] gpui: Update animated image timing to pause when inactive (#52685) - only advance frames and request animation frames while the window is active. - Reset last_frame_time to None when inactive to avoid fast catch-up after resuming focus - This fixes an issue where if you would have a gif open and lose focus, the app tries to speedup to catch up the elapsed time 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 is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Might Close #21563, I shall measure the performance, I originally only intended the fix to fix the speed up issue as I faced it while working on gifs. Release Notes: - N/A or Added/Fixed/Improved ... --- crates/gpui/src/elements/img.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index ccf10d038c271ac54a0060b4c17c9de86ce9eb5c..ccd4123048c22fda796ec3ae9d367209d4974c38 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -315,20 +315,24 @@ impl Element for Img { if let Some(state) = &mut state { let frame_count = data.frame_count(); if frame_count > 1 { - let current_time = Instant::now(); - if let Some(last_frame_time) = state.last_frame_time { - let elapsed = current_time - last_frame_time; - let frame_duration = - Duration::from(data.delay(state.frame_index)); - - if elapsed >= frame_duration { - state.frame_index = - (state.frame_index + 1) % frame_count; - state.last_frame_time = - Some(current_time - (elapsed - frame_duration)); + if window.is_window_active() { + let current_time = Instant::now(); + if let Some(last_frame_time) = state.last_frame_time { + let elapsed = current_time - last_frame_time; + let frame_duration = + Duration::from(data.delay(state.frame_index)); + + if elapsed >= frame_duration { + state.frame_index = + (state.frame_index + 1) % frame_count; + state.last_frame_time = + Some(current_time - (elapsed - frame_duration)); + } + } else { + state.last_frame_time = Some(current_time); } } else { - state.last_frame_time = Some(current_time); + state.last_frame_time = None; } } state.started_loading = None; @@ -365,7 +369,10 @@ impl Element for Img { }; } - if global_id.is_some() && data.frame_count() > 1 { + if global_id.is_some() + && data.frame_count() > 1 + && window.is_window_active() + { window.request_animation_frame(); } } From 49ed4dc3fe2b547bf6508a855364ec53ec3fe76d Mon Sep 17 00:00:00 2001 From: Anas Limem <160512789+anaslimem@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:32:42 +0100 Subject: [PATCH 5/7] Hide cursor in embedded terminal when not focused (#52404) ## Context Fixes #52063 This change hides the cursor in embedded terminal mode when not focused. Embedded mode is used for read-only terminal output (like the Agent panel). Showing a cursor in a read-only context when unfocused is confusing, so we suppress it. Screenshot 2026-03-25 at 12 03 15 ## How to Review The change is in a single file: `crates/terminal_view/src/terminal_view.rs:754-761`. Focus on the `should_show_cursor()` method Verify the logic correctly hides the cursor only when both conditions are met (Embedded mode AND not focused). ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments (N/A - no unsafe code) - [x] The content is consistent with the UI/UX checklist - [x] Tests cover the new/changed behavior (behavior is minimal UI fix, existing tests should cover) - [x] Performance impact has been considered and is acceptable (negligible) Release Notes: - Fixed cursor visibility issue in embedded terminal panels --- crates/terminal_view/src/terminal_view.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index f3188f0f3ab4e7288d406fb43b0d3a416a76771f..98b37eee576d3c9a5b21def618133c1b9fe53e37 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -754,7 +754,14 @@ impl TerminalView { } pub fn should_show_cursor(&self, focused: bool, cx: &mut Context) -> bool { - // Always show cursor when not focused or in special modes + // Hide cursor when in embedded mode and not focused (read-only output like Agent panel) + if let TerminalMode::Embedded { .. } = &self.mode { + if !focused { + return false; + } + } + + // For Standalone mode: always show cursor when not focused or in special modes if !focused || self .terminal From 501e72dbe7961cfa598a8f43232427bbe9e5806b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 Mar 2026 00:34:29 -0700 Subject: [PATCH 6/7] Fix crash when LSP returns multiple nested definition ranges (#52701) This fixes a crash when navigating to multiple definitions in a singleton buffer (https://github.com/zed-industries/zed/pull/51461). Release Notes: - N/A --- crates/editor/src/editor.rs | 14 +++++++++ crates/editor/src/editor_tests.rs | 49 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ed3fb5ccf8fe915718c012d208deb8fb32a4615e..6ac1e351886ff3558a5f132ec37790f21b2dea3b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18219,6 +18219,20 @@ impl Editor { for ranges in locations.values_mut() { ranges.sort_by_key(|range| (range.start, Reverse(range.end))); ranges.dedup(); + // Merge overlapping or contained ranges. After sorting by + // (start, Reverse(end)), we can merge in a single pass: + // if the next range starts before the current one ends, + // extend the current range's end if needed. + let mut i = 0; + while i + 1 < ranges.len() { + if ranges[i + 1].start <= ranges[i].end { + let merged_end = ranges[i].end.max(ranges[i + 1].end); + ranges[i].end = merged_end; + ranges.remove(i + 1); + } else { + i += 1; + } + } let fits_in_one_excerpt = ranges .iter() .tuple_windows() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2ba12a2ff18a4d6a01d52f7c2a61c682942f9a4d..eb8b59a511ee580e0bc6f2ba0c7ce9a5ebdccb48 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25403,6 +25403,55 @@ async fn test_goto_definition_far_ranges_open_multibuffer(cx: &mut TestAppContex }); } +#[gpui::test] +async fn test_goto_definition_contained_ranges(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + definition_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + // The LSP returns two single-line definitions on the same row where one + // range contains the other. Both are on the same line so the + // `fits_in_one_excerpt` check won't underflow, and the code reaches + // `change_selections`. + cx.set_state( + &r#"fn caller() { + let _ = ˇtarget(); + } + fn target_outer() { fn target_inner() {} } + "# + .unindent(), + ); + + // Return two definitions on the same line: an outer range covering the + // whole line and an inner range for just the inner function name. + cx.set_request_handler::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Array(vec![ + // Inner range: just "target_inner" (cols 23..35) + lsp::Location { + uri: url.clone(), + range: lsp::Range::new(lsp::Position::new(3, 23), lsp::Position::new(3, 35)), + }, + // Outer range: the whole line (cols 0..48) + lsp::Location { + uri: url, + range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 48)), + }, + ]))) + }); + + let navigated = cx + .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx)) + .await + .expect("Failed to navigate to definitions"); + assert_eq!(navigated, Navigated::Yes); +} + #[gpui::test] async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) { init_test(cx, |_| {}); From 7f7520b98ed1a0ce03dab1d5ddcb87163f1bcdb8 Mon Sep 17 00:00:00 2001 From: Yao Xiao <108576690+Charlie-XIAO@users.noreply.github.com> Date: Mon, 30 Mar 2026 04:19:09 -0400 Subject: [PATCH 7/7] agent_ui: Fix expanded message editor jitters while typing (#52612) 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 is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #52132. *This PR previously also fixes the "expanded message editor not taking up full height" part, but #52545 has already fixed that. Yet it seems to leave a new issue (that was previously not revealed) behind, as follows.* This PR fixes the `Full + ExcludeOverscrollMargin` editor mode (which the agent panel message editor uses in expanded mode), which could jitter while typing because render-time layout and scroll-position updates were clamping against different effective `scroll_beyond_last_line` policies. This PR fixes that inconsistency so the expanded editor stays stable while typing, and adds a regression test covering `ExcludeOverscrollMargin` scroll clamping. https://github.com/user-attachments/assets/86abf04d-c1a1-419b-96d0-8ca097c0acb0 https://github.com/user-attachments/assets/03dbdc3c-f58e-4378-8c6a-4bda1ae425c8 Release Notes: - Fixed the expanded Agent Panel message editor so it no longer jitters while typing. --------- Co-authored-by: MrSubidubi --- crates/editor/src/editor_tests.rs | 57 ++++++++++++++++++++++++++++++- crates/editor/src/element.rs | 32 ++++++----------- crates/editor/src/scroll.rs | 23 ++++++++++--- 3 files changed, 86 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index eb8b59a511ee580e0bc6f2ba0c7ce9a5ebdccb48..a630951735f0f3ddf913503e0c9de0bf0df3fe62 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -49,7 +49,8 @@ use serde_json::{self, json}; use settings::{ AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent, - ProjectSettingsContent, SearchSettingsContent, SettingsContent, SettingsStore, + ProjectSettingsContent, ScrollBeyondLastLine, SearchSettingsContent, SettingsContent, + SettingsStore, }; use std::borrow::Cow; use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; @@ -2784,6 +2785,60 @@ async fn test_autoscroll(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_exclude_overscroll_margin_clamps_scroll_position(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + update_test_editor_settings(cx, &|settings| { + settings.scroll_beyond_last_line = Some(ScrollBeyondLastLine::OnePage); + }); + + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, window, cx| { + editor.set_mode(EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sizing_behavior: SizingBehavior::ExcludeOverscrollMargin, + }); + editor + .style(cx) + .text + .line_height_in_pixels(window.rem_size()) + }); + let window = cx.window; + cx.simulate_window_resize(window, size(px(1000.), 6. * line_height)); + cx.set_state( + &r#" + ˇone + two + three + four + five + six + seven + eight + nine + ten + eleven + "# + .unindent(), + ); + + cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let max_scroll_top = + (snapshot.max_point().row().as_f64() - editor.visible_line_count().unwrap() + 1.) + .max(0.); + + editor.set_scroll_position(gpui::Point::new(0., max_scroll_top + 10.), window, cx); + + assert_eq!( + editor.snapshot(window, cx).scroll_position(), + gpui::Point::new(0., max_scroll_top) + ); + }); +} + #[gpui::test] async fn test_move_page_up_page_down(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 285a1cf6fbb7bbcdde27e2258ee8c936711dcb14..968048f68513a09c460bb06789103923bbbca828 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -9772,26 +9772,14 @@ impl Element for EditorElement { f64::from(visible_bounds.size.height / line_height); // The max scroll position for the top of the window - let max_scroll_top = if matches!( - snapshot.mode, - EditorMode::SingleLine - | EditorMode::AutoHeight { .. } - | EditorMode::Full { - sizing_behavior: SizingBehavior::ExcludeOverscrollMargin - | SizingBehavior::SizeByContent, - .. - } - ) { - (max_row - height_in_lines + 1.).max(0.) - } else { - let settings = EditorSettings::get_global(cx); - match settings.scroll_beyond_last_line { - ScrollBeyondLastLine::OnePage => max_row, - ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.), - ScrollBeyondLastLine::VerticalScrollMargin => { - (max_row - height_in_lines + 1. + settings.vertical_scroll_margin) - .max(0.) - } + let scroll_beyond_last_line = self.editor.read(cx).scroll_beyond_last_line(cx); + let max_scroll_top = match scroll_beyond_last_line { + ScrollBeyondLastLine::OnePage => max_row, + ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.).max(0.), + ScrollBeyondLastLine::VerticalScrollMargin => { + let settings = EditorSettings::get_global(cx); + (max_row - height_in_lines + 1. + settings.vertical_scroll_margin) + .max(0.) } }; @@ -10309,6 +10297,7 @@ impl Element for EditorElement { ), longest_line_blame_width, EditorSettings::get_global(cx), + scroll_beyond_last_line, ); let mut scroll_width = scrollbar_layout_information.scroll_range.width; @@ -11187,8 +11176,9 @@ impl ScrollbarLayoutInformation { document_size: Size, longest_line_blame_width: Pixels, settings: &EditorSettings, + scroll_beyond_last_line: ScrollBeyondLastLine, ) -> Self { - let vertical_overscroll = match settings.scroll_beyond_last_line { + let vertical_overscroll = match scroll_beyond_last_line { ScrollBeyondLastLine::OnePage => editor_bounds.size.height, ScrollBeyondLastLine::Off => glyph_grid_cell.height, ScrollBeyondLastLine::VerticalScrollMargin => { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index b10f7650a051c3ad3c31c1426eb98aeee4f9da07..c2280e90f7d30d53c0818119df70b7c32161b78b 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -5,7 +5,7 @@ pub(crate) mod scroll_amount; use crate::editor_settings::ScrollBeyondLastLine; use crate::{ Anchor, DisplayPoint, DisplayRow, Editor, EditorEvent, EditorMode, EditorSettings, - InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint, + InlayHintRefreshReason, MultiBufferSnapshot, RowExt, SizingBehavior, ToPoint, display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, persistence::EditorDb, @@ -372,6 +372,7 @@ impl ScrollManager { &mut self, scroll_position: gpui::Point, map: &DisplaySnapshot, + scroll_beyond_last_line: ScrollBeyondLastLine, local: bool, autoscroll: bool, workspace_id: Option, @@ -379,7 +380,7 @@ impl ScrollManager { cx: &mut Context, ) -> WasScrolled { let scroll_top = scroll_position.y.max(0.); - let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { + let scroll_top = match scroll_beyond_last_line { ScrollBeyondLastLine::OnePage => scroll_top, ScrollBeyondLastLine::Off => { if let Some(height_in_lines) = self.visible_line_count { @@ -400,7 +401,6 @@ impl ScrollManager { } } }; - let scroll_top_row = DisplayRow(scroll_top as u32); let scroll_top_buffer_point = map .clip_point( @@ -639,6 +639,20 @@ impl Editor { self.scroll_manager.vertical_scroll_margin as usize } + pub(crate) fn scroll_beyond_last_line(&self, cx: &App) -> ScrollBeyondLastLine { + match self.mode { + EditorMode::Minimap { .. } + | EditorMode::Full { + sizing_behavior: SizingBehavior::Default, + .. + } => EditorSettings::get_global(cx).scroll_beyond_last_line, + + EditorMode::Full { .. } | EditorMode::SingleLine | EditorMode::AutoHeight { .. } => { + ScrollBeyondLastLine::Off + } + } + } + pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context) { self.scroll_manager.vertical_scroll_margin = margin_rows as f64; cx.notify(); @@ -776,10 +790,11 @@ impl Editor { } else { scroll_position }; - + let scroll_beyond_last_line = self.scroll_beyond_last_line(cx); self.scroll_manager.set_scroll_position( adjusted_position, &display_map, + scroll_beyond_last_line, local, autoscroll, workspace_id,