@@ -36,8 +36,8 @@ use ui::{
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
- CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, ScreenShare,
- ShareProject, Workspace,
+ CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, OpenChannelNotesById,
+ ScreenShare, ShareProject, Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt},
};
@@ -114,6 +114,13 @@ pub fn init(cx: &mut App) {
});
}
});
+ workspace.register_action(|_, action: &OpenChannelNotesById, window, cx| {
+ let channel_id = client::ChannelId(action.channel_id);
+ let workspace = cx.entity();
+ window.defer(cx, move |window, cx| {
+ ChannelView::open(channel_id, None, workspace, window, cx).detach_and_log_err(cx)
+ });
+ });
// TODO: make it possible to bind this one to a held key for push to talk?
// how to make "toggle_on_modifiers_press" contextual?
workspace.register_action(|_, _: &Mute, _, cx| title_bar::collab::toggle_mute(cx));
@@ -4,10 +4,12 @@ mod file_finder_tests;
use futures::future::join_all;
pub use open_path_prompt::OpenPathDelegate;
+use channel::ChannelStore;
+use client::ChannelId;
use collections::HashMap;
use editor::Editor;
use file_icons::FileIcons;
-use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
+use fuzzy::{CharBag, PathMatch, PathMatchCandidate, StringMatch, StringMatchCandidate};
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
@@ -45,8 +47,8 @@ use util::{
rel_path::RelPath,
};
use workspace::{
- ModalView, OpenOptions, OpenVisible, SplitDirection, Workspace, item::PreviewTabsSettings,
- notifications::NotifyResultExt, pane,
+ ModalView, OpenChannelNotesById, OpenOptions, OpenVisible, SplitDirection, Workspace,
+ item::PreviewTabsSettings, notifications::NotifyResultExt, pane,
};
use zed_actions::search::ToggleIncludeIgnored;
@@ -321,7 +323,7 @@ impl FileFinder {
if let Some(workspace) = delegate.workspace.upgrade()
&& let Some(m) = delegate.matches.get(delegate.selected_index())
{
- let path = match &m {
+ let path = match m {
Match::History { path, .. } => {
let worktree_id = path.project.worktree_id;
ProjectPath {
@@ -334,6 +336,7 @@ impl FileFinder {
path: m.0.path.clone(),
},
Match::CreateNew(p) => p.clone(),
+ Match::Channel { .. } => return,
};
let open_task = workspace.update(cx, move |workspace, cx| {
workspace.split_path_preview(path, false, Some(split_direction), window, cx)
@@ -392,6 +395,7 @@ pub struct FileFinderDelegate {
file_finder: WeakEntity<FileFinder>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
+ channel_store: Option<Entity<ChannelStore>>,
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
@@ -450,13 +454,18 @@ struct Matches {
matches: Vec<Match>,
}
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+#[derive(Debug, Clone)]
enum Match {
History {
path: FoundPath,
panel_match: Option<ProjectPanelOrdMatch>,
},
Search(ProjectPanelOrdMatch),
+ Channel {
+ channel_id: ChannelId,
+ channel_name: SharedString,
+ string_match: StringMatch,
+ },
CreateNew(ProjectPath),
}
@@ -465,7 +474,7 @@ impl Match {
match self {
Match::History { path, .. } => Some(&path.project.path),
Match::Search(panel_match) => Some(&panel_match.0.path),
- Match::CreateNew(_) => None,
+ Match::Channel { .. } | Match::CreateNew(_) => None,
}
}
@@ -479,7 +488,7 @@ impl Match {
.read(cx)
.absolutize(&path_match.path),
),
- Match::CreateNew(_) => None,
+ Match::Channel { .. } | Match::CreateNew(_) => None,
}
}
@@ -487,7 +496,7 @@ impl Match {
match self {
Match::History { panel_match, .. } => panel_match.as_ref(),
Match::Search(panel_match) => Some(panel_match),
- Match::CreateNew(_) => None,
+ Match::Channel { .. } | Match::CreateNew(_) => None,
}
}
}
@@ -628,7 +637,6 @@ impl Matches {
(_, Match::CreateNew(_)) => return cmp::Ordering::Greater,
_ => {}
}
- debug_assert!(a.panel_match().is_some() && b.panel_match().is_some());
match (&a, &b) {
// bubble currently opened files to the top
@@ -651,32 +659,35 @@ impl Matches {
}
}
- let a_panel_match = match a.panel_match() {
- Some(pm) => pm,
- None => {
- return if b.panel_match().is_some() {
- cmp::Ordering::Less
- } else {
- cmp::Ordering::Equal
- };
+ // For file-vs-file matches, use the existing detailed comparison.
+ if let (Some(a_panel), Some(b_panel)) = (a.panel_match(), b.panel_match()) {
+ let a_in_filename = Self::is_filename_match(a_panel);
+ let b_in_filename = Self::is_filename_match(b_panel);
+
+ match (a_in_filename, b_in_filename) {
+ (true, false) => return cmp::Ordering::Greater,
+ (false, true) => return cmp::Ordering::Less,
+ _ => {}
}
- };
- let b_panel_match = match b.panel_match() {
- Some(pm) => pm,
- None => return cmp::Ordering::Greater,
- };
+ return a_panel.cmp(b_panel);
+ }
- let a_in_filename = Self::is_filename_match(a_panel_match);
- let b_in_filename = Self::is_filename_match(b_panel_match);
+ let a_score = Self::match_score(a);
+ let b_score = Self::match_score(b);
+ // When at least one side is a channel, compare by raw score.
+ a_score
+ .partial_cmp(&b_score)
+ .unwrap_or(cmp::Ordering::Equal)
+ }
- match (a_in_filename, b_in_filename) {
- (true, false) => return cmp::Ordering::Greater,
- (false, true) => return cmp::Ordering::Less,
- _ => {} // Both are filename matches or both are path matches
+ fn match_score(m: &Match) -> f64 {
+ match m {
+ Match::History { panel_match, .. } => panel_match.as_ref().map_or(0.0, |pm| pm.0.score),
+ Match::Search(pm) => pm.0.score,
+ Match::Channel { string_match, .. } => string_match.score,
+ Match::CreateNew(_) => 0.0,
}
-
- a_panel_match.cmp(b_panel_match)
}
/// Determines if the match occurred within the filename rather than in the path
@@ -833,10 +844,12 @@ impl FileFinderDelegate {
cx: &mut Context<FileFinder>,
) -> Self {
Self::subscribe_to_updates(&project, window, cx);
+ let channel_store = ChannelStore::try_global(cx);
Self {
file_finder,
workspace,
project,
+ channel_store,
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
@@ -971,6 +984,68 @@ impl FileFinderDelegate {
path_style,
);
+ // Add channel matches
+ if let Some(channel_store) = &self.channel_store {
+ let channel_store = channel_store.read(cx);
+ let channels: Vec<_> = channel_store.channels().cloned().collect();
+ if !channels.is_empty() {
+ let candidates = channels
+ .iter()
+ .enumerate()
+ .map(|(id, channel)| StringMatchCandidate::new(id, &channel.name));
+ let channel_query = query.path_query();
+ let query_lower = channel_query.to_lowercase();
+ let mut channel_matches = Vec::new();
+ for candidate in candidates {
+ let channel_name = candidate.string;
+ let name_lower = channel_name.to_lowercase();
+
+ let mut positions = Vec::new();
+ let mut query_idx = 0;
+ for (name_idx, name_char) in name_lower.char_indices() {
+ if query_idx < query_lower.len() {
+ let query_char =
+ query_lower[query_idx..].chars().next().unwrap_or_default();
+ if name_char == query_char {
+ positions.push(name_idx);
+ query_idx += query_char.len_utf8();
+ }
+ }
+ }
+
+ if query_idx == query_lower.len() {
+ let channel = &channels[candidate.id];
+ let score = if name_lower == query_lower {
+ 1.0
+ } else if name_lower.starts_with(&query_lower) {
+ 0.8
+ } else {
+ 0.5 * (query_lower.len() as f64 / name_lower.len() as f64)
+ };
+ channel_matches.push(Match::Channel {
+ channel_id: channel.id,
+ channel_name: channel.name.clone(),
+ string_match: StringMatch {
+ candidate_id: candidate.id,
+ score,
+ positions,
+ string: channel_name,
+ },
+ });
+ }
+ }
+ for channel_match in channel_matches {
+ match self
+ .matches
+ .position(&channel_match, self.currently_opened_path.as_ref())
+ {
+ Ok(_duplicate) => {}
+ Err(ix) => self.matches.matches.insert(ix, channel_match),
+ }
+ }
+ }
+ }
+
let query_path = query.raw_query.as_str();
if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) {
let available_worktree = self
@@ -1095,6 +1170,16 @@ impl FileFinderDelegate {
}
}
Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
+ Match::Channel {
+ channel_name,
+ string_match,
+ ..
+ } => (
+ channel_name.to_string(),
+ string_match.positions.clone(),
+ "Channel Notes".to_string(),
+ vec![],
+ ),
Match::CreateNew(project_path) => (
format!("Create file: {}", project_path.path.display(path_style)),
vec![],
@@ -1479,6 +1564,16 @@ impl PickerDelegate for FileFinderDelegate {
if let Some(m) = self.matches.get(self.selected_index())
&& let Some(workspace) = self.workspace.upgrade()
{
+ // Channel matches are handled separately since they dispatch an action
+ // rather than directly opening a file path.
+ if let Match::Channel { channel_id, .. } = m {
+ let channel_id = channel_id.0;
+ let finder = self.file_finder.clone();
+ window.dispatch_action(OpenChannelNotesById { channel_id }.boxed_clone(), cx);
+ finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
+ return;
+ }
+
let open_task = workspace.update(cx, |workspace, cx| {
let split_or_open =
|workspace: &mut Workspace,
@@ -1571,6 +1666,7 @@ impl PickerDelegate for FileFinderDelegate {
window,
cx,
),
+ Match::Channel { .. } => unreachable!("handled above"),
}
});
@@ -1627,7 +1723,7 @@ impl PickerDelegate for FileFinderDelegate {
let path_match = self.matches.get(ix)?;
- let history_icon = match &path_match {
+ let end_icon = match path_match {
Match::History { .. } => Icon::new(IconName::HistoryRerun)
.color(Color::Muted)
.size(IconSize::Small)
@@ -1636,6 +1732,10 @@ impl PickerDelegate for FileFinderDelegate {
.flex_none()
.size(IconSize::Small.rems())
.into_any_element(),
+ Match::Channel { .. } => v_flex()
+ .flex_none()
+ .size(IconSize::Small.rems())
+ .into_any_element(),
Match::CreateNew(_) => Icon::new(IconName::Plus)
.color(Color::Muted)
.size(IconSize::Small)
@@ -1643,21 +1743,24 @@ impl PickerDelegate for FileFinderDelegate {
};
let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
- let file_icon = maybe!({
- if !settings.file_icons {
- return None;
- }
- let abs_path = path_match.abs_path(&self.project, cx)?;
- let file_name = abs_path.file_name()?;
- let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
- Some(Icon::from_path(icon).color(Color::Muted))
- });
+ let file_icon = match path_match {
+ Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)),
+ _ => maybe!({
+ if !settings.file_icons {
+ return None;
+ }
+ let abs_path = path_match.abs_path(&self.project, cx)?;
+ let file_name = abs_path.file_name()?;
+ let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
+ Some(Icon::from_path(icon).color(Color::Muted))
+ }),
+ };
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.start_slot::<Icon>(file_icon)
- .end_slot::<AnyElement>(history_icon)
+ .end_slot::<AnyElement>(end_icon)
.inset(true)
.toggle_state(selected)
.child(