repo-quest 0.3.4

Interactive programming tutorials via Github.
Documentation
//! Copied from https://github.com/mikaelmello/inquire/blob/main/inquire/examples/complex_autocompletion.rs

use std::{fs, io::ErrorKind, path::Path};

use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use inquire::{
  CustomUserError,
  autocompletion::{Autocomplete, Replacement},
  validator::{ErrorMessage, StringValidator, Validation},
};

#[derive(Clone, Default)]
pub struct FilePathCompleter {
  input: String,
  paths: Vec<String>,
}

impl FilePathCompleter {
  fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
    if input == self.input && !self.paths.is_empty() {
      return Ok(());
    }

    self.input = input.to_owned();
    self.paths.clear();

    let input_path = std::path::PathBuf::from(input);

    let fallback_parent = input_path
      .parent()
      .map(|p| {
        if p.to_string_lossy() == "" {
          std::path::PathBuf::from(".")
        } else {
          p.to_owned()
        }
      })
      .unwrap_or_else(|| std::path::PathBuf::from("."));

    let scan_dir = if input.ends_with('/') {
      input_path
    } else {
      fallback_parent.clone()
    };

    let entries = match std::fs::read_dir(scan_dir) {
      Ok(read_dir) => Ok(read_dir),
      Err(err) if err.kind() == ErrorKind::NotFound => std::fs::read_dir(fallback_parent),
      Err(err) => Err(err),
    }?
    .collect::<Result<Vec<_>, _>>()?;

    for entry in entries {
      let path = entry.path();
      let path_str = if path.is_dir() {
        format!("{}/", path.to_string_lossy())
      } else {
        path.to_string_lossy().to_string()
      };

      self.paths.push(path_str);
    }

    Ok(())
  }

  fn fuzzy_sort(&self, input: &str) -> Vec<(String, i64)> {
    let mut matches: Vec<(String, i64)> = self
      .paths
      .iter()
      .filter_map(|path| {
        SkimMatcherV2::default()
          .smart_case()
          .fuzzy_match(path, input)
          .map(|score| (path.clone(), score))
      })
      .collect();

    matches.sort_by(|a, b| b.1.cmp(&a.1));
    matches
  }
}

impl Autocomplete for FilePathCompleter {
  fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
    self.update_input(input)?;

    let matches = self.fuzzy_sort(input);
    Ok(matches.into_iter().take(15).map(|(path, _)| path).collect())
  }

  fn get_completion(
    &mut self,
    input: &str,
    highlighted_suggestion: Option<String>,
  ) -> Result<Replacement, CustomUserError> {
    self.update_input(input)?;

    Ok(if let Some(suggestion) = highlighted_suggestion {
      Replacement::Some(suggestion)
    } else {
      let matches = self.fuzzy_sort(input);
      matches
        .first()
        .map(|(path, _)| Replacement::Some(path.clone()))
        .unwrap_or(Replacement::None)
    })
  }
}

#[derive(Clone, Default)]
pub struct FilePathValidator {}

impl StringValidator for FilePathValidator {
  fn validate(&self, input: &str) -> Result<Validation, CustomUserError> {
    let path = Path::new(input);
    if !fs::exists(path)? {
      Ok(Validation::Invalid(ErrorMessage::Custom(format!(
        "File does not exist at path `{}`",
        path.display()
      ))))
    } else {
      Ok(Validation::Valid)
    }
  }
}