diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 405924edb227e4c561caafeee2f8cd3e51567023..6ac1e351886ff3558a5f132ec37790f21b2dea3b 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| { @@ -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() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2a0d2fbfe0199126cd8b86c016e5ffbbdbdb9ae3..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, |_| {}); @@ -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::(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, |_| {}); 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, 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(); } } 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/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3f38ee2f0fd7f64fd996d9011d28ec942d02c86d..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 @@ -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") 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); 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";