ad_editor/
util.rs

1//! Utility functions
2use crate::{config::config_path, editor::built_in_commands};
3use std::{
4    iter::Peekable,
5    path::Path,
6    str::Chars,
7    sync::{Arc, LockResult, RwLock, RwLockReadGuard},
8};
9use tracing::warn;
10
11/// A wrapper around an Arc<RwLock<T>> so that the owner is only
12/// permitted read access to the underlying value.
13#[derive(Debug, Default, Clone)]
14pub struct ReadOnlyLock<T>(Arc<RwLock<T>>);
15
16impl<T> ReadOnlyLock<T> {
17    /// Construct a new ReadOnlyLock wrapping an inner Arc<RwLock<T>>
18    pub fn new(inner: Arc<RwLock<T>>) -> Self {
19        Self(inner)
20    }
21
22    /// Obtain a read guard from the underlying RwLock
23    pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
24        self.0.read()
25    }
26}
27
28/// Pull in data from the ad crate itself to auto-generate the docs on the functionality
29/// available in the editor.
30pub(crate) fn gen_help_docs() -> String {
31    let help_template = include_str!("../data/help-template.txt");
32
33    help_template
34        .replace("{{BUILT_IN_COMMANDS}}", &commands_section())
35        .replace("{{CONFIG_PATH}}", &config_path())
36}
37
38fn commands_section() -> String {
39    let commands = built_in_commands();
40    let mut buf = Vec::with_capacity(commands.len());
41
42    for (cmds, desc) in commands.into_iter() {
43        buf.push((cmds.join(" | "), desc));
44    }
45
46    let w_max = buf.iter().map(|(s, _)| s.len()).max().unwrap();
47    let mut s = String::new();
48
49    for (cmds, desc) in buf.into_iter() {
50        s.push_str(&format!("{:width$} -- {desc}\n", cmds, width = w_max));
51    }
52
53    s
54}
55
56// returns the parsed number and following character if there was one.
57// initial must be a valid ascii digit
58pub(crate) fn parse_num(initial: char, it: &mut Peekable<Chars<'_>>) -> usize {
59    let mut s = String::from(initial);
60    loop {
61        match it.peek() {
62            Some(ch) if ch.is_ascii_digit() => {
63                s.push(it.next().unwrap());
64            }
65            _ => return s.parse().unwrap(),
66        }
67    }
68}
69
70pub(crate) fn normalize_line_endings(mut s: String) -> String {
71    if !s.contains('\r') {
72        return s;
73    }
74
75    warn!("normalizing \\r characters to \\n");
76    s = s.replace("\r\n", "\n");
77    s.replace("\r", "\n")
78}
79
80/// Locate the first parent directory containing a target file
81pub(crate) fn parent_dir_containing<'a>(initial: &'a Path, target: &str) -> Option<&'a Path> {
82    initial
83        .ancestors()
84        .find(|&p| p.is_dir() && p.join(target).exists())
85}