diff --git a/Cargo.lock b/Cargo.lock index 0a9921b248..fc490089e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10487,6 +10487,7 @@ dependencies = [ "take-until", "tempfile", "tendril", + "unicase", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 57d2a0b3fb..2757fed05d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -294,6 +294,7 @@ tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", r tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" } tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" } unindent = "0.1.7" +unicase = "2.6" url = "2.2" uuid = { version = "1.1.2", features = ["v4"] } wasmtime = "18.0" diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 639600876a..093d47e124 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -26,7 +26,7 @@ serde_json.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true -unicase = "2.6" +unicase.workspace = true util.workspace = true client.workspace = true workspace.workspace = true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e531191a54..b9a5422227 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -27,7 +27,7 @@ use std::{cmp::Ordering, ffi::OsStr, ops::Range, path::Path, sync::Arc}; use theme::ThemeSettings; use ui::{prelude::*, v_flex, ContextMenu, Icon, KeyBinding, Label, ListItem}; use unicase::UniCase; -use util::{maybe, ResultExt, TryFutureExt}; +use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::DetachAndPromptErr, @@ -1498,35 +1498,6 @@ impl ProjectPanel { } } -#[derive(Debug, PartialEq)] -struct NumericPrefixWithSuffix<'a>(i32, &'a str); - -impl<'a> NumericPrefixWithSuffix<'a> { - fn from_str(str: &'a str) -> Option { - let mut chars = str.chars(); - let prefix: String = chars.by_ref().take_while(|c| c.is_digit(10)).collect(); - let remainder = chars.as_str(); - - match prefix.parse::() { - Ok(prefix) => Some(NumericPrefixWithSuffix(prefix, remainder)), - Err(_) => None, - } - } -} - -impl<'a> PartialOrd for NumericPrefixWithSuffix<'a> { - fn partial_cmp(&self, other: &Self) -> Option { - let NumericPrefixWithSuffix(num_a, remainder_a) = self; - let NumericPrefixWithSuffix(num_b, remainder_b) = other; - - Some( - num_a - .cmp(&num_b) - .then_with(|| UniCase::new(remainder_a).cmp(&UniCase::new(remainder_b))), - ) - } -} - impl Render for ProjectPanel { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let has_worktree = self.visible_entries.len() != 0; diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index fc06ebeb4a..9725320507 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -31,6 +31,7 @@ serde_json.workspace = true smol.workspace = true take-until = "0.2.0" tempfile = { workspace = true, optional = true } +unicase.workspace = true url.workspace = true [target.'cfg(windows)'.dependencies] diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 8f7a6fffbd..ae3ab3ee5a 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -22,6 +22,7 @@ use std::{ task::{Context, Poll}, time::Instant, }; +use unicase::UniCase; pub use take_until::*; @@ -487,6 +488,43 @@ impl RangeExt for RangeInclusive { } } +/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one, +/// case-insensitive. +/// +/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc` +/// into `1-abc, 2, 10, 11-def, .., 21-abc` +#[derive(Debug, PartialEq, Eq)] +pub struct NumericPrefixWithSuffix<'a>(i32, &'a str); + +impl<'a> NumericPrefixWithSuffix<'a> { + pub fn from_str(str: &'a str) -> Option { + let mut chars = str.chars(); + let prefix: String = chars.by_ref().take_while(|c| c.is_digit(10)).collect(); + let remainder = chars.as_str(); + + match prefix.parse::() { + Ok(prefix) => Some(NumericPrefixWithSuffix(prefix, remainder)), + Err(_) => None, + } + } +} + +impl Ord for NumericPrefixWithSuffix<'_> { + fn cmp(&self, other: &Self) -> Ordering { + let NumericPrefixWithSuffix(num_a, remainder_a) = self; + let NumericPrefixWithSuffix(num_b, remainder_b) = other; + num_a + .cmp(&num_b) + .then_with(|| UniCase::new(remainder_a).cmp(&UniCase::new(remainder_b))) + } +} + +impl<'a> PartialOrd for NumericPrefixWithSuffix<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -526,4 +564,23 @@ mod tests { assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); } + + #[test] + fn test_numeric_prefix_with_suffix() { + let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; + sorted.sort_by_key(|s| { + NumericPrefixWithSuffix::from_str(s).unwrap_or_else(|| { + panic!("Cannot convert string `{s}` into NumericPrefixWithSuffix") + }) + }); + assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); + + for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] { + assert_eq!( + NumericPrefixWithSuffix::from_str(numeric_prefix_less), + None, + "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" + ) + } + } }