file_finder: Show collab channels in file search (#51120)
Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
1 parent
657098eeb4
commit
2cb8a77dbe
7 files changed
+176
-45
No files matched your search
Generated
+3
@@ -6242,6 +6242,8 @@ name = "file_finder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"channel",
|
||||
"client",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor",
|
||||
@@ -6255,6 +6257,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"project_panel",
|
||||
"remote_connection",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
|
||||
@@ -156,6 +156,10 @@ impl ChannelStore {
|
||||
cx.global::<GlobalChannelStore>().0.clone()
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalChannelStore>().map(|g| g.0.clone())
|
||||
}
|
||||
|
||||
pub fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
||||
let rpc_subscriptions = [
|
||||
client.add_message_handler(cx.weak_entity(), Self::handle_update_channels),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -14,6 +14,8 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
channel.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
file_icons.workspace = true
|
||||
@@ -45,3 +47,4 @@ serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
remote_connection = { workspace = true, features = ["test-support"] }
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
let a_in_filename = Self::is_filename_match(a_panel_match);
|
||||
let b_in_filename = Self::is_filename_match(b_panel_match);
|
||||
|
||||
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
|
||||
return a_panel.cmp(b_panel);
|
||||
}
|
||||
|
||||
a_panel_match.cmp(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)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
|
||||
@@ -3709,7 +3709,7 @@ impl SearchEntries {
|
||||
fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
|
||||
let mut search_entries = SearchEntries::default();
|
||||
for m in &picker.delegate.matches.matches {
|
||||
match &m {
|
||||
match m {
|
||||
Match::History {
|
||||
path: history_path,
|
||||
panel_match: path_match,
|
||||
@@ -3734,6 +3734,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
|
||||
search_entries.search_matches.push(path_match.0.clone());
|
||||
}
|
||||
Match::CreateNew(_) => {}
|
||||
Match::Channel { .. } => {}
|
||||
}
|
||||
}
|
||||
search_entries
|
||||
@@ -3768,6 +3769,7 @@ fn assert_match_at_position(
|
||||
Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
|
||||
Match::Search(path_match) => path_match.0.path.file_name(),
|
||||
Match::CreateNew(project_path) => project_path.path.file_name(),
|
||||
Match::Channel { channel_name, .. } => Some(channel_name.as_str()),
|
||||
}
|
||||
.unwrap();
|
||||
assert_eq!(match_file_name, expected_file_name);
|
||||
|
||||
@@ -8330,6 +8330,15 @@ actions!(
|
||||
CopyRoomId,
|
||||
]
|
||||
);
|
||||
|
||||
/// Opens the channel notes for a specific channel by its ID.
|
||||
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = collab)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OpenChannelNotesById {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
|
||||
Reference in new issue
Block a user