mirror of https://github.com/sayanarijit/xplr
Release 0.21.0 (#602)
* Add xplr.util.lscolor and xplr.util.paint (#569) * Add xplr.util.lscolor and xplr.util.style * Fix formatting * Fix clippy suggestions * Remove redundant closures * Optimize, support NO_COLOR, and rename style to paint * Use xplr.util.paint and xplr.util.color in init.lua Co-authored-by: Noah Mayr <dev@noahmayr.com> * Add utility function xplr.util.textwrap (#567) * Add utility function xplr.util.wrap * Cleanup and fix formatting * Update src/lua/util.rs Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com> * Update wrap to return lines instead * Fix doc * Rename wrap -> text wrap Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com> Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * Add xplr.util.relative_to and xplr.util.path_shorthand (#568) * Add xplr.util.relative_to and xplr.util.path_shorthand * Remove duplicate slash at end * Use pwd from env and remove pathdiff package * Some fixes and improvements * Generate docs * Some more improvements * Improve selection rendering * Improve functions with test cases * Update docs * Minor doc fix * Rename path_shorthand -> shortened * Handle homedir edgecase Also fix init.lua * Minor fix * Use config argument for relative and shortened paths * Prefix relative paths with "." and fix edge cases where we're not showing the file name * Use and_then instead of map and flatten * WIP: Move selection rendering to lua * Make selection renderer function configurable on lua side * Some improvements * Some impovements * Minor doc fix * Remove symlink style --------- Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * Add xplr.util.layout_replaced (#574) Closes: https://github.com/sayanarijit/xplr/issues/573 * Improve selection operations (#575) - `:sl` to list selection. - `:ss` to softlink. - `:sh` to hardlink. - Avoid conflict by adding suffix. - Unselect individual path only on operation success. Closes: - https://github.com/sayanarijit/xplr/issues/572 - https://github.com/sayanarijit/xplr/issues/571 - https://github.com/sayanarijit/xplr/issues/570 * Minor updates * Add more features (#581) * Add more features - Key binding ":se" to edit selection list in $EDITOR - New utility functions: - xplr.util.clone - xplr.util.exists - xplr.util.is_dir - xplr.util.is_file - xplr.util.is_symlink - xplr.util.is_absolute - xplr.util.path_split - xplr.util.node Closes: https://github.com/sayanarijit/xplr/issues/580 Closes: https://github.com/sayanarijit/xplr/issues/579 Closes: https://github.com/sayanarijit/xplr/issues/577 * Fix edit selection list * Fix clippy lints * Fix layout link in doc * xplr.util.shortened -> xplr.util.shorten * Fix more clippy lints * Fix xplr.util.shorten name change * More UI utilities and improvements (#582) * More UI utilities and improvements - Apply style only to the file column in the table. - Properly quote paths. - Expose the applicable style from config in the table renderer argument. - Add utility functions: - xplr.util.node_type - xplr.util.style_mix - xplr.util.shell_escape * Make escaping play nice with shorten * Fix tests * Fix doc * Some fixes * Fix selection editor * Fix clear selection for selection editor * Add selection navigation (#583) * Add selection navigation - FocusNextSelection (ctrl-n) - FocusPreviousSelection (ctrl-p) Also improve batch operations * Minor doc fixes * Minor doc fix * Remove tab -> ctrl-i binding * Improve batch operation interaction - More robust focus operation. - Focus on failed to delete paths. * Fix Rust compatibility * Fix panic on permission denial Also, improve the error messages. * More logging improvements * Fix layout_replace only working with table parameters (#586) * Improve builtin search mode (#585) * Improve builtin search mode * Remove commented out code * Make search ranking and algorithm more extensible * Flatten messages BREAKING: xplr.config.general.sort_and_filter_ui.search_identifier -> xplr.config.general.sort_and_filter_ui.search_identifiers Messages: - Search - SearchFromInput - SearchFuzzy - SearchFuzzyUnranked - SearchFuzzyUnrankedFromInput - SearchRegexUnrankedFromInput - SearchRegex - SearchRegexUnranked - SearchRegexUnrankedFromInput - SearchRegexUnrankedFromInput - CycleSearchAlgorithm - EnableRankedSearch - DisableRankedSearch - ToggleRankedSearch Static config: xplr.config.general.search.algorithm = "Fuzzy" * Handle search ranking in search algorithm * Make CycleSearchAlgorithm only cycle between algorithms, without changing ranking * Separate algorithm and ordering * Minor doc updates * Some cleanup * Final touch * Cycle -> Toggle --------- Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * Fix layout replace for unit layouts (#588) * Allow custom title and ui config in custom layout. (#589) * Allow custom title and ui config in custom layout. Adds the following layouts: - Static - Dynamic Deprecates `CustomContent` (but won't be removed to maintain compatibility). Closes: https://github.com/sayanarijit/xplr/issues/563 * Delete init.lua * Update docs/en/src/layout.md * Update docs/en/src/layout.md * Rename - Paragraph => CustomParagraph - List => CustomList - Table => CustomTable Also update init.lua * Fix clippy errs * Fix doc links * Fix search order * Improve working with file permissions (#591) * Improve working with file permissions Implements: - xplr.util.permissions_rwx - xplr.util.permissions_octal * Edit permissions * Add permissions in Resolved Node (#592) * Add permissions in Relolved Node And handle application/x-executable mime type. * Fix bench * Improve permissions editor * More permissions editor improvements * Doc updates * Remove ResolvedNode.permissions (#593) Reason: Too much serialization making lua calls slow. * Add workaround for macos with legacy coreutils (#595) Refs: - https://github.com/sayanarijit/xplr/issues/594 - https://github.com/sayanarijit/xplr/issues/559 * Use H:M:S format to display logs (#596) * Keep the selection list and clear manually (#597) * Keep the selection list and clear manually Ref: https://github.com/sayanarijit/map.xplr/issues/4 * Fix linting err * Fix broken history (#599) * Fix broken hostory Fixes: https://github.com/sayanarijit/xplr/issues/598 * Minor cleanup * Slightly optimize selection retention (#600) * Update deps * chrono -> time * update: 0.20.2 -> 0.21.1 * Update post-install.md * Upgrade guide * Minor fix * Fix tests * Add missing doc * Fix clippy lints --------- Co-authored-by: Noah Mayr <dev@noahmayr.com>extend-selection v0.21.0
parent
59279b816d
commit
e0d683b13a
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,77 @@
|
||||
# Searching
|
||||
|
||||
xplr supports searching paths using different algorithm. The search mechanism
|
||||
generally appears between filters and sorters in the `Sort & filter` panel.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
fzy:foo↓
|
||||
```
|
||||
|
||||
This line means that the nodes visible on the table are being filtered using the
|
||||
[fuzzy matching][1] algorithm on the input `foo`. The arrow means that ranking based
|
||||
ordering is being applied, i.e. [sorters][2] are being ignored.
|
||||
|
||||
## Node Searcher Applicable
|
||||
|
||||
Node Searcher contains the following fields:
|
||||
|
||||
- [pattern][3]
|
||||
- [recoverable_focus][4]
|
||||
- [algorithm][5]
|
||||
- [unordered][7]
|
||||
|
||||
### pattern
|
||||
|
||||
The patters used to search.
|
||||
|
||||
Type: string
|
||||
|
||||
### recoverable_focus
|
||||
|
||||
Where to focus when search is cancelled.
|
||||
|
||||
Type: nullable string
|
||||
|
||||
### algorithm
|
||||
|
||||
Search algorithm to use. Defaults to the value set in
|
||||
[xplr.config.general.search.algorithm][8].
|
||||
|
||||
It can be one of the following:
|
||||
|
||||
- Fuzzy
|
||||
- Regex
|
||||
|
||||
### unordered
|
||||
|
||||
Whether to skip ordering the search result by algorithm based ranking. Defaults
|
||||
to the value set in [xplr.config.general.search.unordered][9].
|
||||
|
||||
Type: boolean
|
||||
|
||||
## Example:
|
||||
|
||||
```lua
|
||||
local searcher = {
|
||||
pattern = "pattern to search",
|
||||
recoverable_focus = "/path/to/focus/on/cancel",
|
||||
algorithm = "Fuzzy",
|
||||
unordered = false,
|
||||
}
|
||||
|
||||
xplr.util.explore({ searcher = searcher })
|
||||
```
|
||||
|
||||
See [xplr.util.explore][6].
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/Approximate_string_matching
|
||||
[2]: sorting.md
|
||||
[3]: #pattern
|
||||
[4]: #recoverable_focus
|
||||
[5]: #algorithm
|
||||
[6]: xplr.util.md#xplrutilexplore
|
||||
[7]: #unordered
|
||||
[8]: general-config.md#xplrconfiggeneralsearchalgorithm
|
||||
[9]: general-config.md#xplrconfiggeneralsearchunordered
|
@ -0,0 +1,224 @@
|
||||
// Things of the past, mostly bad decisions, which cannot erased, stays in this
|
||||
// haunted module.
|
||||
|
||||
use crate::app;
|
||||
use crate::lua;
|
||||
use crate::ui::block;
|
||||
use crate::ui::string_to_text;
|
||||
use crate::ui::Constraint;
|
||||
use crate::ui::ContentRendererArg;
|
||||
use mlua::Lua;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tui::backend::Backend;
|
||||
use tui::layout::Constraint as TuiConstraint;
|
||||
use tui::layout::Rect as TuiRect;
|
||||
use tui::widgets::Cell;
|
||||
use tui::widgets::List;
|
||||
use tui::widgets::ListItem;
|
||||
use tui::widgets::Paragraph;
|
||||
use tui::widgets::Row;
|
||||
use tui::widgets::Table;
|
||||
use tui::Frame;
|
||||
|
||||
/// A cursed enum from crate::ui.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum ContentBody {
|
||||
/// A paragraph to render
|
||||
StaticParagraph { render: String },
|
||||
|
||||
/// A Lua function that returns a paragraph to render
|
||||
DynamicParagraph { render: String },
|
||||
|
||||
/// List to render
|
||||
StaticList { render: Vec<String> },
|
||||
|
||||
/// A Lua function that returns lines to render
|
||||
DynamicList { render: String },
|
||||
|
||||
/// A table to render
|
||||
StaticTable {
|
||||
widths: Vec<Constraint>,
|
||||
col_spacing: Option<u16>,
|
||||
render: Vec<Vec<String>>,
|
||||
},
|
||||
|
||||
/// A Lua function that returns a table to render
|
||||
DynamicTable {
|
||||
widths: Vec<Constraint>,
|
||||
col_spacing: Option<u16>,
|
||||
render: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A cursed struct from crate::ui.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CustomContent {
|
||||
pub title: Option<String>,
|
||||
pub body: ContentBody,
|
||||
}
|
||||
|
||||
/// A cursed function from crate::ui.
|
||||
pub fn draw_custom_content<B: Backend>(
|
||||
f: &mut Frame<B>,
|
||||
screen_size: TuiRect,
|
||||
layout_size: TuiRect,
|
||||
app: &app::App,
|
||||
content: CustomContent,
|
||||
lua: &Lua,
|
||||
) {
|
||||
let config = app.config.general.panel_ui.default.clone();
|
||||
let title = content.title;
|
||||
let body = content.body;
|
||||
|
||||
match body {
|
||||
ContentBody::StaticParagraph { render } => {
|
||||
let render = string_to_text(render);
|
||||
let content = Paragraph::new(render).block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {t} ")).unwrap_or_default(),
|
||||
));
|
||||
f.render_widget(content, layout_size);
|
||||
}
|
||||
|
||||
ContentBody::DynamicParagraph { render } => {
|
||||
let ctx = ContentRendererArg {
|
||||
app: app.to_lua_ctx_light(),
|
||||
layout_size: layout_size.into(),
|
||||
screen_size: screen_size.into(),
|
||||
};
|
||||
|
||||
let render = lua::serialize(lua, &ctx)
|
||||
.map(|arg| {
|
||||
lua::call(lua, &render, arg).unwrap_or_else(|e| format!("{e:?}"))
|
||||
})
|
||||
.unwrap_or_else(|e| e.to_string());
|
||||
|
||||
let render = string_to_text(render);
|
||||
|
||||
let content = Paragraph::new(render).block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {t} ")).unwrap_or_default(),
|
||||
));
|
||||
f.render_widget(content, layout_size);
|
||||
}
|
||||
|
||||
ContentBody::StaticList { render } => {
|
||||
let items = render
|
||||
.into_iter()
|
||||
.map(string_to_text)
|
||||
.map(ListItem::new)
|
||||
.collect::<Vec<ListItem>>();
|
||||
|
||||
let content = List::new(items).block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {t} ")).unwrap_or_default(),
|
||||
));
|
||||
f.render_widget(content, layout_size);
|
||||
}
|
||||
|
||||
ContentBody::DynamicList { render } => {
|
||||
let ctx = ContentRendererArg {
|
||||
app: app.to_lua_ctx_light(),
|
||||
layout_size: layout_size.into(),
|
||||
screen_size: screen_size.into(),
|
||||
};
|
||||
|
||||
let items = lua::serialize(lua, &ctx)
|
||||
.map(|arg| {
|
||||
lua::call(lua, &render, arg)
|
||||
.unwrap_or_else(|e| vec![format!("{e:?}")])
|
||||
})
|
||||
.unwrap_or_else(|e| vec![e.to_string()])
|
||||
.into_iter()
|
||||
.map(string_to_text)
|
||||
.map(ListItem::new)
|
||||
.collect::<Vec<ListItem>>();
|
||||
|
||||
let content = List::new(items).block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {t} ")).unwrap_or_default(),
|
||||
));
|
||||
f.render_widget(content, layout_size);
|
||||
}
|
||||
|
||||
ContentBody::StaticTable {
|
||||
widths,
|
||||
col_spacing,
|
||||
render,
|
||||
} => {
|
||||
let rows = render
|
||||
.into_iter()
|
||||
.map(|cols| {
|
||||
Row::new(
|
||||
cols.into_iter()
|
||||
.map(string_to_text)
|
||||
.map(Cell::from)
|
||||
.collect::<Vec<Cell>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Row>>();
|
||||
|
||||
let widths = widths
|
||||
.into_iter()
|
||||
.map(|w| w.to_tui(screen_size, layout_size))
|
||||
.collect::<Vec<TuiConstraint>>();
|
||||
|
||||
let content = Table::new(rows)
|
||||
.widths(&widths)
|
||||
.column_spacing(col_spacing.unwrap_or(1))
|
||||
.block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {t} ")).unwrap_or_default(),
|
||||
));
|
||||
|
||||
f.render_widget(content, layout_size);
|
||||
}
|
||||
|
||||
ContentBody::DynamicTable {
|
||||
widths,
|
||||
col_spacing,
|
||||
render,
|
||||
} => {
|
||||
let ctx = ContentRendererArg {
|
||||
app: app.to_lua_ctx_light(),
|
||||
layout_size: layout_size.into(),
|
||||
screen_size: screen_size.into(),
|
||||
};
|
||||
|
||||
let rows = lua::serialize(lua, &ctx)
|
||||
.map(|arg| {
|
||||
lua::call(lua, &render, arg)
|
||||
.unwrap_or_else(|e| vec![vec![format!("{e:?}")]])
|
||||
})
|
||||
.unwrap_or_else(|e| vec![vec![e.to_string()]])
|
||||
.into_iter()
|
||||
.map(|cols| {
|
||||
Row::new(
|
||||
cols.into_iter()
|
||||
.map(string_to_text)
|
||||
.map(Cell::from)
|
||||
.collect::<Vec<Cell>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Row>>();
|
||||
|
||||
let widths = widths
|
||||
.into_iter()
|
||||
.map(|w| w.to_tui(screen_size, layout_size))
|
||||
.collect::<Vec<TuiConstraint>>();
|
||||
|
||||
let mut content = Table::new(rows).widths(&widths).block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {t} ")).unwrap_or_default(),
|
||||
));
|
||||
|
||||
if let Some(col_spacing) = col_spacing {
|
||||
content = content.column_spacing(col_spacing);
|
||||
};
|
||||
|
||||
f.render_widget(content, layout_size);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,495 @@
|
||||
use anyhow::{bail, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use snailquote::escape;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref HOME: Option<PathBuf> = dirs::home_dir();
|
||||
}
|
||||
|
||||
// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
|
||||
pub fn diff<P, B>(path: P, base: B) -> Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let base = base.as_ref();
|
||||
|
||||
if path.is_absolute() != base.is_absolute() {
|
||||
if path.is_absolute() {
|
||||
Ok(PathBuf::from(path))
|
||||
} else {
|
||||
let path = path.to_string_lossy();
|
||||
bail!("{path}: is not absolute")
|
||||
}
|
||||
} else {
|
||||
let mut ita = path.components();
|
||||
let mut itb = base.components();
|
||||
let mut comps: Vec<Component> = vec![];
|
||||
loop {
|
||||
match (ita.next(), itb.next()) {
|
||||
(None, None) => break,
|
||||
(Some(a), None) => {
|
||||
comps.push(a);
|
||||
comps.extend(ita.by_ref());
|
||||
break;
|
||||
}
|
||||
(None, _) => comps.push(Component::ParentDir),
|
||||
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
|
||||
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
|
||||
(Some(_), Some(b)) if b == Component::ParentDir => {
|
||||
let path = path.to_string_lossy();
|
||||
let base = base.to_string_lossy();
|
||||
bail!("{base} is not a parent of {path}")
|
||||
}
|
||||
(Some(a), Some(_)) => {
|
||||
comps.push(Component::ParentDir);
|
||||
for _ in itb {
|
||||
comps.push(Component::ParentDir);
|
||||
}
|
||||
comps.push(a);
|
||||
comps.extend(ita.by_ref());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(comps.iter().map(|c| c.as_os_str()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct RelativityConfig<B: AsRef<Path>> {
|
||||
base: Option<B>,
|
||||
with_prefix_dots: Option<bool>,
|
||||
without_suffix_dots: Option<bool>,
|
||||
}
|
||||
|
||||
impl<B: AsRef<Path>> RelativityConfig<B> {
|
||||
pub fn with_base(mut self, base: B) -> Self {
|
||||
self.base = Some(base);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_prefix_dots(mut self) -> Self {
|
||||
self.with_prefix_dots = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn without_suffix_dots(mut self) -> Self {
|
||||
self.without_suffix_dots = Some(true);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn relative_to<P, B>(
|
||||
path: P,
|
||||
config: Option<&RelativityConfig<B>>,
|
||||
) -> Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let base = match config.and_then(|c| c.base.as_ref()) {
|
||||
Some(base) => PathBuf::from(base.as_ref()),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
|
||||
let diff = diff(path, base)?;
|
||||
|
||||
let relative = if diff.to_str() == Some("") {
|
||||
".".into()
|
||||
} else {
|
||||
diff
|
||||
};
|
||||
|
||||
let relative = if config.and_then(|c| c.with_prefix_dots).unwrap_or(false)
|
||||
&& !relative.starts_with(".")
|
||||
&& !relative.starts_with("..")
|
||||
{
|
||||
PathBuf::from(".").join(relative)
|
||||
} else {
|
||||
relative
|
||||
};
|
||||
|
||||
let relative = if !config.and_then(|c| c.without_suffix_dots).unwrap_or(false) {
|
||||
relative
|
||||
} else if relative.ends_with(".") {
|
||||
match (path.parent(), path.file_name()) {
|
||||
(Some(_), Some(name)) => PathBuf::from("..").join(name),
|
||||
(_, _) => relative,
|
||||
}
|
||||
} else if relative.ends_with("..") {
|
||||
match (path.parent(), path.file_name()) {
|
||||
(Some(parent), Some(name)) => {
|
||||
if parent.parent().is_some() {
|
||||
relative.join("..").join(name)
|
||||
} else {
|
||||
// always prefer absolute path if it's a child of the root directory
|
||||
// to guarantee that the basename is included
|
||||
path.into()
|
||||
}
|
||||
}
|
||||
(_, _) => relative,
|
||||
}
|
||||
} else {
|
||||
relative
|
||||
};
|
||||
|
||||
Ok(relative)
|
||||
}
|
||||
|
||||
pub fn shorten<P, B>(path: P, config: Option<&RelativityConfig<B>>) -> Result<String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let pathstring = path.to_string_lossy().to_string();
|
||||
let relative = relative_to(path, config)?;
|
||||
|
||||
let relative = relative.to_string_lossy().to_string();
|
||||
|
||||
let fromhome = HOME
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
path.strip_prefix(h).ok().map(|p| {
|
||||
if p.to_str() == Some("") {
|
||||
"~".into()
|
||||
} else {
|
||||
PathBuf::from("~").join(p).to_string_lossy().to_string()
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(pathstring);
|
||||
|
||||
if relative.len() < fromhome.len() {
|
||||
Ok(relative)
|
||||
} else {
|
||||
Ok(fromhome)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
type Config<'a> = Option<&'a RelativityConfig<String>>;
|
||||
|
||||
const NONE: Config = Config::None;
|
||||
|
||||
fn default<'a>() -> RelativityConfig<&'a str> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_to_pwd() {
|
||||
let path = std::env::current_dir().unwrap();
|
||||
|
||||
let relative = relative_to(&path, NONE).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("."));
|
||||
|
||||
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("."));
|
||||
|
||||
let relative =
|
||||
relative_to(&path, Some(&default().without_suffix_dots())).unwrap();
|
||||
assert_eq!(
|
||||
relative,
|
||||
PathBuf::from("..").join(path.file_name().unwrap())
|
||||
);
|
||||
|
||||
let relative = relative_to(
|
||||
&path,
|
||||
Some(&default().with_prefix_dots().without_suffix_dots()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
relative,
|
||||
PathBuf::from("..").join(path.file_name().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_to_parent() {
|
||||
let path = std::env::current_dir().unwrap();
|
||||
let parent = path.parent().unwrap();
|
||||
|
||||
let relative = relative_to(parent, NONE).unwrap();
|
||||
assert_eq!(relative, PathBuf::from(".."));
|
||||
|
||||
let relative = relative_to(parent, Some(&default().with_prefix_dots())).unwrap();
|
||||
assert_eq!(relative, PathBuf::from(".."));
|
||||
|
||||
let relative =
|
||||
relative_to(parent, Some(&default().without_suffix_dots())).unwrap();
|
||||
assert_eq!(
|
||||
relative,
|
||||
PathBuf::from("../..").join(parent.file_name().unwrap())
|
||||
);
|
||||
|
||||
let relative = relative_to(
|
||||
parent,
|
||||
Some(&default().with_prefix_dots().without_suffix_dots()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
relative,
|
||||
PathBuf::from("../..").join(parent.file_name().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_to_file() {
|
||||
let path = std::env::current_dir().unwrap().join("foo").join("bar");
|
||||
|
||||
let relative = relative_to(&path, NONE).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("foo/bar"));
|
||||
|
||||
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("./foo/bar"));
|
||||
|
||||
let relative = relative_to(
|
||||
&path,
|
||||
Some(&default().with_prefix_dots().without_suffix_dots()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(relative, PathBuf::from("./foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_to_root() {
|
||||
let relative = relative_to("/foo", Some(&default().with_base("/"))).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("foo"));
|
||||
|
||||
let relative = relative_to(
|
||||
"/foo",
|
||||
Some(
|
||||
&default()
|
||||
.with_base("/")
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(relative, PathBuf::from("./foo"));
|
||||
|
||||
let relative = relative_to("/", Some(&default().with_base("/"))).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("."));
|
||||
|
||||
let relative = relative_to(
|
||||
"/",
|
||||
Some(
|
||||
&default()
|
||||
.with_base("/")
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(relative, PathBuf::from("."));
|
||||
|
||||
let relative = relative_to("/", Some(&default().with_base("/foo"))).unwrap();
|
||||
assert_eq!(relative, PathBuf::from(".."));
|
||||
|
||||
let relative = relative_to(
|
||||
"/",
|
||||
Some(
|
||||
&default()
|
||||
.with_base("/foo")
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(relative, PathBuf::from(".."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_to_base() {
|
||||
let path = "/some/directory";
|
||||
let base = "/another/foo/bar";
|
||||
|
||||
let relative = relative_to(path, Some(&default().with_base(base))).unwrap();
|
||||
assert_eq!(relative, PathBuf::from("../../../some/directory"));
|
||||
|
||||
let relative = relative_to(
|
||||
path,
|
||||
Some(
|
||||
&default()
|
||||
.with_base(base)
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(relative, PathBuf::from("../../../some/directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shorten_home() {
|
||||
let path = HOME.as_ref().unwrap();
|
||||
|
||||
let res = shorten(path, NONE).unwrap();
|
||||
assert_eq!(res, "~");
|
||||
|
||||
let res = shorten(
|
||||
path,
|
||||
Some(&default().with_prefix_dots().without_suffix_dots()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "~");
|
||||
|
||||
let res = shorten(
|
||||
path,
|
||||
Some(&default().with_prefix_dots().without_suffix_dots()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "~");
|
||||
|
||||
let res = shorten(path.join("foo"), NONE).unwrap();
|
||||
assert_eq!(res, "~/foo");
|
||||
|
||||
let res = shorten(
|
||||
path.join("foo"),
|
||||
Some(&default().with_prefix_dots().without_suffix_dots()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "~/foo");
|
||||
|
||||
let res = shorten(format!("{}foo", path.to_string_lossy()), NONE).unwrap();
|
||||
assert_ne!(res, "~/foo");
|
||||
assert_eq!(res, format!("{}foo", path.to_string_lossy()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shorten_base() {
|
||||
let path = "/present/working/directory";
|
||||
let base = "/present/foo/bar";
|
||||
|
||||
let res = shorten(path, Some(&default().with_base(base))).unwrap();
|
||||
assert_eq!(res, "../../working/directory");
|
||||
|
||||
let res = shorten(
|
||||
path,
|
||||
Some(
|
||||
&default()
|
||||
.with_base(base)
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "../../working/directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shorten_pwd() {
|
||||
let path = "/present/working/directory";
|
||||
|
||||
let res = shorten(path, Some(&default().with_base(path))).unwrap();
|
||||
assert_eq!(res, ".");
|
||||
|
||||
let res = shorten(
|
||||
path,
|
||||
Some(
|
||||
&default()
|
||||
.with_base(path)
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "../directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shorten_parent() {
|
||||
let path = "/present/working";
|
||||
let base = "/present/working/directory";
|
||||
|
||||
let res = shorten(path, Some(&default().with_base(base))).unwrap();
|
||||
assert_eq!(res, "..");
|
||||
|
||||
let res = shorten(
|
||||
path,
|
||||
Some(
|
||||
&default()
|
||||
.with_base(base)
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "../../working");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shorten_root() {
|
||||
let res = shorten("/", Some(&default().with_base("/"))).unwrap();
|
||||
assert_eq!(res, "/");
|
||||
|
||||
let res = shorten(
|
||||
"/",
|
||||
Some(
|
||||
&default()
|
||||
.with_base("/")
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "/");
|
||||
|
||||
let res = shorten("/foo", Some(&default().with_base("/"))).unwrap();
|
||||
assert_eq!(res, "foo");
|
||||
|
||||
let res = shorten(
|
||||
"/foo",
|
||||
Some(
|
||||
&default()
|
||||
.with_base("/")
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "/foo");
|
||||
|
||||
let res = shorten(
|
||||
"/",
|
||||
Some(
|
||||
&default()
|
||||
.with_base("/foo")
|
||||
.with_prefix_dots()
|
||||
.without_suffix_dots(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_escape() {
|
||||
let text = "foo".to_string();
|
||||
assert_eq!(escape(&text), "foo");
|
||||
|
||||
let text = "foo bar".to_string();
|
||||
assert_eq!(escape(&text), "'foo bar'");
|
||||
|
||||
let text = "foo\nbar".to_string();
|
||||
assert_eq!(escape(&text), "\"foo\\nbar\"");
|
||||
|
||||
let text = "foo$bar".to_string();
|
||||
assert_eq!(escape(&text), "'foo$bar'");
|
||||
|
||||
let text = "foo'$\n'bar".to_string();
|
||||
assert_eq!(escape(&text), "\"foo'\\$\\n'bar\"");
|
||||
|
||||
let text = "a'b\"c".to_string();
|
||||
assert_eq!(escape(&text), "\"a'b\\\"c\"");
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory};
|
||||
use skim::{MatchEngine, MatchEngineFactory, SkimItem};
|
||||
|
||||
lazy_static! {
|
||||
static ref FUZZY_FACTORY: ExactOrFuzzyEngineFactory =
|
||||
ExactOrFuzzyEngineFactory::builder().build();
|
||||
static ref REGEX_FACTORY: RegexEngineFactory = RegexEngineFactory::builder().build();
|
||||
}
|
||||
|
||||
pub 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, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum SearchAlgorithm {
|
||||
#[default]
|
||||
Fuzzy,
|
||||
Regex,
|
||||
}
|
||||
|
||||
impl SearchAlgorithm {
|
||||
pub fn toggle(self) -> Self {
|
||||
match self {
|
||||
Self::Fuzzy => Self::Regex,
|
||||
Self::Regex => Self::Fuzzy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn engine(&self, pattern: &str) -> Box<dyn MatchEngine> {
|
||||
match self {
|
||||
Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern),
|
||||
Self::Regex => REGEX_FACTORY.create_engine(pattern),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue