Introduce an outline panel (#12637)

Adds a new panel: `OutlinePanel` which looks very close to project
panel:

<img width="256" alt="Screenshot 2024-06-10 at 23 19 05"
src="https://github.com/zed-industries/zed/assets/2690773/c66e6e78-44ec-4de8-8d60-43238bb09ae9">

has similar settings and keymap (actions work in the `OutlinePanel`
context and are under `outline_panel::` namespace), with two notable
differences:
* no "edit" actions such as cut/copy/paste/delete/etc.
* directory auto folding is enabled by default

Empty view: 
<img width="841" alt="Screenshot 2024-06-10 at 23 19 11"
src="https://github.com/zed-industries/zed/assets/2690773/dc8bf37c-5a70-4fd5-9b57-76271eb7a40c">


When editor gets active, the panel displays all related files in a tree
(similar to what the project panel does) and all related excerpts'
outlines under each file.
Same as in the project panel, directories can be expanded or collapsed,
unfolded or folded; clicking file entries or outlines scrolls the buffer
to the corresponding excerpt; changing editor's selection reveals the
corresponding outline in the panel.

The panel is applicable to any singleton buffer:
<img width="1215" alt="Screenshot 2024-06-10 at 23 19 35"
src="https://github.com/zed-industries/zed/assets/2690773/a087631f-5c2d-4d4d-ae25-30ab9731d528">

<img width="1728" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/e4f8082c-d12d-4473-8500-e8fd1051285b">

or any multi buffer:

(search multi buffer)

<img width="1728" alt="Screenshot 2024-06-10 at 23 19 41"
src="https://github.com/zed-industries/zed/assets/2690773/60f768a3-6716-4520-9b13-42da8fd15f50">

(diagnostics multi buffer)
<img width="1728" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/64e285bd-9530-4bf2-8f1f-10ee5596067c">

Release Notes:
- Added an outline panel to show a "map" of the active editor
This commit is contained in:
Kirill Bulatov 2024-06-12 23:22:52 +03:00 committed by GitHub
parent 7f56f4e78e
commit 8451dba6a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 2860 additions and 57 deletions

27
Cargo.lock generated
View file

@ -7132,7 +7132,6 @@ dependencies = [
"project", "project",
"rope", "rope",
"serde_json", "serde_json",
"settings",
"smol", "smol",
"theme", "theme",
"tree-sitter-rust", "tree-sitter-rust",
@ -7142,6 +7141,31 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "outline_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"db",
"editor",
"file_icons",
"git",
"gpui",
"language",
"log",
"menu",
"project",
"schemars",
"serde",
"serde_json",
"settings",
"unicase",
"util",
"workspace",
"worktree",
]
[[package]] [[package]]
name = "outref" name = "outref"
version = "0.5.1" version = "0.5.1"
@ -13255,6 +13279,7 @@ dependencies = [
"node_runtime", "node_runtime",
"notifications", "notifications",
"outline", "outline",
"outline_panel",
"parking_lot", "parking_lot",
"profiling", "profiling",
"project", "project",

View file

@ -64,6 +64,7 @@ members = [
"crates/ollama", "crates/ollama",
"crates/open_ai", "crates/open_ai",
"crates/outline", "crates/outline",
"crates/outline_panel",
"crates/picker", "crates/picker",
"crates/prettier", "crates/prettier",
"crates/project", "crates/project",
@ -212,6 +213,7 @@ notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" } ollama = { path = "crates/ollama" }
open_ai = { path = "crates/open_ai" } open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" } outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
picker = { path = "crates/picker" } picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" } plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" } plugin_macros = { path = "crates/plugin_macros" }

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>

After

Width:  |  Height:  |  Size: 349 B

View file

@ -439,6 +439,7 @@
"ctrl-shift-p": "command_palette::Toggle", "ctrl-shift-p": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy", "ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus", "ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll", "ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle", "ctrl-k m": "language_selector::Toggle",
@ -562,6 +563,18 @@
"ctrl-enter": "project_search::SearchInNew" "ctrl-enter": "project_search::SearchInNew"
} }
}, },
{
"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",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{ {
"context": "ProjectPanel", "context": "ProjectPanel",
"bindings": { "bindings": {
@ -583,7 +596,10 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFinder", "alt-ctrl-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory" "alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"
} }
}, },
{ {

View file

@ -475,6 +475,7 @@
"cmd-shift-p": "command_palette::Toggle", "cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy", "cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus", "cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus", "cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll", "cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle", "cmd-k m": "language_selector::Toggle",
@ -584,6 +585,18 @@
"cmd-enter": "project_search::SearchInNew" "cmd-enter": "project_search::SearchInNew"
} }
}, },
{
"context": "OutlinePanel",
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFinder",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{ {
"context": "ProjectPanel", "context": "ProjectPanel",
"bindings": { "bindings": {

View file

@ -302,6 +302,29 @@
/// when a directory has only one directory inside. /// when a directory has only one directory inside.
"auto_fold_dirs": false "auto_fold_dirs": false
}, },
"outline_panel": {
// Whether to show the outline panel button in the status bar
"button": true,
// Default width of the outline panel.
"default_width": 240,
// Where to dock the outline panel. Can be 'left' or 'right'.
"dock": "left",
// Whether to show file icons in the outline panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the outline panel.
"folder_icons": true,
// Whether to show the git status in the outline panel.
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to reveal it in the outline panel automatically,
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true
},
"collaboration_panel": { "collaboration_panel": {
// Whether to show the collaboration panel button in the status bar. // Whether to show the collaboration panel button in the status bar.
"button": true, "button": true,

View file

@ -149,6 +149,9 @@ use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::find_url; use crate::hover_links::find_url;
pub const FILE_HEADER_HEIGHT: u8 = 1;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1;
pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u8 = 1;
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024; const MAX_LINE_LEN: usize = 1024;
@ -529,6 +532,7 @@ pub struct Editor {
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>, tasks_update_task: Option<Task<()>>,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>, previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
file_header_size: u8,
} }
#[derive(Clone)] #[derive(Clone)]
@ -1651,9 +1655,8 @@ impl Editor {
}), }),
merge_adjacent: true, merge_adjacent: true,
}; };
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
let display_map = cx.new_model(|cx| { let display_map = cx.new_model(|cx| {
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
DisplayMap::new( DisplayMap::new(
buffer.clone(), buffer.clone(),
style.font(), style.font(),
@ -1661,8 +1664,8 @@ impl Editor {
None, None,
show_excerpt_controls, show_excerpt_controls,
file_header_size, file_header_size,
1, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
1, MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
fold_placeholder, fold_placeholder,
cx, cx,
) )
@ -1812,6 +1815,7 @@ impl Editor {
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
blame: None, blame: None,
blame_subscription: None, blame_subscription: None,
file_header_size,
tasks: Default::default(), tasks: Default::default(),
_subscriptions: vec![ _subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed), cx.observe(&buffer, Self::on_buffer_changed),
@ -10829,6 +10833,12 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() }) cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
} }
multi_buffer::Event::ExcerptsEdited { ids } => {
cx.emit(EditorEvent::ExcerptsEdited { ids: ids.clone() })
}
multi_buffer::Event::ExcerptsExpanded { ids } => {
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
}
multi_buffer::Event::Reparsed => { multi_buffer::Event::Reparsed => {
self.tasks_update_task = Some(self.refresh_runnables(cx)); self.tasks_update_task = Some(self.refresh_runnables(cx));
@ -11299,6 +11309,10 @@ impl Editor {
})); }));
self self
} }
pub fn file_header_size(&self) -> u8 {
self.file_header_size
}
} }
fn hunks_for_selections( fn hunks_for_selections(
@ -11743,6 +11757,12 @@ pub enum EditorEvent {
ExcerptsRemoved { ExcerptsRemoved {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
ExcerptsExpanded {
ids: Vec<ExcerptId>,
},
BufferEdited, BufferEdited,
Edited, Edited,
Reparsed, Reparsed,

View file

@ -1,6 +1,6 @@
use std::iter::FromIterator; use std::iter::FromIterator;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct CharBag(u64); pub struct CharBag(u64);
impl CharBag { impl CharBag {

View file

@ -316,7 +316,7 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GitFileStatus { pub enum GitFileStatus {
Added, Added,
Modified, Modified,

View file

@ -1,6 +1,9 @@
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use serde::de::{self, Deserialize, Deserializer, Visitor}; use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt; use std::{
fmt,
hash::{Hash, Hasher},
};
/// Convert an RGB hex color code number to a color type /// Convert an RGB hex color code number to a color type
pub fn rgb(hex: u32) -> Rgba { pub fn rgb(hex: u32) -> Rgba {
@ -267,6 +270,15 @@ impl Ord for Hsla {
impl Eq for Hsla {} impl Eq for Hsla {}
impl Hash for Hsla {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u32(u32::from_be_bytes(self.h.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.s.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.l.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.a.to_be_bytes()));
}
}
/// Construct an [`Hsla`] object from plain values /// Construct an [`Hsla`] object from plain values
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla { Hsla {

View file

@ -1,4 +1,8 @@
use std::{iter, mem, ops::Range}; use std::{
hash::{Hash, Hasher},
iter, mem,
ops::Range,
};
use crate::{ use crate::{
black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement, black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement,
@ -319,6 +323,20 @@ pub struct HighlightStyle {
impl Eq for HighlightStyle {} impl Eq for HighlightStyle {}
impl Hash for HighlightStyle {
fn hash<H: Hasher>(&self, state: &mut H) {
self.color.hash(state);
self.font_weight.hash(state);
self.font_style.hash(state);
self.background_color.hash(state);
self.underline.hash(state);
self.strikethrough.hash(state);
state.write_u32(u32::from_be_bytes(
self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(),
));
}
}
impl Style { impl Style {
/// Returns true if the style is visible and the background is opaque. /// Returns true if the style is visible and the background is opaque.
pub fn has_opaque_background(&self) -> bool { pub fn has_opaque_background(&self) -> bool {
@ -549,7 +567,7 @@ impl Default for Style {
} }
/// The properties that can be applied to an underline. /// The properties that can be applied to an underline.
#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)] #[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
#[refineable(Debug)] #[refineable(Debug)]
pub struct UnderlineStyle { pub struct UnderlineStyle {
/// The thickness of the underline. /// The thickness of the underline.
@ -563,7 +581,7 @@ pub struct UnderlineStyle {
} }
/// The properties that can be applied to a strikethrough. /// The properties that can be applied to a strikethrough.
#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)] #[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
#[refineable(Debug)] #[refineable(Debug)]
pub struct StrikethroughStyle { pub struct StrikethroughStyle {
/// The thickness of the strikethrough. /// The thickness of the strikethrough.

View file

@ -2738,12 +2738,13 @@ impl BufferSnapshot {
Some(items) Some(items)
} }
fn outline_items_containing( pub fn outline_items_containing<T: ToOffset>(
&self, &self,
range: Range<usize>, range: Range<T>,
include_extra_context: bool, include_extra_context: bool,
theme: Option<&SyntaxTheme>, theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<Anchor>>> { ) -> Option<Vec<OutlineItem<Anchor>>> {
let range = range.to_offset(self);
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
grammar.outline_config.as_ref().map(|c| &c.query) grammar.outline_config.as_ref().map(|c| &c.query)
}); });

View file

@ -70,7 +70,7 @@ pub use language_registry::{
PendingLanguageServer, QUERY_FILENAME_PREFIXES, PendingLanguageServer, QUERY_FILENAME_PREFIXES,
}; };
pub use lsp::LanguageServerId; pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem}; pub use outline::{render_item, Outline, OutlineItem};
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer}; pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
pub use text::{AnchorRangeExt, LineEnding}; pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor}; pub use tree_sitter::{Node, Parser, Tree, TreeCursor};

View file

@ -1,6 +1,11 @@
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{BackgroundExecutor, HighlightStyle}; use gpui::{
relative, AppContext, BackgroundExecutor, FontStyle, FontWeight, HighlightStyle, StyledText,
TextStyle, WhiteSpace,
};
use settings::Settings;
use std::ops::Range; use std::ops::Range;
use theme::{ActiveTheme, ThemeSettings};
/// An outline of all the symbols contained in a buffer. /// An outline of all the symbols contained in a buffer.
#[derive(Debug)] #[derive(Debug)]
@ -11,7 +16,7 @@ pub struct Outline<T> {
path_candidate_prefixes: Vec<usize>, path_candidate_prefixes: Vec<usize>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct OutlineItem<T> { pub struct OutlineItem<T> {
pub depth: usize, pub depth: usize,
pub range: Range<T>, pub range: Range<T>,
@ -138,3 +143,34 @@ impl<T> Outline<T> {
tree_matches tree_matches
} }
} }
pub fn render_item<T>(
outline_item: &OutlineItem<T>,
custom_highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
cx: &AppContext,
) -> StyledText {
let settings = ThemeSettings::get_global(cx);
// TODO: We probably shouldn't need to build a whole new text style here
// but I'm not sure how to get the current one and modify it.
// Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
let highlights = gpui::combine_highlights(
custom_highlights,
outline_item.highlight_ranges.iter().cloned(),
);
StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights)
}

View file

@ -77,6 +77,9 @@ pub enum Event {
ExcerptsRemoved { ExcerptsRemoved {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },
ExcerptsExpanded {
ids: Vec<ExcerptId>,
},
ExcerptsEdited { ExcerptsEdited {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },
@ -1666,8 +1669,9 @@ impl MultiBuffer {
} }
self.sync(cx); self.sync(cx);
let ids = ids.into_iter().collect::<Vec<_>>();
let snapshot = self.snapshot(cx); let snapshot = self.snapshot(cx);
let locators = snapshot.excerpt_locators_for_ids(ids); let locators = snapshot.excerpt_locators_for_ids(ids.iter().copied());
let mut new_excerpts = SumTree::new(); let mut new_excerpts = SumTree::new();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
let mut edits = Vec::<Edit<usize>>::new(); let mut edits = Vec::<Edit<usize>>::new();
@ -1746,6 +1750,7 @@ impl MultiBuffer {
cx.emit(Event::Edited { cx.emit(Event::Edited {
singleton_buffer_edited: false, singleton_buffer_edited: false,
}); });
cx.emit(Event::ExcerptsExpanded { ids });
cx.notify(); cx.notify();
} }

View file

@ -19,7 +19,6 @@ gpui.workspace = true
language.workspace = true language.workspace = true
ordered-float.workspace = true ordered-float.workspace = true
picker.workspace = true picker.workspace = true
settings.workspace = true
smol.workspace = true smol.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true

View file

@ -2,19 +2,18 @@ use editor::{scroll::Autoscroll, Anchor, AnchorRangeExt, Editor, EditorMode};
use fuzzy::StringMatch; use fuzzy::StringMatch;
use gpui::{ use gpui::{
actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
FontStyle, FontWeight, HighlightStyle, ParentElement, Point, Render, Styled, StyledText, Task, HighlightStyle, ParentElement, Point, Render, Styled, Task, View, ViewContext, VisualContext,
TextStyle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, WeakView, WindowContext,
}; };
use language::Outline; use language::Outline;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::{ use std::{
cmp::{self, Reverse}, cmp::{self, Reverse},
sync::Arc, sync::Arc,
}; };
use theme::{color_alpha, ActiveTheme, ThemeSettings}; use theme::{color_alpha, ActiveTheme};
use ui::{prelude::*, ListItem, ListItemSpacing}; use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt; use util::ResultExt;
use workspace::{DismissDecision, ModalView}; use workspace::{DismissDecision, ModalView};
@ -268,38 +267,12 @@ impl PickerDelegate for OutlineViewDelegate {
selected: bool, selected: bool,
cx: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let settings = ThemeSettings::get_global(cx); let mat = self.matches.get(ix)?;
let outline_item = self.outline.items.get(mat.candidate_id)?;
// TODO: We probably shouldn't need to build a whole new text style here
// but I'm not sure how to get the current one and modify it.
// Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
let mut highlight_style = HighlightStyle::default(); let mut highlight_style = HighlightStyle::default();
highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3)); highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
let custom_highlights = mat.ranges().map(|range| (range, highlight_style));
let mat = &self.matches[ix];
let outline_item = &self.outline.items[mat.candidate_id];
let highlights = gpui::combine_highlights(
mat.ranges().map(|range| (range, highlight_style)),
outline_item.highlight_ranges.iter().cloned(),
);
let styled_text =
StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights);
Some( Some(
ListItem::new(ix) ListItem::new(ix)
@ -310,7 +283,7 @@ impl PickerDelegate for OutlineViewDelegate {
div() div()
.text_ui(cx) .text_ui(cx)
.pl(rems(outline_item.depth as f32)) .pl(rems(outline_item.depth as f32))
.child(styled_text), .child(language::render_item(outline_item, custom_highlights, cx)),
), ),
) )
} }

View file

@ -0,0 +1,37 @@
[package]
name = "outline_panel"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/outline_panel.rs"
doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
file_icons.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
unicase.workspace = true
util.workspace = true
worktree.workspace = true
workspace.workspace = true
[package.metadata.cargo-machete]
ignored = ["log"]

View file

@ -0,0 +1 @@
../../LICENSE-GPL

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
use anyhow;
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum OutlinePanelDockPosition {
Left,
Right,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct OutlinePanelSettings {
pub button: bool,
pub default_width: Pixels,
pub dock: OutlinePanelDockPosition,
pub file_icons: bool,
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct OutlinePanelSettingsContent {
/// Whether to show the outline panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Customise default width (in pixels) taken by outline panel
///
/// Default: 240
pub default_width: Option<f32>,
/// The position of outline panel
///
/// Default: left
pub dock: Option<OutlinePanelDockPosition>,
/// Whether to show file icons in the outline panel.
///
/// Default: true
pub file_icons: Option<bool>,
/// Whether to show folder icons or chevrons for directories in the outline panel.
///
/// Default: true
pub folder_icons: Option<bool>,
/// Whether to show the git status in the outline panel.
///
/// Default: true
pub git_status: Option<bool>,
/// Amount of indentation (in pixels) for nested items.
///
/// Default: 20
pub indent_size: Option<f32>,
/// Whether to reveal it in the outline panel automatically,
/// when a corresponding project entry becomes active.
/// Gitignored entries are never auto revealed.
///
/// Default: true
pub auto_reveal_entries: Option<bool>,
/// Whether to fold directories automatically
/// when directory has only one directory inside.
///
/// Default: true
pub auto_fold_dirs: Option<bool>,
}
impl Settings for OutlinePanelSettings {
const KEY: Option<&'static str> = Some("outline_panel");
type FileContent = OutlinePanelSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
sources.json_merge()
}
}

View file

@ -144,6 +144,7 @@ pub enum IconName {
InlayHint, InlayHint,
Library, Library,
Link, Link,
ListTree,
MagicWand, MagicWand,
MagnifyingGlass, MagnifyingGlass,
MailOpen, MailOpen,
@ -274,6 +275,7 @@ impl IconName {
IconName::InlayHint => "icons/inlay_hint.svg", IconName::InlayHint => "icons/inlay_hint.svg",
IconName::Library => "icons/library.svg", IconName::Library => "icons/library.svg",
IconName::Link => "icons/link.svg", IconName::Link => "icons/link.svg",
IconName::ListTree => "icons/list_tree.svg",
IconName::MagicWand => "icons/magic_wand.svg", IconName::MagicWand => "icons/magic_wand.svg",
IconName::MagnifyingGlass => "icons/magnifying_glass.svg", IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
IconName::MailOpen => "icons/mail_open.svg", IconName::MailOpen => "icons/mail_open.svg",

View file

@ -2000,7 +2000,7 @@ impl Snapshot {
} }
} }
fn traverse_from_path( pub fn traverse_from_path(
&self, &self,
include_files: bool, include_files: bool,
include_dirs: bool, include_dirs: bool,
@ -2991,7 +2991,7 @@ impl File {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Entry { pub struct Entry {
pub id: ProjectEntryId, pub id: ProjectEntryId,
pub kind: EntryKind, pub kind: EntryKind,
@ -3020,7 +3020,7 @@ pub struct Entry {
pub is_private: bool, pub is_private: bool,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EntryKind { pub enum EntryKind {
UnloadedDir, UnloadedDir,
PendingDir, PendingDir,
@ -4818,6 +4818,14 @@ impl<'a> Traversal<'a> {
false false
} }
pub fn back_to_parent(&mut self) -> bool {
let Some(parent_path) = self.cursor.item().and_then(|entry| entry.path.parent()) else {
return false;
};
self.cursor
.seek(&TraversalTarget::Path(parent_path), Bias::Left, &())
}
pub fn entry(&self) -> Option<&'a Entry> { pub fn entry(&self) -> Option<&'a Entry> {
self.cursor.item() self.cursor.item()
} }

View file

@ -68,6 +68,7 @@ nix = {workspace = true, features = ["pthread", "signal"] }
node_runtime.workspace = true node_runtime.workspace = true
notifications.workspace = true notifications.workspace = true
outline.workspace = true outline.workspace = true
outline_panel.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
profiling.workspace = true profiling.workspace = true
project.workspace = true project.workspace = true

View file

@ -185,6 +185,7 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
outline::init(cx); outline::init(cx);
project_symbols::init(cx); project_symbols::init(cx);
project_panel::init(Assets, cx); project_panel::init(Assets, cx);
outline_panel::init(Assets, cx);
tasks_ui::init(cx); tasks_ui::init(cx);
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx); channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
search::init(cx); search::init(cx);

View file

@ -18,6 +18,7 @@ pub use open_listener::*;
use anyhow::Context as _; use anyhow::Context as _;
use assets::Assets; use assets::Assets;
use futures::{channel::mpsc, select_biased, StreamExt}; use futures::{channel::mpsc, select_biased, StreamExt};
use outline_panel::OutlinePanel;
use project::TaskSourceKind; use project::TaskSourceKind;
use project_panel::ProjectPanel; use project_panel::ProjectPanel;
use quick_action_bar::QuickActionBar; use quick_action_bar::QuickActionBar;
@ -190,6 +191,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let assistant_panel = let assistant_panel =
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone()); assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
let channels_panel = let channels_panel =
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
@ -202,6 +204,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let ( let (
project_panel, project_panel,
outline_panel,
terminal_panel, terminal_panel,
assistant_panel, assistant_panel,
channels_panel, channels_panel,
@ -209,6 +212,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
notification_panel, notification_panel,
) = futures::try_join!( ) = futures::try_join!(
project_panel, project_panel,
outline_panel,
terminal_panel, terminal_panel,
assistant_panel, assistant_panel,
channels_panel, channels_panel,
@ -219,6 +223,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace_handle.update(&mut cx, |workspace, cx| { workspace_handle.update(&mut cx, |workspace, cx| {
workspace.add_panel(assistant_panel, cx); workspace.add_panel(assistant_panel, cx);
workspace.add_panel(project_panel, cx); workspace.add_panel(project_panel, cx);
workspace.add_panel(outline_panel, cx);
workspace.add_panel(terminal_panel, cx); workspace.add_panel(terminal_panel, cx);
workspace.add_panel(channels_panel, cx); workspace.add_panel(channels_panel, cx);
workspace.add_panel(chat_panel, cx); workspace.add_panel(chat_panel, cx);
@ -377,6 +382,13 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.toggle_panel_focus::<ProjectPanel>(cx); workspace.toggle_panel_focus::<ProjectPanel>(cx);
}, },
) )
.register_action(
|workspace: &mut Workspace,
_: &outline_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
workspace.toggle_panel_focus::<OutlinePanel>(cx);
},
)
.register_action( .register_action(
|workspace: &mut Workspace, |workspace: &mut Workspace,
_: &collab_ui::collab_panel::ToggleFocus, _: &collab_ui::collab_panel::ToggleFocus,
@ -3093,9 +3105,9 @@ mod tests {
command_palette::init(cx); command_palette::init(cx);
language::init(cx); language::init(cx);
editor::init(cx); editor::init(cx);
project_panel::init_settings(cx);
collab_ui::init(&app_state, cx); collab_ui::init(&app_state, cx);
project_panel::init((), cx); project_panel::init((), cx);
outline_panel::init((), cx);
terminal_view::init(cx); terminal_view::init(cx);
assistant::init(app_state.client.clone(), cx); assistant::init(app_state.client.clone(), cx);
tasks_ui::init(cx); tasks_ui::init(cx);

View file

@ -123,6 +123,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
}), }),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action("Project Panel", project_panel::ToggleFocus), MenuItem::action("Project Panel", project_panel::ToggleFocus),
MenuItem::action("Outline Panel", outline_panel::ToggleFocus),
MenuItem::action("Collab Panel", collab_panel::ToggleFocus), MenuItem::action("Collab Panel", collab_panel::ToggleFocus),
MenuItem::action("Terminal Panel", terminal_panel::ToggleFocus), MenuItem::action("Terminal Panel", terminal_panel::ToggleFocus),
MenuItem::separator(), MenuItem::separator(),