Make search ranking and algorithm more extensible

pull/585/head
Noah Mayr 2 years ago
parent 16747e7925
commit 8ca5ba0ac7

@ -19,6 +19,8 @@ pub use crate::msg::out::MsgOut;
pub use crate::node::Node; pub use crate::node::Node;
pub use crate::node::ResolvedNode; pub use crate::node::ResolvedNode;
pub use crate::pipe::Pipe; pub use crate::pipe::Pipe;
use crate::search::SearchAlgorithm;
use crate::search::SearchOrder;
use crate::ui::Layout; use crate::ui::Layout;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
@ -528,9 +530,9 @@ impl App {
ClearNodeSorters => self.clear_node_sorters(), ClearNodeSorters => self.clear_node_sorters(),
SearchFuzzy(p) => self.search_fuzzy(p), SearchFuzzy(p) => self.search_fuzzy(p),
SearchFuzzyFromInput => self.search_fuzzy_from_input(), SearchFuzzyFromInput => self.search_fuzzy_from_input(),
ToggleRankedSearch => self.toggle_ranked_search(), CycleSearchOrder => self.cycle_search_order(),
EnableRankedSearch => self.set_ranked_search(true), SetSearchOrder(r) => self.set_search_order(r),
DisableRankedSearch => self.set_ranked_search(false), SetSearchAlgorithm(a) => self.set_search_algorithm(a),
AcceptSearch => self.accept_search(), AcceptSearch => self.accept_search(),
CancelSearch => self.cancel_search(), CancelSearch => self.cancel_search(),
EnableMouse => self.enable_mouse(), EnableMouse => self.enable_mouse(),
@ -1615,16 +1617,18 @@ impl App {
} }
pub fn search_fuzzy(mut self, pattern: String) -> Result<Self> { pub fn search_fuzzy(mut self, pattern: String) -> Result<Self> {
let (rf, ranked) = self let rf = self
.explorer_config .explorer_config
.searcher .searcher
.as_ref() .as_ref()
.map(|s| (s.recoverable_focus.clone(), s.ranked)) .map(|s| s.recoverable_focus.clone())
.unwrap_or_else(|| { .unwrap_or_else(|| self.focused_node().map(|n| n.absolute_path.clone()));
(self.focused_node().map(|n| n.absolute_path.clone()), true)
});
self.explorer_config.searcher = Some(NodeSearcher::new(pattern, rf, ranked)); self.explorer_config.searcher = Some(NodeSearcher::new(
pattern,
rf,
self.config.general.search.clone(),
));
Ok(self) Ok(self)
} }
@ -1636,19 +1640,30 @@ impl App {
} }
} }
fn toggle_ranked_search(mut self) -> Result<Self> { fn cycle_search_order(mut self) -> Result<Self> {
self.explorer_config.searcher = self self.explorer_config.searcher.as_mut().map(|s| {
.explorer_config s.config.order = match s.config.order {
.searcher SearchOrder::Ranked => SearchOrder::Sorted,
.map(|searcher| searcher.toggle_ranked()); SearchOrder::Sorted => SearchOrder::Ranked,
};
s
});
Ok(self) Ok(self)
} }
fn set_ranked_search(mut self, ranking: bool) -> Result<Self> { fn set_search_order(mut self, order: SearchOrder) -> Result<Self> {
self.explorer_config.searcher = self self.explorer_config.searcher.as_mut().map(|s| {
.explorer_config s.config.order = order;
.searcher s
.map(|searcher| searcher.with_ranked(ranking)); });
Ok(self)
}
fn set_search_algorithm(mut self, algorithm: SearchAlgorithm) -> Result<Self> {
self.explorer_config.searcher.as_mut().map(|s| {
s.config.algorithm = algorithm;
s
});
Ok(self) Ok(self)
} }

