Make search ranking and algorithm more extensible

pull/585/head
Noah Mayr 1 year ago
parent 16747e7925
commit 8ca5ba0ac7

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

@ -4,6 +4,8 @@ use crate::app::NodeFilter;
use crate::app::NodeSorter;
use crate::app::NodeSorterApplicable;
use crate::node::Node;
use crate::search::SearchAlgorithm;
use crate::search::SearchOrder;
use crate::ui::Border;
use crate::ui::BorderType;
use crate::ui::Constraint;
@ -236,6 +238,16 @@ pub struct SortAndFilterUi {
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)]
#[serde(deny_unknown_fields)]
pub struct PanelUi {
@ -309,6 +321,9 @@ pub struct GeneralConfig {
#[serde(default)]
pub sort_and_filter_ui: SortAndFilterUi,
#[serde(default)]
pub search: SearchConfig,
#[serde(default)]
pub panel_ui: PanelUi,

@ -1,37 +1,13 @@
use crate::app::{
DirectoryBuffer, ExplorerConfig, ExternalMsg, InternalMsg, MsgIn, Node, Task,
};
use crate::search::SearchOrder;
use anyhow::{Error, Result};
use lazy_static::lazy_static;
use skim::prelude::ExactOrFuzzyEngineFactory;
use skim::{MatchEngineFactory, SkimItem};
use std::fs;
use std::path::PathBuf;
use std::sync::mpsc::Sender;
use std::sync::Arc;
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>> {
let dirs = fs::read_dir(parent)?;
let mut nodes = dirs
@ -47,23 +23,16 @@ pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result<Vec<Node>> {
.filter(|n| config.filter(n))
.collect::<Vec<Node>>();
nodes = if let Some((pattern, ranked)) =
config.searcher.as_ref().map(|s| (&s.pattern, &s.ranked))
{
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<(_, _)>>();
nodes = if let Some(searcher) = config.searcher.as_ref() {
let mut ranked_nodes =
searcher.config.algorithm.search(&searcher.pattern, nodes);
if *ranked {
ranked_nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1))
} else {
ranked_nodes.sort_by(|(a, _), (b, _)| config.sort(a, b))
}
match searcher.config.order {
SearchOrder::Ranked => ranked_nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1)),
SearchOrder::Sorted => {
ranked_nodes.sort_by(|(a, _), (b, _)| config.sort(a, b))
}
};
ranked_nodes.into_iter().map(|(n, _)| n).collect::<Vec<_>>()
} else {

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

@ -17,6 +17,7 @@ pub mod permissions;
pub mod pipe;
pub mod pwd_watcher;
pub mod runner;
pub mod search;
pub mod ui;
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 regex::Regex;
use serde::{Deserialize, Serialize};
@ -943,32 +948,32 @@ pub enum ExternalMsg {
/// - YAML: `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.
///
/// Example:
///
/// - Lua: `"ToggleRankedSearch"`
/// - YAML: `ToggleRankedSearch`
ToggleRankedSearch,
/// - Lua: `"CycleSearchOrder"`
/// - YAML: `CycleSearchOrder`
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.
///
/// Example:
///
/// - Lua: `"EnableRankedSearch"`
/// - YAML: `EnableRankedSearch`
EnableRankedSearch,
/// - Lua: `{ SetSearchOrder = "Ranked" }`
/// - YAML: `SetSearchOrder: Sorted`
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.
///
/// Example:
///
/// - Lua: `"DisableRankedSearch"`
/// - YAML: `DisableRankedSearch`
DisableRankedSearch,
/// - Lua: `{ SetSearchAlgorithm = { Skim = "Fuzzy" } }`
/// - YAML: `SetSearchAlgorithm: {Skim: Regex}`
SetSearchAlgorithm(SearchAlgorithm),
/// Accepts the search by keeping the latest focus while in search mode.
/// Automatically calls `ExplorePwd`.
@ -1684,35 +1689,20 @@ pub struct NodeSearcher {
#[serde(default)]
pub recoverable_focus: Option<String>,
pub ranked: bool,
#[serde(default)]
pub config: SearchConfig,
}
impl NodeSearcher {
pub fn new(
pattern: String,
recoverable_focus: Option<String>,
ranked: bool,
config: SearchConfig,
) -> Self {
Self {
pattern,
recoverable_focus,
ranked,
}
}
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,
config,
}
}
}

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

Loading…
Cancel
Save