`, 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
,
- cx: &mut Context,
-) -> 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
- })
- })
-}
diff --git a/crates/editor/src/code_lens.rs b/crates/editor/src/code_lens.rs
index 87d2426878e..c1bf2525d9e 100644
--- a/crates/editor/src/code_lens.rs
+++ b/crates/editor/src/code_lens.rs
@@ -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,
@@ -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,
window: &mut Window,
cx: &mut Context,
) -> 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::()
+ })
+ .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 {
+ 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(),
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 9102fc428f6..b01d1592abb 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -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,
diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs
index 025cd4251ad..6dfbf334024 100644
--- a/crates/editor/src/editor_tests.rs
+++ b/crates/editor/src/editor_tests.rs
@@ -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::>()
- .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 = 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::>()
- })
- };
-
- 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,
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 1cefd2179b7..0646bf7a068 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -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::::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)
};
diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs
index 125f09c9661..1752aefc5e6 100644
--- a/crates/editor/src/items.rs
+++ b/crates/editor/src/items.rs
@@ -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],
@@ -1813,48 +1809,45 @@ impl SearchableItem for Editor {
_: &mut Window,
cx: &mut Context,
) -> 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(¤t_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(¤t_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(
diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs
index 347383d0171..43479f1713c 100644
--- a/crates/editor/src/split.rs
+++ b/crates/editor/src/split.rs
@@ -433,17 +433,6 @@ impl SplittableEditor {
self.lhs.as_ref().map(|s| &s.editor)
}
- pub fn update_editors(
- &self,
- cx: &mut Context,
- f: impl Fn(&mut Editor, &mut Context),
- ) {
- 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.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) {
- 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)
diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml
index f7f09956c15..67ebab62295 100644
--- a/crates/file_finder/Cargo.toml
+++ b/crates/file_finder/Cargo.toml
@@ -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
diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs
index d75481f6f74..9a9cc983fa7 100644
--- a/crates/file_finder/src/file_finder.rs
+++ b/crates/file_finder/src/file_finder.rs
@@ -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,
- window: &mut Window,
- cx: &mut Context>,
- ) -> 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>) -> Option {
let focus_handle = self.focus_handle.clone();
diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml
index eefe2717f22..e7b8dcd4ebd 100644
--- a/crates/fs/Cargo.toml
+++ b/crates/fs/Cargo.toml
@@ -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
diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs
index 6b7e4816a18..6694b19e373 100644
--- a/crates/fs/src/fs.rs
+++ b/crates/fs/src/fs.rs
@@ -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: i64 = 0x01021997; // Plan 9 / WSL2 interop (drvfs)
- const NFS_SUPER_MAGIC: i64 = 0x6969;
- const CIFS_MAGIC: i64 = 0xFF534D42u32 as i64; // CIFS/SMB
- const SMB_SUPER_MAGIC: i64 = 0x517B;
- const SMB2_MAGIC: i64 = 0xFE534D42u32 as i64;
- const FUSE_SUPER_MAGIC: i64 = 0x65735546; // FUSE (includes sshfs)
-
- let fs_type = stat.f_type;
- 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// 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>> = 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 = 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()
);
}
diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs
index 909424558b7..02a6b087811 100644
--- a/crates/fs/src/fs_watcher.rs
+++ b/crates/fs/src/fs_watcher.rs
@@ -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>>,
registrations: Mutex, WatcherRegistrationId>>,
- mode: WatcherMode,
}
impl FsWatcher {
pub fn new(
tx: smol::channel::Sender<()>,
pending_path_events: Arc>>,
- 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::((
@@ -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 = 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<¬ify::Event, ¬ify::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: ¬ify::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::>();
+
+ 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>>,
- mut path_events: Vec,
-) {
- 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>>,
- root_path: &SanitizedPath,
- watched_root: &Path,
- event: ¬ify::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::>();
-
- 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>>,
- watched_root: &Path,
- error: ¬ify::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, path_events: &mut Vec) {
if !path_events
.iter()
@@ -247,34 +215,29 @@ fn is_covered_rescan(kind: Option, path: &Path, ancestor: &Path)
pub struct WatcherRegistrationId(u32);
struct WatcherRegistrationState {
- callback: Arc Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync>,
+ callback: Arc,
path: Arc,
- mode: WatcherMode,
}
struct WatcherState {
watchers: HashMap,
- native_path_registrations: HashMap, u32>,
- poll_path_registrations: HashMap, u32>,
+ path_registrations: HashMap, u32>,
last_registration: WatcherRegistrationId,
}
-impl WatcherState {
- fn path_registrations(&mut self, mode: WatcherMode) -> &mut HashMap, u32> {
- match mode {
- WatcherMode::Native => &mut self.native_path_registrations,
- WatcherMode::Poll => &mut self.poll_path_registrations,
- }
- }
-}
-
pub struct GlobalWatcher {
state: Mutex,
- // DANGER: never keep state lock while holding watcher lock
- // two mutexes because calling watcher.add triggers watcher.event, which needs watchers.
- native_watcher: Mutex