Port recent Warp terminal and UI improvements
Improve tab-group search, rename focus, history navigation, shell prompt and completion handling, emoji width promotion, and macOS clipboard support. Guard unavailable settings/model-selector/undo-close views and forward the MCP OAuth callback issuer through the existing authentication flow. Adapt local changes from 14 upstream Warp commits through f571865ca3f2, reviewed from the latest 200 commits. Preserve Galaxy provider lifecycle, permissions, storage, and branding; exclude hosted/cloud infrastructure. Fix existing boolean-assertion lints and use stronger optimization with four code-generation units for the app test target to fit macOS unwind offsets. Production and dependency compilation settings remain unchanged. Validation: two clean builds and a final build; both prescribed Clippy commands with -D warnings; formatting, shell syntax and completion cleanup; 167 focused regression tests. Galaxy was not launched.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
pub mod telemetry;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -17,9 +17,9 @@ use galaxyui::elements::{
|
||||
resizable_state_handle, Border, ChildAnchor, Clipped, ClippedScrollStateHandle,
|
||||
ClippedScrollable, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
DispatchEventResult, DragAxis, DragBarSide, Draggable, DropShadow, DropTarget, Element, Empty,
|
||||
EventHandler, Expanded, Fill as ElementFill, Flex, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle, OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds,
|
||||
PositionedElementAnchor, PositionedElementOffsetBounds, Radius, Resizable,
|
||||
EventHandler, Expanded, Fill as ElementFill, Flex, Highlight, Hoverable, MainAxisAlignment,
|
||||
MainAxisSize, MouseStateHandle, OffsetPositioning, Padding, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, PositionedElementAnchor, PositionedElementOffsetBounds, Radius, Resizable,
|
||||
ResizableStateHandle, SavePosition, ScrollTarget, ScrollToPositionMode, ScrollbarWidth,
|
||||
Shrinkable, Stack, Text,
|
||||
};
|
||||
@@ -1165,12 +1165,18 @@ impl VerticalTabsPanelState {
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the indices (in original order) of tab groups that have at least
|
||||
/// one pane matching the current search query. Returns all indices when the
|
||||
/// query is empty.
|
||||
/// Returns the indices (in original order) of tabs that match the current
|
||||
/// search query, either through their own text or by belonging to a tab
|
||||
/// group whose displayed name matches. Returns all indices when the query
|
||||
/// is empty.
|
||||
///
|
||||
/// This drives tab cycling under an active search, so it must admit exactly
|
||||
/// the tabs `render_groups` renders — otherwise the panel would show tabs
|
||||
/// the next/previous-tab keybindings refuse to visit.
|
||||
pub(super) fn matching_tab_indices(
|
||||
&self,
|
||||
tabs: &[TabData],
|
||||
tab_groups: &HashMap<TabGroupId, TabGroup>,
|
||||
active_tab_index: usize,
|
||||
app: &AppContext,
|
||||
) -> Vec<usize> {
|
||||
@@ -1178,6 +1184,7 @@ impl VerticalTabsPanelState {
|
||||
return (0..tabs.len()).collect();
|
||||
}
|
||||
let query_lower = self.search_query.to_lowercase();
|
||||
let matched_groups = matched_group_ids(tab_groups, &query_lower);
|
||||
let resolved_mode = resolve_vertical_tabs_mode(app);
|
||||
let display_granularity = match resolved_mode {
|
||||
VerticalTabsResolvedMode::Panes => VerticalTabsDisplayGranularity::Panes,
|
||||
@@ -1188,6 +1195,10 @@ impl VerticalTabsPanelState {
|
||||
tabs.iter()
|
||||
.enumerate()
|
||||
.filter(|(tab_index, tab)| {
|
||||
// A group-name match admits every member, regardless of its own text.
|
||||
if tab_admitted_by_group_name(tab.group_id, &matched_groups) {
|
||||
return true;
|
||||
}
|
||||
let pane_group = tab.pane_group.as_ref(app);
|
||||
let visible_pane_ids = pane_group.visible_pane_ids();
|
||||
match resolved_mode {
|
||||
@@ -1780,7 +1791,7 @@ fn render_groups(
|
||||
.collect()
|
||||
} else {
|
||||
let query_lower = query.to_lowercase();
|
||||
workspace
|
||||
let own_matches: Vec<(usize, Option<Vec<PaneId>>)> = workspace
|
||||
.tabs
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1881,7 +1892,15 @@ fn render_groups(
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
|
||||
// A query matching a group's name reveals every tab under that group,
|
||||
// even members whose own text does not match.
|
||||
let matched_groups = matched_group_ids(&workspace.tab_groups, &query_lower);
|
||||
let tab_group_ids: Vec<Option<TabGroupId>> =
|
||||
workspace.tabs.iter().map(|tab| tab.group_id).collect();
|
||||
|
||||
merge_group_name_matches(&tab_group_ids, &matched_groups, own_matches)
|
||||
};
|
||||
|
||||
if visible_tabs.is_empty() {
|
||||
@@ -1929,7 +1948,14 @@ fn render_groups(
|
||||
.get(&gid)
|
||||
.map(|group| (gid, group.clone()))
|
||||
}) {
|
||||
Some((group_id, group)) => {
|
||||
Some((group_id, mut group)) => {
|
||||
// While a search is active, render every surviving group
|
||||
// expanded so its matches are visible without a click. This
|
||||
// only mutates the local clone — the stored `collapsed` flag is
|
||||
// untouched, so clearing the query restores the real state.
|
||||
if !query.is_empty() {
|
||||
group.collapsed = false;
|
||||
}
|
||||
// Members are a contiguous subslice of `visible_tabs`.
|
||||
let run_len = visible_tabs[i..]
|
||||
.iter()
|
||||
@@ -2737,6 +2763,7 @@ fn render_tab_group_header_icon_button(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_grouped_tabs_header(
|
||||
group: &TabGroup,
|
||||
search_query: &str,
|
||||
member_count: usize,
|
||||
mouse_states: &TabGroupMouseStates,
|
||||
is_collapsed: bool,
|
||||
@@ -2745,9 +2772,9 @@ fn render_grouped_tabs_header(
|
||||
is_being_renamed: bool,
|
||||
rename_editor: Option<&ViewHandle<EditorView>>,
|
||||
collapsed_member_kinds: Option<&[SummaryPaneKind]>,
|
||||
app: &AppContext,
|
||||
ctx: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let appearance = Appearance::as_ref(ctx);
|
||||
let theme = appearance.theme();
|
||||
let font_family = appearance.ui_font_family();
|
||||
let main_text_color = theme.main_text_color(theme.background());
|
||||
@@ -2785,15 +2812,17 @@ fn render_grouped_tabs_header(
|
||||
|
||||
let title_element: Box<dyn Element> =
|
||||
if let Some(editor) = rename_editor.filter(|_| is_being_renamed) {
|
||||
render_inline_tab_rename_editor(editor, appearance, app)
|
||||
render_inline_tab_rename_editor(editor, appearance, ctx)
|
||||
} else {
|
||||
let title_text = group
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "New Group".to_string());
|
||||
let title_text = group_display_name(group);
|
||||
let highlight_indices = group_name_highlight_indices(&title_text, search_query);
|
||||
Text::new_inline(title_text, font_family, 12.)
|
||||
.with_clip(ClipConfig::ellipsis())
|
||||
.with_color(main_text_color.into())
|
||||
.with_single_highlight(
|
||||
Highlight::new().with_properties(Properties::default().weight(Weight::Bold)),
|
||||
highlight_indices,
|
||||
)
|
||||
.finish()
|
||||
};
|
||||
let subtitle_text = if member_count == 1 {
|
||||
@@ -3012,6 +3041,7 @@ fn render_grouped_tab_container(
|
||||
is_collapsed.then(|| workspace.compute_group_member_kinds(group.id, app));
|
||||
let header = render_grouped_tabs_header(
|
||||
&group,
|
||||
&state.search_query,
|
||||
member_count,
|
||||
&mouse_states,
|
||||
is_collapsed,
|
||||
@@ -3908,6 +3938,102 @@ fn uses_outer_group_container(display_granularity: VerticalTabsDisplayGranularit
|
||||
matches!(display_granularity, VerticalTabsDisplayGranularity::Panes)
|
||||
}
|
||||
|
||||
/// Header text for a tab group the user has never named.
|
||||
const UNTITLED_GROUP_NAME: &str = "New Group";
|
||||
|
||||
/// The group title as displayed in the panel header, including the fallback
|
||||
/// used for a group the user has never named. Search matches against this so a
|
||||
/// query matches what is actually on screen.
|
||||
fn group_display_name(group: &TabGroup) -> String {
|
||||
group
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| UNTITLED_GROUP_NAME.to_string())
|
||||
}
|
||||
|
||||
fn group_name_highlight_indices(text: &str, query: &str) -> Vec<usize> {
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let text_lower = text.to_lowercase();
|
||||
let query_lower = query.to_lowercase();
|
||||
let matches: Vec<_> = text_lower
|
||||
.char_indices()
|
||||
.filter(|(start, _)| text_lower[*start..].starts_with(&query_lower))
|
||||
.map(|(start, _)| start..start + query_lower.len())
|
||||
.collect();
|
||||
let mut indices = Vec::new();
|
||||
let mut offset = 0;
|
||||
// Lowercasing can expand a character, so match bytes must map back to original char indices.
|
||||
for (index, character) in text.chars().enumerate() {
|
||||
let end = offset + character.to_lowercase().map(char::len_utf8).sum::<usize>();
|
||||
if matches
|
||||
.iter()
|
||||
.any(|range| range.start < end && offset < range.end)
|
||||
{
|
||||
indices.push(index);
|
||||
}
|
||||
offset = end;
|
||||
}
|
||||
indices
|
||||
}
|
||||
|
||||
/// Returns the ids of tab groups whose displayed name contains `query_lower`.
|
||||
fn matched_group_ids(
|
||||
tab_groups: &HashMap<TabGroupId, TabGroup>,
|
||||
query_lower: &str,
|
||||
) -> HashSet<TabGroupId> {
|
||||
tab_groups
|
||||
.iter()
|
||||
.filter(|(_, group)| {
|
||||
group_display_name(group)
|
||||
.to_lowercase()
|
||||
.contains(query_lower)
|
||||
})
|
||||
.map(|(group_id, _)| *group_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether a tab is admitted by its group's name matching the query, rather
|
||||
/// than by its own text. Ungrouped tabs are never admitted this way.
|
||||
fn tab_admitted_by_group_name(
|
||||
group_id: Option<TabGroupId>,
|
||||
matched_groups: &HashSet<TabGroupId>,
|
||||
) -> bool {
|
||||
group_id.is_some_and(|id| matched_groups.contains(&id))
|
||||
}
|
||||
|
||||
/// Merges text matches with members of name-matched tab groups while preserving tab order.
|
||||
/// Members admitted by group name use `None` so the full tab is shown.
|
||||
fn merge_group_name_matches(
|
||||
tab_group_ids: &[Option<TabGroupId>],
|
||||
matched_groups: &HashSet<TabGroupId>,
|
||||
own_matches: Vec<(usize, Option<Vec<PaneId>>)>,
|
||||
) -> Vec<(usize, Option<Vec<PaneId>>)> {
|
||||
if matched_groups.is_empty() {
|
||||
return own_matches;
|
||||
}
|
||||
|
||||
let mut merged: Vec<(usize, Option<Vec<PaneId>>)> = Vec::with_capacity(own_matches.len());
|
||||
let mut own_matches = own_matches.into_iter().peekable();
|
||||
|
||||
for (tab_index, group_id) in tab_group_ids.iter().enumerate() {
|
||||
let in_matched_group = tab_admitted_by_group_name(*group_id, matched_groups);
|
||||
let own_match = own_matches.next_if(|(index, _)| *index == tab_index);
|
||||
|
||||
match (in_matched_group, own_match) {
|
||||
// The group name matched, so the whole tab is shown regardless of
|
||||
// whether it also matched on its own text.
|
||||
(true, _) => merged.push((tab_index, None)),
|
||||
(false, Some(own_match)) => merged.push(own_match),
|
||||
(false, None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
merged
|
||||
}
|
||||
|
||||
fn search_fragments_contain_query(fragments: &[String], query_lower: &str) -> bool {
|
||||
fragments
|
||||
.iter()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use galaxyui::elements::PositionedElementOffsetBounds;
|
||||
@@ -8,13 +9,14 @@ use pathfinder_geometry::vector::Vector2F;
|
||||
use super::{
|
||||
branch_label_display, coalesce_summary_branch_entries, code_detail_kind_label,
|
||||
compact_branch_subtitle_display, detail_sidecar_width_and_bounds,
|
||||
detail_target_for_hovered_row, non_terminal_search_text_fragments,
|
||||
detail_target_for_hovered_row, group_display_name, group_name_highlight_indices,
|
||||
matched_group_ids, merge_group_name_matches, non_terminal_search_text_fragments,
|
||||
pane_ids_for_display_granularity, pane_search_text_fragments, preferred_agent_tab_titles,
|
||||
push_normalized_unique_summary_label, search_fragments_contain_query,
|
||||
select_summary_pane_kind_icons, should_keep_detail_sidecar_visible_for_mouse_position,
|
||||
sort_summary_primary_labels_status_first, summary_overflow_count,
|
||||
summary_search_text_fragments, terminal_kind_badge_label, terminal_primary_line_data,
|
||||
terminal_pull_request_badge_label, terminal_search_text_fragments,
|
||||
summary_search_text_fragments, tab_admitted_by_group_name, terminal_kind_badge_label,
|
||||
terminal_primary_line_data, terminal_pull_request_badge_label, terminal_search_text_fragments,
|
||||
terminal_title_fallback_font, uses_outer_group_container, visible_pane_ids_for_detail_target,
|
||||
vtab_diff_stats_text, AgentTabTextPreference, SummaryPaneKind, SummaryPaneKindIcons,
|
||||
TerminalAgentText, TerminalPrimaryLineData, TerminalPrimaryLineFont, VerticalTabsDetailTarget,
|
||||
@@ -27,6 +29,7 @@ use crate::pane_group::pane::IPaneType;
|
||||
use crate::pane_group::{PaneId, TerminalPaneId};
|
||||
use crate::safe_triangle::SafeTriangle;
|
||||
use crate::terminal::CLIAgent;
|
||||
use crate::workspace::tab_group::{TabGroup, TabGroupId};
|
||||
use crate::workspace::tab_settings::VerticalTabsDisplayGranularity;
|
||||
|
||||
fn label(text: &str) -> VerticalTabsSummaryPrimaryLabel {
|
||||
@@ -1151,3 +1154,236 @@ fn summary_search_fragments_include_hidden_overflow_values() {
|
||||
assert!(search_fragments_contain_query(&fragments, "+2"));
|
||||
assert!(search_fragments_contain_query(&fragments, "-3"));
|
||||
}
|
||||
|
||||
fn tab_group(name: Option<&str>) -> TabGroup {
|
||||
TabGroup {
|
||||
name: name.map(str::to_string),
|
||||
..TabGroup::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_display_name_uses_the_group_name_when_set() {
|
||||
assert_eq!(group_display_name(&tab_group(Some("backend"))), "backend");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_display_name_falls_back_to_the_untitled_header_text() {
|
||||
assert_eq!(group_display_name(&tab_group(None)), "New Group");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_match_includes_members_that_did_not_match_on_their_own() {
|
||||
let group = TabGroupId::new();
|
||||
let tab_group_ids = vec![Some(group), Some(group), None];
|
||||
|
||||
let merged = merge_group_name_matches(
|
||||
&tab_group_ids,
|
||||
&HashSet::from([group]),
|
||||
// Only the second tab matched on its own title.
|
||||
vec![(1, None)],
|
||||
);
|
||||
|
||||
assert_eq!(merged, vec![(0, None), (1, None)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_match_keeps_results_ordered_by_tab_index() {
|
||||
let group = TabGroupId::new();
|
||||
// Ungrouped tabs on either side of the group.
|
||||
let tab_group_ids = vec![None, Some(group), Some(group), None];
|
||||
|
||||
let merged = merge_group_name_matches(
|
||||
&tab_group_ids,
|
||||
&HashSet::from([group]),
|
||||
vec![(0, None), (3, None)],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merged,
|
||||
vec![(0, None), (1, None), (2, None), (3, None)],
|
||||
"group members must be interleaved in tab order, not appended"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_match_upgrades_a_pane_filtered_member_to_all_panes() {
|
||||
let group = TabGroupId::new();
|
||||
let tab_group_ids = vec![Some(group)];
|
||||
|
||||
let merged = merge_group_name_matches(
|
||||
&tab_group_ids,
|
||||
&HashSet::from([group]),
|
||||
// The tab matched on one pane row only.
|
||||
vec![(0, Some(vec![pane_id()]))],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merged,
|
||||
vec![(0, None)],
|
||||
"a group-name match shows the whole tab, not a pane-filtered slice"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_match_for_a_group_with_no_members_changes_nothing() {
|
||||
let empty_group = TabGroupId::new();
|
||||
let tab_group_ids = vec![None, None];
|
||||
|
||||
let merged = merge_group_name_matches(
|
||||
&tab_group_ids,
|
||||
&HashSet::from([empty_group]),
|
||||
vec![(1, None)],
|
||||
);
|
||||
|
||||
assert_eq!(merged, vec![(1, None)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_match_leaves_tabs_outside_the_group_filtered() {
|
||||
let matched = TabGroupId::new();
|
||||
let other = TabGroupId::new();
|
||||
let tab_group_ids = vec![Some(matched), Some(other), None];
|
||||
|
||||
let merged = merge_group_name_matches(&tab_group_ids, &HashSet::from([matched]), vec![]);
|
||||
|
||||
assert_eq!(
|
||||
merged,
|
||||
vec![(0, None)],
|
||||
"only members of the name-matched group are force-included"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_group_name_match_leaves_the_filtered_tabs_untouched() {
|
||||
let group = TabGroupId::new();
|
||||
let tab_group_ids = vec![Some(group), None];
|
||||
let own_matches = vec![(1, Some(vec![pane_id()]))];
|
||||
|
||||
let merged = merge_group_name_matches(&tab_group_ids, &HashSet::new(), own_matches.clone());
|
||||
|
||||
assert_eq!(merged, own_matches);
|
||||
}
|
||||
|
||||
fn tab_groups_map(groups: Vec<TabGroup>) -> HashMap<TabGroupId, TabGroup> {
|
||||
groups.into_iter().map(|group| (group.id, group)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matched_group_ids_selects_groups_whose_displayed_name_contains_the_query() {
|
||||
let backend = tab_group(Some("backend"));
|
||||
let frontend = tab_group(Some("frontend"));
|
||||
let backend_id = backend.id;
|
||||
let groups = tab_groups_map(vec![backend, frontend]);
|
||||
|
||||
assert_eq!(
|
||||
matched_group_ids(&groups, "backend"),
|
||||
HashSet::from([backend_id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matched_group_ids_is_case_insensitive() {
|
||||
let group = tab_group(Some("Backend Services"));
|
||||
let id = group.id;
|
||||
let groups = tab_groups_map(vec![group]);
|
||||
|
||||
assert_eq!(matched_group_ids(&groups, "backend"), HashSet::from([id]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matched_group_ids_matches_the_untitled_group_placeholder() {
|
||||
let group = tab_group(None);
|
||||
let id = group.id;
|
||||
let groups = tab_groups_map(vec![group]);
|
||||
|
||||
assert_eq!(matched_group_ids(&groups, "new group"), HashSet::from([id]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matched_group_ids_is_empty_when_nothing_matches() {
|
||||
let groups = tab_groups_map(vec![tab_group(Some("backend"))]);
|
||||
|
||||
assert!(matched_group_ids(&groups, "nomatch").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_is_admitted_by_group_name_only_when_its_group_matched() {
|
||||
let matched = TabGroupId::new();
|
||||
let other = TabGroupId::new();
|
||||
let matched_groups = HashSet::from([matched]);
|
||||
|
||||
assert!(tab_admitted_by_group_name(Some(matched), &matched_groups));
|
||||
assert!(!tab_admitted_by_group_name(Some(other), &matched_groups));
|
||||
assert!(
|
||||
!tab_admitted_by_group_name(None, &matched_groups),
|
||||
"an ungrouped tab is never admitted by a group-name match"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_case_insensitive_substrings() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices("My Backend", "BACK"),
|
||||
vec![3, 4, 5, 6]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_repeated_matches() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices("dev / DEV", "dev"),
|
||||
vec![0, 1, 2, 6, 7, 8]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_overlapping_matches() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices("banana", "ana"),
|
||||
vec![1, 2, 3, 4, 5]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_use_character_indices_after_multibyte_text() {
|
||||
assert_eq!(group_name_highlight_indices("🚀 Équipe", "éq"), vec![2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_map_expanding_lowercase_to_original_characters() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices("İ Backend", "back"),
|
||||
vec![2, 3, 4, 5]
|
||||
);
|
||||
assert_eq!(group_name_highlight_indices("İ Backend", "i"), vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_preserve_contextual_lowercasing() {
|
||||
assert_eq!(group_name_highlight_indices("ΟΣ", "ος"), vec![0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_clear_for_empty_query() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices("Backend", ""),
|
||||
Vec::<usize>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_do_not_use_fuzzy_matching() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices("Backend", "bkd"),
|
||||
Vec::<usize>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_name_highlights_match_untitled_fallback() {
|
||||
assert_eq!(
|
||||
group_name_highlight_indices(&group_display_name(&tab_group(None)), "group"),
|
||||
vec![4, 5, 6, 7, 8]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user