Detailed changes
@@ -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| {
@@ -18217,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()
@@ -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, |_| {});
@@ -8839,6 +8894,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, |_| {});
@@ -25373,6 +25458,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::<lsp::request::GotoDefinition, _, _>(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, |_| {});
@@ -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<Pixels>,
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 => {
@@ -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<ScrollOffset>,
map: &DisplaySnapshot,
+ scroll_beyond_last_line: ScrollBeyondLastLine,
local: bool,
autoscroll: bool,
workspace_id: Option<WorkspaceId>,
@@ -379,7 +380,7 @@ impl ScrollManager {
cx: &mut Context<Editor>,
) -> 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>) {
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,
@@ -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();
}
}
@@ -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<T>(&mut self, rng: &mut T, old_range_count: usize, cx: &mut Context<Self>)
where
T: rand::Rng,
@@ -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>) {
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 {
@@ -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(
@@ -754,7 +754,14 @@ impl TerminalView {
}
pub fn should_show_cursor(&self, focused: bool, cx: &mut Context<Self>) -> 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
@@ -1350,9 +1357,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")
@@ -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<usize> {
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);
@@ -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
@@ -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";