A set of outline panel fixes (#12965)

Kirill Bulatov created

Follow-up of https://github.com/zed-industries/zed/pull/12637

* Wrong font size for the outline items (fixes
https://github.com/zed-industries/zed/pull/12637#issuecomment-2164084021)
* Duplicate context menu item (fixes
https://github.com/zed-industries/zed/issues/12957)
* Missing `space` keybinding for item opening (fixes
https://github.com/zed-industries/zed/issues/12956)
* Adds 60px more to the default width (fixes
https://github.com/zed-industries/zed/issues/12955)
* Incorrect scroll for singleton buffers (fixes
https://github.com/zed-industries/zed/issues/12953)

Release Notes:

- N/A

Change summary

assets/keymaps/default-linux.json         |  11 
assets/keymaps/default-macos.json         |   1 
assets/settings/default.json              |  11 
crates/outline_panel/src/outline_panel.rs | 296 ++++++++++++------------
4 files changed, 153 insertions(+), 166 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -566,11 +566,12 @@
   {
     "context": "OutlinePanel",
     "bindings": {
-      "left": "project_panel::CollapseSelectedEntry",
-      "right": "project_panel::ExpandSelectedEntry",
-      "ctrl-alt-c": "project_panel::CopyPath",
-      "alt-ctrl-shift-c": "project_panel::CopyRelativePath",
-      "alt-ctrl-r": "project_panel::RevealInFinder",
+      "left": "outline_panel::CollapseSelectedEntry",
+      "right": "outline_panel::ExpandSelectedEntry",
+      "ctrl-alt-c": "outline_panel::CopyPath",
+      "alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
+      "alt-ctrl-r": "outline_panel::RevealInFinder",
+      "space": "outline_panel::Open",
       "shift-down": "menu::SelectNext",
       "shift-up": "menu::SelectPrev"
     }

assets/keymaps/default-macos.json 🔗

@@ -593,6 +593,7 @@
       "cmd-alt-c": "outline_panel::CopyPath",
       "alt-cmd-shift-c": "outline_panel::CopyRelativePath",
       "alt-cmd-r": "outline_panel::RevealInFinder",
+      "space": "outline_panel::Open",
       "shift-down": "menu::SelectNext",
       "shift-up": "menu::SelectPrev"
     }

assets/settings/default.json 🔗

@@ -131,14 +131,7 @@
   // The default number of lines to expand excerpts in the multibuffer by.
   "expand_excerpt_lines": 3,
   // Globs to match against file paths to determine if a file is private.
-  "private_files": [
-    "**/.env*",
-    "**/*.pem",
-    "**/*.key",
-    "**/*.cert",
-    "**/*.crt",
-    "**/secrets.yml"
-  ],
+  "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
   // Whether to use additional LSP queries to format (and amend) the code after
   // every "trigger" symbol input, defined by LSP server capabilities.
   "use_on_type_format": true,
@@ -306,7 +299,7 @@
     // Whether to show the outline panel button in the status bar
     "button": true,
     // Default width of the outline panel.
-    "default_width": 240,
+    "default_width": 300,
     // Where to dock the outline panel. Can be 'left' or 'right'.
     "dock": "left",
     // Whether to show file icons in the outline panel.

crates/outline_panel/src/outline_panel.rs 🔗

@@ -41,7 +41,7 @@ use workspace::{
     item::ItemHandle,
     ui::{
         h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, Icon, IconName, IconSize,
-        Label, LabelCommon, ListItem, Selectable,
+        Label, LabelCommon, ListItem, Selectable, StyledTypography,
     },
     OpenInTerminal, Workspace,
 };
@@ -487,6 +487,146 @@ impl OutlinePanel {
         self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
     }
 
+    fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
+        if let Some(selected_entry) = self.selected_entry.clone() {
+            self.open_entry(&selected_entry, cx);
+        }
+    }
+
+    fn open_entry(&mut self, entry: &EntryOwned, cx: &mut ViewContext<OutlinePanel>) {
+        let Some(active_editor) = self
+            .active_item
+            .as_ref()
+            .and_then(|item| item.active_editor.upgrade())
+        else {
+            return;
+        };
+        let active_multi_buffer = active_editor.read(cx).buffer().clone();
+        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
+        let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
+            Point::default()
+        } else {
+            Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
+        };
+
+        match &entry {
+            EntryOwned::Entry(FsEntry::ExternalFile(buffer_id)) => {
+                let scroll_target = multi_buffer_snapshot.excerpts().find_map(
+                    |(excerpt_id, buffer_snapshot, excerpt_range)| {
+                        if &buffer_snapshot.remote_id() == buffer_id {
+                            multi_buffer_snapshot
+                                .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
+                        } else {
+                            None
+                        }
+                    },
+                );
+                if let Some(anchor) = scroll_target {
+                    self.selected_entry = Some(entry.clone());
+                    active_editor.update(cx, |editor, cx| {
+                        editor.set_scroll_anchor(
+                            ScrollAnchor {
+                                offset: offset_from_top,
+                                anchor,
+                            },
+                            cx,
+                        );
+                    })
+                }
+            }
+            entry @ EntryOwned::Entry(FsEntry::Directory(..)) => {
+                self.toggle_expanded(entry, cx);
+            }
+            entry @ EntryOwned::FoldedDirs(..) => {
+                self.toggle_expanded(entry, cx);
+            }
+            EntryOwned::Entry(FsEntry::File(_, file_entry)) => {
+                let scroll_target = self
+                    .project
+                    .update(cx, |project, cx| {
+                        project
+                            .path_for_entry(file_entry.id, cx)
+                            .and_then(|path| project.get_open_buffer(&path, cx))
+                    })
+                    .map(|buffer| {
+                        active_multi_buffer
+                            .read(cx)
+                            .excerpts_for_buffer(&buffer, cx)
+                    })
+                    .and_then(|excerpts| {
+                        let (excerpt_id, excerpt_range) = excerpts.first()?;
+                        multi_buffer_snapshot
+                            .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
+                    });
+                if let Some(anchor) = scroll_target {
+                    self.selected_entry = Some(entry.clone());
+                    active_editor.update(cx, |editor, cx| {
+                        editor.set_scroll_anchor(
+                            ScrollAnchor {
+                                offset: offset_from_top,
+                                anchor,
+                            },
+                            cx,
+                        );
+                    })
+                }
+            }
+            EntryOwned::Outline(_, outline) => {
+                let Some(full_buffer_snapshot) =
+                    outline
+                        .range
+                        .start
+                        .buffer_id
+                        .and_then(|buffer_id| active_multi_buffer.read(cx).buffer(buffer_id))
+                        .or_else(|| {
+                            outline.range.end.buffer_id.and_then(|buffer_id| {
+                                active_multi_buffer.read(cx).buffer(buffer_id)
+                            })
+                        })
+                        .map(|buffer| buffer.read(cx).snapshot())
+                else {
+                    return;
+                };
+                let outline_offset_range = outline.range.to_offset(&full_buffer_snapshot);
+                let scroll_target = multi_buffer_snapshot
+                    .excerpts()
+                    .filter(|(_, buffer_snapshot, _)| {
+                        let buffer_id = buffer_snapshot.remote_id();
+                        Some(buffer_id) == outline.range.start.buffer_id
+                            || Some(buffer_id) == outline.range.end.buffer_id
+                    })
+                    .min_by_key(|(_, _, excerpt_range)| {
+                        let excerpt_offeset_range =
+                            excerpt_range.context.to_offset(&full_buffer_snapshot);
+                        ((outline_offset_range.start / 2 + outline_offset_range.end / 2) as isize
+                            - (excerpt_offeset_range.start / 2 + excerpt_offeset_range.end / 2)
+                                as isize)
+                            .abs()
+                    })
+                    .and_then(|(excerpt_id, excerpt_snapshot, excerpt_range)| {
+                        let location = if outline.range.start.is_valid(excerpt_snapshot) {
+                            outline.range.start
+                        } else {
+                            excerpt_range.context.start
+                        };
+                        multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, location)
+                    });
+                if let Some(anchor) = scroll_target {
+                    self.selected_entry = Some(entry.clone());
+                    active_editor.update(cx, |editor, cx| {
+                        editor.set_scroll_anchor(
+                            ScrollAnchor {
+                                offset: Point::default(),
+                                anchor,
+                            },
+                            cx,
+                        );
+                    })
+                }
+            }
+        }
+    }
+
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         if let Some(selected_entry) = &self.selected_entry {
             let outline_to_select = match selected_entry {
@@ -784,7 +924,6 @@ impl OutlinePanel {
 
         let context_menu = ContextMenu::build(cx, |menu, _| {
             menu.context(self.focus_handle.clone())
-                .action("Copy Relative Path", Box::new(CopyRelativePath))
                 .action("Reveal in Finder", Box::new(RevealInFinder))
                 .action("Open in Terminal", Box::new(OpenInTerminal))
                 .when(is_unfoldable, |menu| {
@@ -1348,6 +1487,7 @@ impl OutlinePanel {
         let settings = OutlinePanelSettings::get_global(cx);
         let rendered_entry = rendered_entry.to_owned_entry();
         div()
+            .text_ui(cx)
             .id(item_id.clone())
             .child(
                 ListItem::new(item_id)
@@ -1369,156 +1509,7 @@ impl OutlinePanel {
                             if event.down.button == MouseButton::Right || event.down.first_mouse {
                                 return;
                             }
-
-                            let Some(active_editor) = outline_panel
-                                .active_item
-                                .as_ref()
-                                .and_then(|item| item.active_editor.upgrade())
-                            else {
-                                return;
-                            };
-                            let active_multi_buffer = active_editor.read(cx).buffer().clone();
-                            let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
-
-                            match &clicked_entry {
-                                EntryOwned::Entry(FsEntry::ExternalFile(buffer_id)) => {
-                                    let scroll_target = multi_buffer_snapshot.excerpts().find_map(
-                                        |(excerpt_id, buffer_snapshot, excerpt_range)| {
-                                            if &buffer_snapshot.remote_id() == buffer_id {
-                                                multi_buffer_snapshot.anchor_in_excerpt(
-                                                    excerpt_id,
-                                                    excerpt_range.context.start,
-                                                )
-                                            } else {
-                                                None
-                                            }
-                                        },
-                                    );
-                                    if let Some(anchor) = scroll_target {
-                                        outline_panel.selected_entry = Some(clicked_entry.clone());
-                                        active_editor.update(cx, |editor, cx| {
-                                            editor.set_scroll_anchor(
-                                                ScrollAnchor {
-                                                    offset: Point::new(
-                                                        0.0,
-                                                        -(editor.file_header_size() as f32),
-                                                    ),
-                                                    anchor,
-                                                },
-                                                cx,
-                                            );
-                                        })
-                                    }
-                                }
-                                entry @ EntryOwned::Entry(FsEntry::Directory(..)) => {
-                                    outline_panel.toggle_expanded(entry, cx);
-                                }
-                                entry @ EntryOwned::FoldedDirs(..) => {
-                                    outline_panel.toggle_expanded(entry, cx);
-                                }
-                                EntryOwned::Entry(FsEntry::File(_, file_entry)) => {
-                                    let scroll_target = outline_panel
-                                        .project
-                                        .update(cx, |project, cx| {
-                                            project
-                                                .path_for_entry(file_entry.id, cx)
-                                                .and_then(|path| project.get_open_buffer(&path, cx))
-                                        })
-                                        .map(|buffer| {
-                                            active_multi_buffer
-                                                .read(cx)
-                                                .excerpts_for_buffer(&buffer, cx)
-                                        })
-                                        .and_then(|excerpts| {
-                                            let (excerpt_id, excerpt_range) = excerpts.first()?;
-                                            multi_buffer_snapshot.anchor_in_excerpt(
-                                                *excerpt_id,
-                                                excerpt_range.context.start,
-                                            )
-                                        });
-                                    if let Some(anchor) = scroll_target {
-                                        outline_panel.selected_entry = Some(clicked_entry.clone());
-                                        active_editor.update(cx, |editor, cx| {
-                                            editor.set_scroll_anchor(
-                                                ScrollAnchor {
-                                                    offset: Point::new(
-                                                        0.0,
-                                                        -(editor.file_header_size() as f32),
-                                                    ),
-                                                    anchor,
-                                                },
-                                                cx,
-                                            );
-                                        })
-                                    }
-                                }
-                                EntryOwned::Outline(_, outline) => {
-                                    let Some(full_buffer_snapshot) = outline
-                                        .range
-                                        .start
-                                        .buffer_id
-                                        .and_then(|buffer_id| {
-                                            active_multi_buffer.read(cx).buffer(buffer_id)
-                                        })
-                                        .or_else(|| {
-                                            outline.range.end.buffer_id.and_then(|buffer_id| {
-                                                active_multi_buffer.read(cx).buffer(buffer_id)
-                                            })
-                                        })
-                                        .map(|buffer| buffer.read(cx).snapshot())
-                                    else {
-                                        return;
-                                    };
-                                    let outline_offset_range =
-                                        outline.range.to_offset(&full_buffer_snapshot);
-                                    let scroll_target = multi_buffer_snapshot
-                                        .excerpts()
-                                        .filter(|(_, buffer_snapshot, _)| {
-                                            let buffer_id = buffer_snapshot.remote_id();
-                                            Some(buffer_id) == outline.range.start.buffer_id
-                                                || Some(buffer_id) == outline.range.end.buffer_id
-                                        })
-                                        .min_by_key(|(_, _, excerpt_range)| {
-                                            let excerpt_offeset_range = excerpt_range
-                                                .context
-                                                .to_offset(&full_buffer_snapshot);
-                                            ((outline_offset_range.start / 2
-                                                + outline_offset_range.end / 2)
-                                                as isize
-                                                - (excerpt_offeset_range.start / 2
-                                                    + excerpt_offeset_range.end / 2)
-                                                    as isize)
-                                                .abs()
-                                        })
-                                        .and_then(
-                                            |(excerpt_id, excerpt_snapshot, excerpt_range)| {
-                                                let location = if outline
-                                                    .range
-                                                    .start
-                                                    .is_valid(excerpt_snapshot)
-                                                {
-                                                    outline.range.start
-                                                } else {
-                                                    excerpt_range.context.start
-                                                };
-                                                multi_buffer_snapshot
-                                                    .anchor_in_excerpt(excerpt_id, location)
-                                            },
-                                        );
-                                    if let Some(anchor) = scroll_target {
-                                        outline_panel.selected_entry = Some(clicked_entry.clone());
-                                        active_editor.update(cx, |editor, cx| {
-                                            editor.set_scroll_anchor(
-                                                ScrollAnchor {
-                                                    offset: Point::default(),
-                                                    anchor,
-                                                },
-                                                cx,
-                                            );
-                                        })
-                                    }
-                                }
-                            }
+                            outline_panel.open_entry(&clicked_entry, cx);
                         })
                     })
                     .on_secondary_mouse_down(cx.listener(
@@ -2366,6 +2357,7 @@ impl Render for OutlinePanel {
                 .size_full()
                 .relative()
                 .key_context(self.dispatch_context(cx))
+                .on_action(cx.listener(Self::open))
                 .on_action(cx.listener(Self::select_next))
                 .on_action(cx.listener(Self::select_prev))
                 .on_action(cx.listener(Self::select_first))