Skip to content

Commit

Permalink
feat(cli): use fix --replace to auto fix icons
Browse files Browse the repository at this point in the history
  • Loading branch information
loichyan committed Mar 28, 2023
1 parent e2e4bc9 commit db26b09
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 71 deletions.
27 changes: 27 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{path::PathBuf, str::FromStr};
use thisctx::IntoError;

const V_PATH: &str = "PATH";
const V_REPLACE: &str = "CLASSES";

#[derive(Debug, Parser)]
pub struct Cli {
Expand Down Expand Up @@ -38,6 +39,12 @@ pub enum Command {
/// Auto-confirm interactive prompts.
#[arg(short, long)]
yes: bool,
/// Replace the prefix of a icon with another.
///
/// For example, use `--replace nf-mdi,nf-md` to replace all `nf-mdi*` icons
/// with the same icons in `nf-md*`.
#[arg(short, long, value_name(V_REPLACE))]
replace: Vec<Replace>,
},
/// Fuzzy search for an icon.
Search {},
Expand Down Expand Up @@ -71,3 +78,23 @@ impl FromStr for UserInput {
}
}
}

#[derive(Debug, Clone)]
pub struct Replace {
pub from: String,
pub to: String,
}

impl FromStr for Replace {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let (from, to) = s
.split_once(',')
.ok_or("the input should be two classes separated by a comma")?;
Ok(Self {
from: from.to_owned(),
to: to.to_owned(),
})
}
}
19 changes: 13 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ mod cli;
mod error;
mod icon;
mod parser;
mod prompt;
mod runtime;

use clap::Parser;
use cli::Command;
use prompt::YesOrNo;
use runtime::{CheckerContext, Runtime};
use thisctx::WithContext;
use tracing::error;

use crate::runtime::YesOrNo;

static CACHED: &str = include_str!("./cached.txt");

fn main_impl() -> error::Result<()> {
let subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::WARN)
.without_time()
.finish();
tracing::subscriber::set_global_default(subscriber).context(error::Any)?;
Expand All @@ -41,14 +42,20 @@ fn main_impl() -> error::Result<()> {
log_or_break!(rt.check(&mut context, path, false));
}
}
// TODO: support autofix
Command::Fix { source, mut yes } => {
let mut context = CheckerContext::default();
Command::Fix {
source,
mut yes,
replace,
} => {
let mut context = CheckerContext {
replace,
..Default::default()
};
for path in source.iter() {
log_or_break!((|| {
if let Some(patched) = rt.check(&mut context, path, true)? {
if !yes {
match rt.prompt_yes_or_no(
match prompt::prompt_yes_or_no(
"Are your sure to write the patched content?",
None,
)? {
Expand Down
44 changes: 44 additions & 0 deletions src/prompt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use crate::error;
use inquire::InquireError;
use std::{fmt, str::FromStr};
use thisctx::{IntoError, WithContext};

pub fn prompt_yes_or_no(msg: &str, help: Option<&str>) -> error::Result<YesOrNo> {
match inquire::CustomType::<YesOrNo>::new(msg)
.with_help_message(help.unwrap_or("Yes/No/All yes, <Ctrl-C> to abort"))
.prompt()
{
Err(InquireError::OperationInterrupted) => error::Interrupted.fail(),
t => t.context(error::Prompt),
}
}

#[derive(Clone, Copy, Debug)]
pub enum YesOrNo {
Yes,
No,
AllYes,
}

impl fmt::Display for YesOrNo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
YesOrNo::Yes => write!(f, "yes"),
YesOrNo::No => write!(f, "no"),
YesOrNo::AllYes => write!(f, "all yes"),
}
}
}

impl FromStr for YesOrNo {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"y" | "yes" => Ok(Self::Yes),
"n" | "no" => Ok(Self::No),
"a" | "all" => Ok(Self::AllYes),
_ => Err("invalid input"),
}
}
}
105 changes: 40 additions & 65 deletions src/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
autocomplete::Autocompleter,
cli::UserInput,
cli::{Replace, UserInput},
error,
icon::{CachedIcon, Icon},
};
Expand All @@ -14,11 +14,12 @@ use indexmap::IndexMap;
use inquire::InquireError;
use ngrammatic::{Corpus, CorpusBuilder};
use once_cell::unsync::OnceCell;
use std::{collections::HashMap, fmt, path::Path, rc::Rc, str::FromStr};
use std::{collections::HashMap, path::Path, rc::Rc};
use thisctx::{IntoError, WithContext};
use tracing::warn;

const SIMILARITY: f32 = 0.75;
const MAX_CHOICES: usize = 7;
const MAX_CHOICES: usize = 4;

pub type FstSet = fst::Set<Vec<u8>>;

Expand Down Expand Up @@ -51,43 +52,43 @@ impl Runtime {
path: &Path,
does_fix: bool,
) -> error::Result<Option<String>> {
macro_rules! report {
($diag:expr) => {
term::emit(&mut context.writer, &context.config, &context.files, $diag)
.context(error::Reporter)?;
};
}

let mut result = None::<String>;
let content = std::fs::read_to_string(path).context(error::Io(path))?;
let file_id = context.files.add(path.display().to_string(), content);
let content = context.files.get(file_id).unwrap().source();
for (start, ch) in content.char_indices() {
for (start, mut ch) in content.char_indices() {
if let Some(&icon) = self.index().get(&ch) {
let icon = &self.icons[icon];
if icon.obsolete {
let mut end = start + 1;
while !content.is_char_boundary(end) {
end += 1;
}
let candidates = self.candidates(icon)?;
let diag = Diagnostic::warning()
.with_message(format!("Found obsolete icon U+{:X}", icon.codepoint as u32))
.with_labels(vec![Label::primary(file_id, start..end)
.with_message(format!("Icon '{}' is marked as obsolete", icon.name))]);
.with_message(format!("Icon '{}' is marked as obsolete", icon.name))])
.with_notes(self.diagnostic_notes(&candidates)?);
term::emit(&mut context.writer, &context.config, &context.files, &diag)
.context(error::Reporter)?;
// Autofix use history.
if let Some(&last) = context.history.get(&icon.codepoint) {
report!(&diag);
cprintln!("# Auto patch using last input '{}'".green, last);
cprintln!("# Auto fix it using last input '{}'".green, last);
ch = last;
// Autofix use replacing.
} else if let Some(new) = self.try_replace(context, icon) {
cprintln!("# Auto replace it with '{}'".green, new);
ch = new;
} else {
let candidates = self.candidates(icon)?;
report!(&diag.with_notes(self.diagnostic_notes(&candidates)?));
// Input a new icon
if does_fix {
// Push all non-patched content.
let res = result.get_or_insert_with(|| content[..start].to_owned());
match self.prompt_input_icon(Some(&candidates)) {
Ok(Some(ch)) => {
res.push(ch);
context.history.insert(icon.codepoint, ch);
continue;
Ok(Some(new)) => {
context.history.insert(icon.codepoint, new);
ch = new;
}
Ok(None) => (),
Err(error::Error::Interrupted) => {
Expand All @@ -100,7 +101,7 @@ impl Runtime {
}
}
}
// Save other characters.
// Save the new character.
if let Some(res) = result.as_mut() {
res.push(ch);
}
Expand All @@ -109,6 +110,18 @@ impl Runtime {
Ok(result)
}

fn try_replace(&self, ctx: &CheckerContext, icon: &Icon) -> Option<char> {
for rep in ctx.replace.iter() {
let Some(name) = icon.name.strip_prefix(&rep.from) else { continue };
let Some(new_icon) = self.icons.get(&format!("{}{name}", rep.to)) else {
warn!("{} cannot be replaced with '{}*'", icon.name, rep.to);
continue;
};
return Some(new_icon.codepoint);
}
None
}

fn candidates(&self, icon: &Icon) -> error::Result<Vec<&Icon>> {
Ok(self
.corpus()
Expand Down Expand Up @@ -202,16 +215,6 @@ impl Runtime {
})
}

pub fn prompt_yes_or_no(&self, msg: &str, help: Option<&str>) -> error::Result<YesOrNo> {
match inquire::CustomType::<YesOrNo>::new(msg)
.with_help_message(help.unwrap_or("Yes/No/All yes, <Ctrl-C> to abort"))
.prompt()
{
Err(InquireError::OperationInterrupted) => error::Interrupted.fail(),
t => t.context(error::Prompt),
}
}

fn autocompleter(&self, candidates: usize) -> Autocompleter {
Autocompleter {
icons: self.icons.clone(),
Expand Down Expand Up @@ -292,10 +295,11 @@ impl RuntimeBuilder {
}

pub struct CheckerContext {
files: SimpleFiles<String, String>,
writer: StandardStream,
config: term::Config,
history: HashMap<char, char>,
pub files: SimpleFiles<String, String>,
pub writer: StandardStream,
pub config: term::Config,
pub history: HashMap<char, char>,
pub replace: Vec<Replace>,
}

impl Default for CheckerContext {
Expand All @@ -305,36 +309,7 @@ impl Default for CheckerContext {
writer: StandardStream::stderr(term::termcolor::ColorChoice::Always),
config: term::Config::default(),
history: HashMap::default(),
}
}
}

#[derive(Clone, Copy, Debug)]
pub enum YesOrNo {
Yes,
No,
AllYes,
}

impl fmt::Display for YesOrNo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
YesOrNo::Yes => write!(f, "yes"),
YesOrNo::No => write!(f, "no"),
YesOrNo::AllYes => write!(f, "all yes"),
}
}
}

impl FromStr for YesOrNo {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"y" | "yes" => Ok(Self::Yes),
"n" | "no" => Ok(Self::No),
"a" | "all" => Ok(Self::AllYes),
_ => Err("invalid input"),
replace: Vec::default(),
}
}
}

0 comments on commit db26b09

Please sign in to comment.