@ -4,6 +4,8 @@ use crate::app::NodeFilter;
use crate::app::NodeSorter; use crate::app::NodeSorter;
use crate::app::NodeSorterApplicable; use crate::app::NodeSorterApplicable;
use crate::node::Node; use crate::node::Node;
use crate::search::SearchAlgorithm;
use crate::search::SearchOrder;
use crate::ui::Border; use crate::ui::Border;
use crate::ui::BorderType; use crate::ui::BorderType;
use crate::ui::Constraint; use crate::ui::Constraint;
@ -236,6 +238,16 @@ pub struct SortAndFilterUi {
pub search_identifier: Option<UiElement>, pub search_identifier: Option<UiElement>,
} }
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchConfig {
#[serde(default)]
pub order: SearchOrder,
#[serde(default)]
pub algorithm: SearchAlgorithm,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct PanelUi { pub struct PanelUi {
@ -309,6 +321,9 @@ pub struct GeneralConfig {
#[serde(default)] #[serde(default)]
pub sort_and_filter_ui: SortAndFilterUi, pub sort_and_filter_ui: SortAndFilterUi,
#[serde(default)]
pub search: SearchConfig,
#[serde(default)] #[serde(default)]
pub panel_ui: PanelUi, pub panel_ui: PanelUi,

@ -1,37 +1,13 @@
use crate::app::{ use crate::app::{
DirectoryBuffer, ExplorerConfig, ExternalMsg, InternalMsg, MsgIn, Node, Task, DirectoryBuffer, ExplorerConfig, ExternalMsg, InternalMsg, MsgIn, Node, Task,
}; };
use crate::search::SearchOrder;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use lazy_static::lazy_static;
use skim::prelude::ExactOrFuzzyEngineFactory;
use skim::{MatchEngineFactory, SkimItem};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::thread; use std::thread;
lazy_static! {
static ref ENGINE_FACTORY: ExactOrFuzzyEngineFactory =
ExactOrFuzzyEngineFactory::builder().build();
}
struct PathItem {
path: String,
}
impl From<String> for PathItem {
fn from(value: String) -> Self {
Self { path: value }
}
}
impl SkimItem for PathItem {
fn text(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::from(&self.path)
}
}
pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result<Vec<Node>> { pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result<Vec<Node>> {
let dirs = fs::read_dir(parent)?; let dirs = fs::read_dir(parent)?;
let mut nodes = dirs let mut nodes = dirs
@ -47,23 +23,16 @@ pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result<Vec<Node>> {
.filter(|n| config.filter(n)) .filter(|n| config.filter(n))
.collect::<Vec<Node>>(); .collect::<Vec<Node>>();
nodes = if let Some((pattern, ranked)) = nodes = if let Some(searcher) = config.searcher.as_ref() {
config.searcher.as_ref().map(|s| (&s.pattern, &s.ranked)) let mut ranked_nodes =
{ searcher.config.algorithm.search(&searcher.pattern, nodes);
let engine = ENGINE_FACTORY.create_engine(pattern);
let mut ranked_nodes = nodes
.into_iter()
.filter_map(|n| {
let item = Arc::new(PathItem::from(n.relative_path.clone()));
engine.match_item(item).map(|res| (n, res.rank))
})
.collect::<Vec<(_, _)>>();
if *ranked { match searcher.config.order {
ranked_nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1)) SearchOrder::Ranked => ranked_nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1)),
} else { SearchOrder::Sorted => {
ranked_nodes.sort_by(|(a, _), (b, _)| config.sort(a, b)) ranked_nodes.sort_by(|(a, _), (b, _)| config.sort(a, b))
} }
};
ranked_nodes.into_iter().map(|(n, _)| n).collect::<Vec<_>>() ranked_nodes.into_iter().map(|(n, _)| n).collect::<Vec<_>>()
} else { } else {

@ -467,6 +467,14 @@ xplr.config.general.sort_and_filter_ui.search_identifier = {
style = {}, style = {},
} }
-- -- The configuration
-- --
-- -- Type: { algorithm = [SearchAlgorithm], order = [SearchOrder] }
xplr.config.general.search = {
algorithm = { Skim = "Fuzzy" },
order = "Ranked"
}
-- The content for panel title by default. -- The content for panel title by default.
-- --
-- Type: nullable string -- Type: nullable string
@ -2119,14 +2127,28 @@ xplr.config.modes.builtin.search = {
["ctrl-z"] = { ["ctrl-z"] = {
help = "toggle search ranking", help = "toggle search ranking",
messages = { messages = {
"ToggleRankedSearch", "CycleSearchOrder",
"ExplorePwdAsync",
},
},
["ctrl-r"] = {
help = "regex search",
messages = {
{ SetSearchAlgorithm = { Skim = "Regex" } },
"ExplorePwdAsync",
},
},
["ctrl-f"] = {
help = "fuzzy search",
messages = {
{ SetSearchAlgorithm = { Skim = "Fuzzy" } },
"ExplorePwdAsync", "ExplorePwdAsync",
}, },
}, },
["ctrl-s"] = { ["ctrl-s"] = {
help = "sort (disables ranking)", help = "sort (disables ranking)",
messages = { messages = {
"DisableRankedSearch", { SetSearchOrder = "Sorted" },
"ExplorePwdAsync", "ExplorePwdAsync",
{ SwitchModeBuiltinKeepingInputBuffer = "sort" }, { SwitchModeBuiltinKeepingInputBuffer = "sort" },
}, },

@ -17,6 +17,7 @@ pub mod permissions;
pub mod pipe; pub mod pipe;
pub mod pwd_watcher; pub mod pwd_watcher;
pub mod runner; pub mod runner;
pub mod search;
pub mod ui; pub mod ui;
pub mod yaml; pub mod yaml;

@ -1,4 +1,9 @@
use crate::{app::Node, input::InputOperation}; use crate::{
app::Node,
config::SearchConfig,
input::InputOperation,
search::{SearchAlgorithm, SearchOrder},
};
use indexmap::IndexSet; use indexmap::IndexSet;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -943,32 +948,32 @@ pub enum ExternalMsg {
/// - YAML: `SearchFuzzyFromInput` /// - YAML: `SearchFuzzyFromInput`
SearchFuzzyFromInput, SearchFuzzyFromInput,
/// Toggles sorting based on search match ranking or xplr default. /// Cycles through different search order modes.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely.
/// ///
/// Example: /// Example:
/// ///
/// - Lua: `"ToggleRankedSearch"` /// - Lua: `"CycleSearchOrder"`
/// - YAML: `ToggleRankedSearch` /// - YAML: `CycleSearchOrder`
ToggleRankedSearch, CycleSearchOrder,
/// Enables sorting based on search match ranking instead of xplr default. /// Sets how search results should be ordered.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely.
/// ///
/// Example: /// Example:
/// ///
/// - Lua: `"EnableRankedSearch"` /// - Lua: `{ SetSearchOrder = "Ranked" }`
/// - YAML: `EnableRankedSearch` /// - YAML: `SetSearchOrder: Sorted`
EnableRankedSearch, SetSearchOrder(SearchOrder),
/// Disables sorting based on search match ranking and uses xplr default. /// Sets the search algorithm.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely.
/// ///
/// Example: /// Example:
/// ///
/// - Lua: `"DisableRankedSearch"` /// - Lua: `{ SetSearchAlgorithm = { Skim = "Fuzzy" } }`
/// - YAML: `DisableRankedSearch` /// - YAML: `SetSearchAlgorithm: {Skim: Regex}`
DisableRankedSearch, SetSearchAlgorithm(SearchAlgorithm),
/// Accepts the search by keeping the latest focus while in search mode. /// Accepts the search by keeping the latest focus while in search mode.
/// Automatically calls `ExplorePwd`. /// Automatically calls `ExplorePwd`.
@ -1684,35 +1689,20 @@ pub struct NodeSearcher {
#[serde(default)] #[serde(default)]
pub recoverable_focus: Option<String>, pub recoverable_focus: Option<String>,
pub ranked: bool, #[serde(default)]
pub config: SearchConfig,
} }
impl NodeSearcher { impl NodeSearcher {
pub fn new( pub fn new(
pattern: String, pattern: String,
recoverable_focus: Option<String>, recoverable_focus: Option<String>,
ranked: bool, config: SearchConfig,
) -> Self { ) -> Self {
Self { Self {
pattern, pattern,
recoverable_focus, recoverable_focus,
ranked, config,
}
}
pub fn with_ranked(self, ranked: bool) -> Self {
Self {
pattern: self.pattern,
recoverable_focus: self.recoverable_focus,
ranked,
}
}
pub fn toggle_ranked(self) -> Self {
Self {
pattern: self.pattern,
recoverable_focus: self.recoverable_focus,
ranked: !self.ranked,
} }
} }
} }

@ -0,0 +1,105 @@
use std::sync::Arc;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory};
use skim::{MatchEngine, MatchEngineFactory, SkimItem};
use crate::node::Node;
lazy_static! {
static ref FUZZY_FACTORY: ExactOrFuzzyEngineFactory =
ExactOrFuzzyEngineFactory::builder().build();
static ref REGEX_FACTORY: RegexEngineFactory = RegexEngineFactory::builder().build();
}
struct PathItem {
path: String,
}
impl From<String> for PathItem {
fn from(value: String) -> Self {
Self { path: value }
}
}
impl SkimItem for PathItem {
fn text(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::from(&self.path)
}
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SkimAlgorithm {
#[default]
Fuzzy,
Regex,
}
impl SkimAlgorithm {
fn engine(&self, pattern: &str) -> Box<dyn MatchEngine> {
match self {
Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern),
Self::Regex => REGEX_FACTORY.create_engine(pattern),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SearchAlgorithm {
Skim(SkimAlgorithm),
}
impl Default for SearchAlgorithm {
fn default() -> Self {
Self::Skim(SkimAlgorithm::default())
}
}
impl SearchAlgorithm {
pub fn search(&self, pattern: &str, nodes: Vec<Node>) -> Vec<(Node, [i32; 4])> {
match self {
Self::Skim(algorithm) => {
let engine = algorithm.engine(pattern);
nodes
.into_iter()
.filter_map(|n| {
let item = Arc::new(PathItem::from(n.relative_path.clone()));
engine.match_item(item).map(|res| (n, res.rank))
})
.collect::<Vec<(_, _)>>()
}
}
}
pub fn label(&self) -> String {
match self {
SearchAlgorithm::Skim(algorithm) => {
let kind = match algorithm {
SkimAlgorithm::Fuzzy => "fuzzy",
SkimAlgorithm::Regex => "regex",
};
format!("skim ({kind})")
}
}
}
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SearchOrder {
#[default]
Ranked,
Sorted,
}
impl SearchOrder {
pub fn label(&self) -> String {
match self {
SearchOrder::Ranked => "rank".to_string(),
SearchOrder::Sorted => "sort".to_string(),
}
}
}

@ -3,6 +3,7 @@ use crate::app::{Node, ResolvedNode};
use crate::config::PanelUiConfig; use crate::config::PanelUiConfig;
use crate::lua; use crate::lua;
use crate::permissions::Permissions; use crate::permissions::Permissions;
use crate::search::SearchOrder;
use crate::{app, path}; use crate::{app, path};
use ansi_to_tui::IntoText; use ansi_to_tui::IntoText;
use indexmap::IndexSet; use indexmap::IndexSet;
@ -1067,11 +1068,15 @@ fn draw_sort_n_filter<B: Backend>(
}) })
.unwrap_or((Span::raw("s"), Span::raw(""))) .unwrap_or((Span::raw("s"), Span::raw("")))
}) })
.take(if let Some(true) = search.map(|s| s.ranked) { .take(
0 if let Some(SearchOrder::Sorted) =
} else { search.map(|s| s.config.order.to_owned())
{
sort_by.len() sort_by.len()
}), } else {
0
},
),
) )
.chain(search.iter().map(|s| { .chain(search.iter().map(|s| {
ui.search_identifier ui.search_identifier

Loading…
Cancel
Save