Compare commits
2 Commits
vim-syntax
...
highlight-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e19533e8c4 | ||
|
|
3148583f79 |
@@ -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,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user