use std::ops::Range;
use gpui::{
div, prelude::FluentBuilder, rems, App, HighlightStyle, IntoElement, ParentElement, RenderOnce,
SharedString, StyleRefinement, Styled, StyledText, Window,
};
use crate::{ActiveTheme, StyledExt};
const MASKED: &'static str = "•";
#[derive(Clone)]
pub enum HighlightsMatch {
Prefix(SharedString),
Full(SharedString),
}
impl HighlightsMatch {
pub fn as_str(&self) -> &str {
match self {
Self::Prefix(s) => s.as_str(),
Self::Full(s) => s.as_str(),
}
}
#[inline]
pub fn is_prefix(&self) -> bool {
matches!(self, Self::Prefix(_))
}
}
impl From<&str> for HighlightsMatch {
fn from(value: &str) -> Self {
Self::Full(value.to_string().into())
}
}
impl From<String> for HighlightsMatch {
fn from(value: String) -> Self {
Self::Full(value.into())
}
}
impl From<SharedString> for HighlightsMatch {
fn from(value: SharedString) -> Self {
Self::Full(value)
}
}
#[derive(IntoElement)]
pub struct Label {
style: StyleRefinement,
label: SharedString,
secondary: Option<SharedString>,
masked: bool,
highlights_text: Option<HighlightsMatch>,
}
impl Label {
pub fn new(label: impl Into<SharedString>) -> Self {
let label: SharedString = label.into();
Self {
style: Default::default(),
label,
secondary: None,
masked: false,
highlights_text: None,
}
}
pub fn secondary(mut self, secondary: impl Into<SharedString>) -> Self {
self.secondary = Some(secondary.into());
self
}
pub fn masked(mut self, masked: bool) -> Self {
self.masked = masked;
self
}
pub fn highlights(mut self, text: impl Into<HighlightsMatch>) -> Self {
self.highlights_text = Some(text.into());
self
}
fn full_text(&self) -> SharedString {
match &self.secondary {
Some(secondary) => format!("{} {}", self.label, secondary).into(),
None => self.label.clone(),
}
}
fn highlight_ranges(&self, total_length: usize) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
let full_text = self.full_text();
if self.secondary.is_some() {
ranges.push(0..self.label.len());
ranges.push(self.label.len()..total_length);
}
if let Some(matched) = &self.highlights_text {
let matched_str = matched.as_str();
if !matched_str.is_empty() {
let search_lower = matched_str.to_lowercase();
let full_text_lower = full_text.to_lowercase();
if matched.is_prefix() {
if full_text_lower.starts_with(&search_lower) {
ranges.push(0..matched_str.len());
}
} else {
let mut search_start = 0;
while let Some(pos) = full_text_lower[search_start..].find(&search_lower) {
let match_start = search_start + pos;
let match_end = match_start + matched_str.len();
if match_end <= full_text.len() {
ranges.push(match_start..match_end);
}
search_start = match_start + 1;
while !full_text.is_char_boundary(search_start)
&& search_start < full_text.len()
{
search_start += 1;
}
if search_start >= full_text.len() {
break;
}
}
}
}
}
ranges
}
fn measure_highlights(
&self,
length: usize,
cx: &mut App,
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
let ranges = self.highlight_ranges(length);
if ranges.is_empty() {
return None;
}
let mut highlights = Vec::new();
let mut highlight_ranges_added = 0;
if self.secondary.is_some() {
highlights.push((ranges[0].clone(), HighlightStyle::default()));
highlights.push((
ranges[1].clone(),
HighlightStyle {
color: Some(cx.theme().muted_foreground),
..Default::default()
},
));
highlight_ranges_added = 2;
}
for range in ranges.iter().skip(highlight_ranges_added) {
highlights.push((
range.clone(),
HighlightStyle {
color: Some(cx.theme().blue),
..Default::default()
},
));
}
Some(gpui::combine_highlights(vec![], highlights).collect())
}
}
impl Styled for Label {
fn style(&mut self) -> &mut gpui::StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Label {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let mut text = self.full_text();
let chars_count = text.chars().count();
if self.masked {
text = SharedString::from(MASKED.repeat(chars_count))
};
let highlights = self.measure_highlights(text.len(), cx);
div()
.line_height(rems(1.25))
.text_color(cx.theme().foreground)
.refine_style(&self.style)
.child(
StyledText::new(&text).when_some(highlights, |this, hl| this.with_highlights(hl)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highlight_ranges() {
let label = Label::new("Hello World");
let result = label.highlight_ranges("Hello World".len());
assert_eq!(result, Vec::<Range<usize>>::new());
let label = Label::new("Hello").secondary("World");
let total_length = "Hello World".len();
let result = label.highlight_ranges(total_length);
assert_eq!(result.len(), 2);
assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11);
let label = Label::new("Hello World").highlights("WORLD");
let result = label.highlight_ranges("Hello World".len());
assert_eq!(result.len(), 1);
assert_eq!(result[0], 6..11);
let label = Label::new("Hello Hello Hello").highlights("Hello");
let result = label.highlight_ranges("Hello Hello Hello".len());
assert_eq!(result.len(), 3);
assert_eq!(result[0], 0..5); assert_eq!(result[1], 6..11); assert_eq!(result[2], 12..17);
let label = Label::new("Hello World").highlights("xyz");
let result = label.highlight_ranges("Hello World".len());
assert_eq!(result, Vec::<Range<usize>>::new());
let label = Label::new("Hello World").highlights("");
let result = label.highlight_ranges("Hello World".len());
assert_eq!(result, Vec::<Range<usize>>::new());
let label = Label::new("Hello").secondary("World").highlights("llo");
let total_length = "Hello World".len();
let result = label.highlight_ranges(total_length);
assert_eq!(result.len(), 3);
assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 2..5);
let label = Label::new("Hello").secondary("World").highlights("World");
let total_length = "Hello World".len();
let result = label.highlight_ranges(total_length);
assert_eq!(result.len(), 3);
assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 6..11);
let label = Label::new("Hello").secondary("World").highlights("o W");
let total_length = "Hello World".len();
let result = label.highlight_ranges(total_length);
assert_eq!(result.len(), 3);
assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 4..7);
let label = Label::new("aaaa").highlights("aa");
let result = label.highlight_ranges("aaaa".len());
assert!(result.len() >= 2);
assert_eq!(result[0], 0..2); assert_eq!(result[1], 1..3);
let label = Label::new("你好世界,Hello World").highlights("世界");
let result = label.highlight_ranges("你好世界,Hello World".len());
assert_eq!(result.len(), 1);
let text = "你好世界,Hello World";
let start = text.find("世界").unwrap();
let end = start + "世界".len();
assert_eq!(result[0], start..end);
}
#[test]
fn test_highlight_ranges_prefix() {
let label = Label::new("aaaa").highlights(HighlightsMatch::Prefix("aa".into()));
let result = label.highlight_ranges("aaaa".len());
assert_eq!(result.len(), 1);
assert_eq!(result[0], 0..2);
let label_full =
Label::new("Hello Hello").highlights(HighlightsMatch::Full("Hello".into()));
let result_full = label_full.highlight_ranges("Hello Hello".len());
assert_eq!(result_full.len(), 2);
let label_prefix =
Label::new("Hello Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
let result_prefix = label_prefix.highlight_ranges("Hello Hello".len());
assert_eq!(result_prefix.len(), 1); assert_eq!(result_prefix[0], 0..5);
let label =
Label::new("Hello hello HELLO").highlights(HighlightsMatch::Prefix("hello".into()));
let result = label.highlight_ranges("Hello hello HELLO".len());
assert_eq!(result.len(), 1);
assert_eq!(result[0], 0..5);
let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("xyz".into()));
let result = label.highlight_ranges("Hello World".len());
assert_eq!(result.len(), 0);
let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("".into()));
let result = label.highlight_ranges("Hello World".len());
assert_eq!(result.len(), 0);
let label = Label::new("Hello")
.secondary("Hello World")
.highlights(HighlightsMatch::Prefix("Hello".into()));
let total_length = "Hello Hello World".len();
let result = label.highlight_ranges(total_length);
assert_eq!(result.len(), 3); assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..17); assert_eq!(result[2], 0..5);
let label = Label::new("abc")
.secondary("def abc def")
.highlights(HighlightsMatch::Prefix("abc".into()));
let total_length = "abc def abc def".len();
let result = label.highlight_ranges(total_length);
assert_eq!(result.len(), 3); assert_eq!(result[0], 0..3); assert_eq!(result[1], 3..15); assert_eq!(result[2], 0..3);
let label = Label::new("你好世界你好").highlights(HighlightsMatch::Prefix("你好".into()));
let result = label.highlight_ranges("你好世界你好".len());
assert_eq!(result.len(), 1);
assert_eq!(result[0], 0..6);
let label = Label::new("abababab").highlights(HighlightsMatch::Prefix("abab".into()));
let result = label.highlight_ranges("abababab".len());
assert_eq!(result.len(), 1);
assert_eq!(result[0], 0..4);
let label =
Label::new("xyz Hello abc Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
let result = label.highlight_ranges("xyz Hello abc Hello".len());
assert_eq!(result.len(), 0);
let prefix_match = HighlightsMatch::Prefix("test".into());
let full_match = HighlightsMatch::Full("test".into());
assert!(prefix_match.is_prefix());
assert!(!full_match.is_prefix());
let prefix_match = HighlightsMatch::Prefix("test".into());
assert_eq!(prefix_match.as_str(), "test");
}
}