Compare commits

...

2 Commits

Author SHA1 Message Date
Richard Feldman
e19533e8c4 make it way more complicated but still broken
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-27 15:49:35 -04:00
Richard Feldman
3148583f79 wip
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-27 15:29:15 -04:00
5 changed files with 246 additions and 57 deletions

View File

@@ -259,6 +259,7 @@ impl ContextPicker {
&path_prefix,
false,
context_store.clone(),
None,
cx,
)
.into_any()
@@ -400,6 +401,7 @@ impl ContextPicker {
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
highlight_positions: None,
})
}),
)
@@ -517,6 +519,7 @@ fn recent_context_picker_entries(
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
highlight_positions: None,
})
}),
);

View File

@@ -9,7 +9,7 @@ use gpui::{
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem, Tooltip};
use ui::{prelude::*, HighlightedLabel, ListItem, Tooltip};
use util::ResultExt as _;
use workspace::{notifications::NotifyResultExt, Workspace};
@@ -193,6 +193,7 @@ impl PickerDelegate for FileContextPickerDelegate {
&path_match.path_prefix,
path_match.is_dir,
self.context_store.clone(),
Some(&path_match.positions),
cx,
)),
)
@@ -279,6 +280,7 @@ pub fn render_file_context_entry(
path_prefix: &Arc<str>,
is_directory: bool,
context_store: WeakEntity<ContextStore>,
highlight_positions: Option<&[usize]>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = if path == Path::new("") {
@@ -325,6 +327,11 @@ pub fn render_file_context_entry(
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
let label = match highlight_positions {
Some(positions) => HighlightedLabel::new(file_name, positions.to_vec()).into_any_element(),
None => Label::new(file_name).into_any_element(),
};
h_flex()
.id(id)
.gap_1p5()
@@ -333,7 +340,7 @@ pub fn render_file_context_entry(
.child(
h_flex()
.gap_1()
.child(Label::new(file_name))
.child(label)
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use ui::{prelude::*, HighlightedLabel, ListItem};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{self, ContextStore};
@@ -51,6 +51,7 @@ impl Render for ThreadContextPicker {
pub struct ThreadContextEntry {
pub id: ThreadId,
pub summary: SharedString,
pub highlight_positions: Option<Vec<usize>>,
}
pub struct ThreadContextPickerDelegate {
@@ -173,8 +174,18 @@ impl PickerDelegate for ThreadContextPickerDelegate {
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let highlights = thread
.highlight_positions
.as_ref()
.map(|vec| vec.as_slice());
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
render_thread_context_entry_with_highlights(
thread,
self.context_store.clone(),
highlights.as_deref(),
cx,
),
))
}
}
@@ -182,12 +193,31 @@ impl PickerDelegate for ThreadContextPickerDelegate {
pub fn render_thread_context_entry(
thread: &ThreadContextEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
cx: &App,
) -> Div {
render_thread_context_entry_with_highlights(thread, context_store, None, cx)
}
pub fn render_thread_context_entry_with_highlights(
thread: &ThreadContextEntry,
context_store: WeakEntity<ContextStore>,
highlight_positions: Option<&[usize]>,
cx: &App,
) -> Div {
let added = context_store.upgrade().map_or(false, |ctx_store| {
ctx_store.read(cx).includes_thread(&thread.id).is_some()
});
// Choose between regular label or highlighted label based on position data
let summary_element = match highlight_positions {
Some(positions) => HighlightedLabel::new(thread.summary.clone(), positions.to_vec())
.truncate()
.into_any_element(),
None => Label::new(thread.summary.clone())
.truncate()
.into_any_element(),
};
h_flex()
.gap_1p5()
.w_full()
@@ -201,7 +231,7 @@ pub fn render_thread_context_entry(
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(thread.summary.clone()).truncate()),
.child(summary_element),
)
.when(added, |el| {
el.child(
@@ -222,40 +252,60 @@ pub(crate) fn search_threads(
thread_store: Entity<ThreadStore>,
cx: &mut App,
) -> Task<Vec<ThreadContextEntry>> {
let threads = thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
});
// Get threads from the thread store
let threads = thread_store
.read(cx)
.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
highlight_positions: None, // Initialize with no highlights
})
.collect::<Vec<_>>();
// Return early for empty queries or if there are no threads
if threads.is_empty() || query.is_empty() {
return Task::ready(threads);
}
// Create candidates list for fuzzy matching
let candidates: Vec<_> = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect();
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
let threads_clone = threads.clone();
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
// Use background executor for the matching
cx.background_executor().spawn(async move {
// Perform fuzzy matching in background
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
// Create result entries with highlight positions included
let result = matches
.into_iter()
.filter_map(|mat| {
let thread = threads_clone.get(mat.candidate_id)?;
// Create a new entry with the highlight positions
Some(ThreadContextEntry {
id: thread.id.clone(),
summary: thread.summary.clone(),
highlight_positions: Some(mat.positions),
})
})
.collect::<Vec<ThreadContextEntry>>();
result
})
}

View File

@@ -953,28 +953,38 @@ impl FileFinderDelegate {
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let mut path_positions = path_match.positions.clone();
let positions = path_match.positions.clone();
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
// Calculate where the filename starts in the full path
let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
if pos >= &file_name_start {
Some(pos - file_name_start)
} else {
None
}
})
.collect();
// Create a copy of the full path without the filename (this is the parent directory)
let parent_path = full_path[..full_path.len() - file_name.len()].to_string();
// Process each highlight position
let mut file_name_positions = Vec::new();
let mut parent_path_positions = Vec::new();
for &pos in &positions {
// For the filename part
if pos >= file_name_start && pos < full_path.len() {
// This position is in the filename part
file_name_positions.push(pos - file_name_start);
}
// For the parent path part
if pos < parent_path.len() {
// This position is in the parent path part
parent_path_positions.push(pos);
}
}
let full_path = full_path.trim_end_matches(&file_name).to_string();
path_positions.retain(|idx| *idx < full_path.len());
(file_name, file_name_positions, full_path, path_positions)
(file_name, file_name_positions, parent_path, parent_path_positions)
}
fn lookup_absolute_path(
@@ -1339,7 +1349,119 @@ impl PickerDelegate for FileFinderDelegate {
.size(IconSize::Small.rems())
.into_any_element(),
};
let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx, ix);
// Get the path information
let path_info = match &path_match {
Match::History {
path: entry_path,
panel_match,
} => {
let worktree_id = entry_path.project.worktree_id;
let project_relative_path = &entry_path.project.path;
let has_worktree = self
.project
.read(cx)
.worktree_for_id(worktree_id, cx)
.is_some();
// Use window to avoid unused variable warning
let _ = window;
if let Some(absolute_path) =
entry_path.absolute.as_ref().filter(|_| !has_worktree)
{
(
absolute_path
.file_name()
.map_or_else(
|| project_relative_path.to_string_lossy(),
|file_name| file_name.to_string_lossy(),
)
.to_string(),
absolute_path.to_string_lossy().to_string(),
Vec::new(),
)
} else {
let mut path = Arc::clone(project_relative_path);
if project_relative_path.as_ref() == Path::new("") {
if let Some(absolute_path) = &entry_path.absolute {
path = Arc::from(absolute_path.as_path());
}
}
let mut path_match = PathMatch {
score: ix as f64,
positions: Vec::new(),
worktree_id: worktree_id.to_usize(),
path,
is_dir: false, // File finder doesn't support directories
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
};
if let Some(found_path_match) = &panel_match {
path_match
.positions
.extend(found_path_match.0.positions.iter())
}
let path_string = path_match.path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let positions = path_match.positions.clone();
let file_name = path_match.path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
(file_name, full_path, positions)
}
}
Match::Search(path_match) => {
let path_string = path_match.0.path.to_string_lossy();
let full_path = [path_match.0.path_prefix.as_ref(), path_string.as_ref()].join("");
let positions = path_match.0.positions.clone();
let file_name = path_match.0.path.file_name().map_or_else(
|| path_match.0.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
(file_name, full_path, positions)
}
};
let (file_name, full_path, positions) = path_info;
// Calculate where the filename starts in the full path
let file_name_start = full_path.len() - file_name.len();
// Create a parent path
let parent_path = full_path[..file_name_start].to_string();
// Create parent path label with highlighting
let parent_highlight_positions: Vec<usize> = positions
.iter()
.filter(|&&pos| pos < parent_path.len())
.copied()
.collect();
let parent_path_label = HighlightedLabel::new(parent_path, parent_highlight_positions)
.size(LabelSize::Small)
.color(Color::Muted);
// Create filename label with highlighting
let file_highlight_positions: Vec<usize> = positions
.iter()
.filter_map(|&pos| {
if pos >= file_name_start {
Some(pos - file_name_start)
} else {
None
}
})
.collect();
let file_name_label = HighlightedLabel::new(file_name.clone(), file_highlight_positions);
let file_icon = maybe!({
if !settings.file_icons {
@@ -1362,7 +1484,7 @@ impl PickerDelegate for FileFinderDelegate {
.gap_2()
.py_px()
.child(file_name_label)
.child(full_path_label),
.child(parent_path_label),
),
)
}

View File

@@ -15,10 +15,17 @@ impl HighlightedLabel {
/// Constructs a label with the given characters highlighted.
/// Characters are identified by UTF-8 byte position.
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
let label_str = label.into();
// Filter out indices that are out of bounds
let valid_indices = highlight_indices
.into_iter()
.filter(|&idx| idx < label_str.len())
.collect();
Self {
base: LabelLike::new(),
label: label.into(),
highlight_indices,
label: label_str,
highlight_indices: valid_indices,
}
}
}