editor: Extract git and input out of editor.rs (#56155)

cc @SomeoneToIgnore

## Summary

Follow-up to https://github.com/zed-industries/zed/discussions/55352,
where the conclusion was to split `editor.rs` incrementally by topic
instead of all at once.

This mechanically extracts two editor topics into focused sibling
modules:

- `crates/editor/src/input.rs`
- `crates/editor/src/git.rs`

The git extraction is intentionally partial for now. I left a lot of
related parts because otherwise the diff was super huge (over 9K lines)
in the Github, so we can move those parts later in the follow-up PRs
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

Release Notes:

- N/A
This commit is contained in:
Mikhail Pertsev 2026-05-08 14:01:43 +02:00 committed by GitHub
parent e78ddcac8d
commit 6aca6364fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 2936 additions and 2912 deletions

File diff suppressed because it is too large Load diff

View file

@ -1 +1,710 @@
pub mod blame;
use super::*;
impl Editor {
pub fn diff_hunks_in_ranges<'a>(
&'a self,
ranges: &'a [Range<Anchor>],
buffer: &'a MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> {
ranges.iter().flat_map(move |range| {
let end_excerpt = buffer.excerpt_containing(range.end..range.end);
let range = range.to_point(buffer);
let mut peek_end = range.end;
if range.end.row < buffer.max_row().0 {
peek_end = Point::new(range.end.row + 1, 0);
}
buffer
.diff_hunks_in_range(range.start..peek_end)
.filter(move |hunk| {
if let Some((_, excerpt_range)) = &end_excerpt
&& let Some(end_anchor) =
buffer.anchor_in_excerpt(excerpt_range.context.end)
&& let Some(hunk_end_anchor) =
buffer.anchor_in_excerpt(hunk.excerpt_range.context.end)
&& hunk_end_anchor.cmp(&end_anchor, buffer).is_gt()
{
false
} else {
true
}
})
})
}
pub fn set_render_diff_hunk_controls(
&mut self,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
cx: &mut Context<Self>,
) {
self.render_diff_hunk_controls = render_diff_hunk_controls;
cx.notify();
}
pub fn working_directory(&self, cx: &App) -> Option<PathBuf> {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local())
&& let Some(dir) = file.abs_path(cx).parent()
{
return Some(dir.to_owned());
}
}
None
}
pub fn target_file_abs_path(&self, cx: &mut Context<Self>) -> Option<PathBuf> {
self.active_buffer(cx).and_then(|buffer| {
let buffer = buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let project = self.project()?.read(cx);
project.absolute_path(&project_path, cx)
} else {
buffer
.file()
.and_then(|file| file.as_local().map(|file| file.abs_path(cx)))
}
})
}
/// Returns the project path for the editor's buffer, if any buffer is
/// opened in the editor.
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
buffer.read(cx).project_path(cx)
} else {
None
}
}
pub fn git_blame_inline_enabled(&self) -> bool {
self.git_blame_inline_enabled
}
pub fn selection_menu_enabled(&self, cx: &App) -> bool {
self.show_selection_menu
.unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu)
}
pub fn toggle_selection_menu(
&mut self,
_: &ToggleSelectionMenu,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.show_selection_menu = self
.show_selection_menu
.map(|show_selections_menu| !show_selections_menu)
.or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu));
cx.notify();
}
pub fn blame(&self) -> Option<&Entity<GitBlame>> {
self.blame.as_ref()
}
pub fn show_git_blame_gutter(&self) -> bool {
self.show_git_blame_gutter
}
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
let ranges: Vec<_> = self
.selections
.disjoint_anchors()
.iter()
.map(|s| s.range())
.collect();
self.buffer
.update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx))
}
pub fn copy_file_name_without_extension(
&mut self,
_: &CopyFileNameWithoutExtension,
_: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(file_stem) = self.active_buffer(cx).and_then(|buffer| {
let file = buffer.read(cx).file()?;
file.path().file_stem()
}) {
cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
}
}
pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
if let Some(file_name) = self.active_buffer(cx).and_then(|buffer| {
let file = buffer.read(cx).file()?;
Some(file.file_name(cx))
}) {
cx.write_to_clipboard(ClipboardItem::new_string(file_name.to_string()));
}
}
pub fn toggle_git_blame(
&mut self,
_: &::git::Blame,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.show_git_blame_gutter = !self.show_git_blame_gutter;
if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
self.start_git_blame(true, window, cx);
}
cx.notify();
}
pub fn toggle_git_blame_inline(
&mut self,
_: &ToggleGitBlameInline,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.toggle_git_blame_inline_internal(true, window, cx);
cx.notify();
}
pub(super) fn toggle_staged_selected_diff_hunks(
&mut self,
_: &::git::ToggleStaged,
_: &mut Window,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let ranges: Vec<_> = self
.selections
.disjoint_anchors()
.iter()
.map(|s| s.range())
.collect();
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
}
pub(super) fn stage_and_next(
&mut self,
_: &::git::StageAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.do_stage_or_unstage_and_next(true, window, cx);
}
pub(super) fn unstage_and_next(
&mut self,
_: &::git::UnstageAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.do_stage_or_unstage_and_next(false, window, cx);
}
pub(super) fn stage_or_unstage_diff_hunks(
&mut self,
stage: bool,
ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) {
if self.delegate_stage_and_restore {
let snapshot = self.buffer.read(cx).snapshot(cx);
let hunks: Vec<_> = self.diff_hunks_in_ranges(&ranges, &snapshot).collect();
if !hunks.is_empty() {
cx.emit(EditorEvent::StageOrUnstageRequested { stage, hunks });
}
return;
}
let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
cx.spawn(async move |this, cx| {
task.await?;
this.update(cx, |this, cx| {
let snapshot = this.buffer.read(cx).snapshot(cx);
let chunk_by = this
.diff_hunks_in_ranges(&ranges, &snapshot)
.chunk_by(|hunk| hunk.buffer_id);
for (buffer_id, hunks) in &chunk_by {
this.do_stage_or_unstage(stage, buffer_id, hunks, cx);
}
})
})
.detach_and_log_err(cx);
}
pub(super) fn do_stage_or_unstage(
&self,
stage: bool,
buffer_id: BufferId,
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
cx: &mut App,
) -> Option<()> {
let project = self.project()?;
let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
let diff = self.buffer.read(cx).diff_for(buffer_id)?;
let buffer_snapshot = buffer.read(cx).snapshot();
let file_exists = buffer_snapshot
.file()
.is_some_and(|file| file.disk_state().exists());
diff.update(cx, |diff, cx| {
diff.stage_or_unstage_hunks(
stage,
&hunks
.map(|hunk| buffer_diff::DiffHunk {
buffer_range: hunk.buffer_range,
// We don't need to pass in word diffs here because they're only used for rendering and
// this function changes internal state
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
secondary_status: hunk.status.secondary,
range: Point::zero()..Point::zero(), // unused
})
.collect::<Vec<_>>(),
&buffer_snapshot,
file_exists,
cx,
)
});
None
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context<Self>) -> bool {
self.buffer.update(cx, |buffer, cx| {
let ranges = vec![Anchor::Min..Anchor::Max];
if !buffer.all_diff_hunks_expanded()
&& buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx)
{
buffer.collapse_diff_hunks(ranges, cx);
true
} else {
false
}
})
}
pub(super) fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool {
if self.buffer.read(cx).all_diff_hunks_expanded() {
return true;
}
let ranges = vec![Anchor::Min..Anchor::Max];
self.buffer
.read(cx)
.has_expanded_diff_hunks_in_ranges(&ranges, cx)
}
pub(super) fn toggle_diff_hunks_in_ranges(
&mut self,
ranges: Vec<Range<Anchor>>,
cx: &mut Context<Editor>,
) {
self.buffer.update(cx, |buffer, cx| {
let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx);
buffer.expand_or_collapse_diff_hunks(ranges, expand, cx);
})
}
pub(super) fn toggle_single_diff_hunk(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
self.buffer.update(cx, |buffer, cx| {
buffer.toggle_single_diff_hunk(range, cx);
})
}
pub(super) fn apply_all_diff_hunks(
&mut self,
_: &ApplyAllDiffHunks,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.read_only(cx) {
return;
}
let buffers = self.buffer.read(cx).all_buffers();
for branch_buffer in buffers {
branch_buffer.update(cx, |branch_buffer, cx| {
branch_buffer.merge_into_base(Vec::new(), cx);
});
}
if let Some(project) = self.project.clone() {
self.save(
SaveOptions {
format: true,
force_format: false,
autosave: false,
},
project,
window,
cx,
)
.detach_and_log_err(cx);
}
}
pub(super) fn apply_selected_diff_hunks(
&mut self,
_: &ApplyDiffHunk,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.read_only(cx) {
return;
}
let snapshot = self.snapshot(window, cx);
let hunks = snapshot.hunks_for_ranges(
self.selections
.all(&snapshot.display_snapshot)
.into_iter()
.map(|selection| selection.range()),
);
let mut ranges_by_buffer = HashMap::default();
self.transact(window, cx, |editor, _window, cx| {
for hunk in hunks {
if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
ranges_by_buffer
.entry(buffer.clone())
.or_insert_with(Vec::new)
.push(hunk.buffer_range.to_offset(buffer.read(cx)));
}
}
for (buffer, ranges) in ranges_by_buffer {
buffer.update(cx, |buffer, cx| {
buffer.merge_into_base(ranges, cx);
});
}
});
if let Some(project) = self.project.clone() {
self.save(
SaveOptions {
format: true,
force_format: false,
autosave: false,
},
project,
window,
cx,
)
.detach_and_log_err(cx);
}
}
pub(super) fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> {
self.active_buffer(cx)?
.read(cx)
.file()
.and_then(|f| f.as_local())
}
pub(super) fn reveal_in_finder(
&mut self,
_: &RevealInFileManager,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(path) = self.target_file_abs_path(cx) {
if let Some(project) = self.project() {
project.update(cx, |project, cx| project.reveal_path(&path, cx));
} else {
cx.reveal_path(&path);
}
}
}
pub(super) fn copy_path(
&mut self,
_: &zed_actions::workspace::CopyPath,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(path) = self.target_file_abs_path(cx)
&& let Some(path) = path.to_str()
{
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
} else {
cx.propagate();
}
}
pub(super) fn copy_relative_path(
&mut self,
_: &zed_actions::workspace::CopyRelativePath,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(path) = self.active_buffer(cx).and_then(|buffer| {
let project = self.project()?.read(cx);
let path = buffer.read(cx).file()?.path();
let path = path.display(project.path_style(cx));
Some(path)
}) {
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
} else {
cx.propagate();
}
}
pub(super) fn go_to_active_debug_line(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
maybe!({
let breakpoint_store = self.breakpoint_store.as_ref()?;
let (active_stack_frame, debug_line_pane_id) = {
let store = breakpoint_store.read(cx);
let active_stack_frame = store.active_position().cloned();
let debug_line_pane_id = store.active_debug_line_pane_id();
(active_stack_frame, debug_line_pane_id)
};
let Some(active_stack_frame) = active_stack_frame else {
self.clear_row_highlights::<ActiveDebugLine>();
return None;
};
if let Some(debug_line_pane_id) = debug_line_pane_id {
if let Some(workspace) = self
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade())
{
let editor_pane_id = workspace
.read(cx)
.pane_for_item_id(cx.entity_id())
.map(|pane| pane.entity_id());
if editor_pane_id.is_some_and(|id| id != debug_line_pane_id) {
self.clear_row_highlights::<ActiveDebugLine>();
return None;
}
}
}
let position = active_stack_frame.position;
let snapshot = self.buffer.read(cx).snapshot(cx);
let multibuffer_anchor = snapshot.anchor_in_excerpt(position)?;
self.clear_row_highlights::<ActiveDebugLine>();
self.go_to_line::<ActiveDebugLine>(
multibuffer_anchor,
Some(cx.theme().colors().editor_debugger_active_line_background),
window,
cx,
);
cx.notify();
Some(())
})
.is_some()
}
pub(super) fn open_git_blame_commit(
&mut self,
_: &OpenGitBlameCommit,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_git_blame_commit_internal(window, cx);
}
pub(super) fn start_git_blame(
&mut self,
user_triggered: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(project) = self.project() {
if let Some(buffer) = self.buffer().read(cx).as_singleton()
&& buffer.read(cx).file().is_none()
{
return;
}
let focused = self.focus_handle(cx).contains_focused(window, cx);
let project = project.clone();
let blame = cx
.new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx));
self.blame_subscription =
Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify()));
self.blame = Some(blame);
}
}
pub(super) fn toggle_git_blame_inline_internal(
&mut self,
user_triggered: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.git_blame_inline_enabled {
self.git_blame_inline_enabled = false;
self.show_git_blame_inline = false;
self.show_git_blame_inline_delay_task.take();
} else {
self.git_blame_inline_enabled = true;
self.start_git_blame_inline(user_triggered, window, cx);
}
cx.notify();
}
pub(super) fn start_git_blame_inline(
&mut self,
user_triggered: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.start_git_blame(user_triggered, window, cx);
if ProjectSettings::get_global(cx)
.git
.inline_blame_delay()
.is_some()
{
self.start_inline_blame_timer(window, cx);
} else {
self.show_git_blame_inline = true
}
}
pub(super) fn render_git_blame_gutter(&self, cx: &App) -> bool {
!self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx)
}
pub(super) fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool {
self.show_git_blame_inline
&& (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some())
&& !self.newest_selection_head_on_empty_line(cx)
&& self.has_blame_entries(cx)
}
fn has_stageable_diff_hunks_in_ranges(
&self,
ranges: &[Range<Anchor>],
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot);
hunks.any(|hunk| hunk.status().has_secondary_hunk())
}
fn save_buffers_for_ranges_if_needed(
&mut self,
ranges: &[Range<Anchor>],
cx: &mut Context<Editor>,
) -> Task<Result<()>> {
let multibuffer = self.buffer.read(cx);
let snapshot = multibuffer.read(cx);
let buffer_ids: HashSet<_> = ranges
.iter()
.flat_map(|range| snapshot.buffer_ids_for_range(range.clone()))
.collect();
drop(snapshot);
let mut buffers = HashSet::default();
for buffer_id in buffer_ids {
if let Some(buffer_entity) = multibuffer.buffer(buffer_id) {
let buffer = buffer_entity.read(cx);
if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty()
{
buffers.insert(buffer_entity);
}
}
}
if let Some(project) = &self.project {
project.update(cx, |project, cx| project.save_buffers(buffers, cx))
} else {
Task::ready(Ok(()))
}
}
fn do_stage_or_unstage_and_next(
&mut self,
stage: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
if ranges.iter().any(|range| range.start != range.end) {
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
return;
}
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
let wrap_around = !all_diff_hunks_expanded;
let snapshot = self.snapshot(window, cx);
let position = self
.selections
.newest::<Point>(&snapshot.display_snapshot)
.head();
self.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
wrap_around,
window,
cx,
);
}
fn open_git_blame_commit_internal(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
let blame = self.blame.as_ref()?;
let snapshot = self.snapshot(window, cx);
let cursor = self
.selections
.newest::<Point>(&snapshot.display_snapshot)
.head();
let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?;
let (_, blame_entry) = blame
.update(cx, |blame, cx| {
blame
.blame_for_rows(
&[RowInfo {
buffer_id: Some(buffer.remote_id()),
buffer_row: Some(point.row),
..Default::default()
}],
cx,
)
.next()
})
.flatten()?;
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
let repo = blame.read(cx).repository(cx, buffer.remote_id())?;
let workspace = self.workspace()?.downgrade();
renderer.open_blame_commit(blame_entry, repo, workspace, window, cx);
None
}
fn has_blame_entries(&self, cx: &App) -> bool {
self.blame()
.is_some_and(|blame| blame.read(cx).has_generated_entries())
}
fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool {
let cursor_anchor = self.selections.newest_anchor().head();
let snapshot = self.buffer.read(cx).snapshot(cx);
let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row);
snapshot.line_len(buffer_row) == 0
}
}

2221
crates/editor/src/input.rs Normal file

File diff suppressed because it is too large Load diff