cli_options.rs

Branches > main > 1d2b8ea > files / nvim / pack / plugins / opt / parinfer-rust / src / cli_options.rs

use crate::types;
use crate::types::*;
use std::env;
use std::io;
use std::io::Read;

pub enum InputType {
    Json,
    Kakoune,
    Text,
}

pub enum OutputType {
    Json,
    Kakoune,
    Text,
}

enum Language {
    Clojure,
    Guile,
    Hy,
    Janet,
    Lisp,
    Picolisp,
    Racket,
    Scheme,
}

pub struct Options {
    matches: getopts::Matches,
}

struct YesNoDefaultOption {
    name: &'static str,
    description: &'static str,
}

impl YesNoDefaultOption {
    fn add(&self, options: &mut getopts::Options) {
        options.optflag("", self.name, self.description);
        options.optflag(
            "",
            &format!("no-{}", self.name),
            &format!("do not {}", self.description),
        );
    }
}

const JANET_LONG_STRINGS_OPTION: YesNoDefaultOption = YesNoDefaultOption {
    name: "janet-long-strings",
    description: "recognize ``` janet-style long strings ```",
};
const LISP_BLOCK_COMMENTS_OPTION: YesNoDefaultOption = YesNoDefaultOption {
    name: "lisp-block-comments",
    description: "recognize #| lisp-style block commments |#.",
};
const LISP_VLINE_SYMBOLS_OPTION: YesNoDefaultOption = YesNoDefaultOption {
    name: "lisp-vline-symbols",
    description: "recognize |lisp-style vline symbol|s.",
};
const GUILE_BLOCK_COMMENTS_OPTION: YesNoDefaultOption = YesNoDefaultOption {
    name: "guile-block-comments",
    description: "recognize #!/guile/block/comments \\n!# )",
};
const SCHEME_SEXP_COMMENTS: YesNoDefaultOption = YesNoDefaultOption {
    name: "scheme-sexp-comments",
    description: "recognize #;( scheme sexp comments )",
};
const HY_BRACKET_STRINGS_OPTION : YesNoDefaultOption = YesNoDefaultOption {
    name: "hy-bracket-strings",
    description: "recognize #[hy-style[ bracket strings ]hy-style]```",
};

fn options() -> getopts::Options {
    let mut options = getopts::Options::new();
    options.optopt("", "comment-char", "(default: ';')", "CC");
    options.optopt("", "string-delimiters", "(default: '\"')", "DELIM");
    options.optflag("h", "help", "show this help message");
    options.optopt(
        "",
        "input-format",
        "'json', 'text' (default: 'text')",
        "FMT",
    );
    GUILE_BLOCK_COMMENTS_OPTION.add(&mut options);
    HY_BRACKET_STRINGS_OPTION.add(&mut options);
    JANET_LONG_STRINGS_OPTION.add(&mut options);
    options.optopt(
        "l",
        "language",
        "'clojure', 'guile', 'hy', 'janet', 'lisp', 'racket', 'scheme' (default: 'clojure')",
        "LANG",
    );
    LISP_BLOCK_COMMENTS_OPTION.add(&mut options);
    LISP_VLINE_SYMBOLS_OPTION.add(&mut options);
    options.optopt(
        "m",
        "mode",
        "parinfer mode (indent, paren, or smart) (default: smart)",
        "MODE",
    );
    options.optopt(
        "",
        "output-format",
        "'json', 'kakoune', 'text' (default: 'text')",
        "FMT",
    );
    SCHEME_SEXP_COMMENTS.add(&mut options);
    options
}

pub fn usage() -> String {
    options().usage("Usage: parinfer-rust [options]")
}

fn parse_language(language: Option<String>) -> Language {
    match language {
        Some(ref s) if s == "clojure" => Language::Clojure,
        Some(ref s) if s == "guile" => Language::Guile,
        Some(ref s) if s == "hy" => Language::Hy,
        Some(ref s) if s == "janet" => Language::Janet,
        Some(ref s) if s == "lisp" => Language::Lisp,
        Some(ref s) if s == "picolisp" => Language::Picolisp,
        Some(ref s) if s == "racket" => Language::Racket,
        Some(ref s) if s == "scheme" => Language::Scheme,
        None => Language::Clojure,
        // Unknown language.  LanguageFeatures kind of work for most lisps
        Some(_) => Language::Clojure,
    }
}

struct LanguageFeatures {
    comment_char: char,
    lisp_vline_symbols: bool,
    lisp_block_comments: bool,
    guile_block_comments: bool,
    scheme_sexp_comments: bool,
    janet_long_strings: bool,
    hy_bracket_strings: bool,
}

impl LanguageFeatures {
    fn for_language(language: Language) -> Self {
        let common = Self {
            comment_char: ';',
            lisp_vline_symbols: false,
            lisp_block_comments: false,
            guile_block_comments: false,
            scheme_sexp_comments: false,
            janet_long_strings: false,
            hy_bracket_strings: false,
        };
        match language {
            Language::Clojure => Self {
                ..common
            },
            Language::Guile => Self {
                lisp_vline_symbols: true,
                lisp_block_comments: true,
                guile_block_comments: true,
                scheme_sexp_comments: true,
                ..common
            },
            Language::Hy => Self {
                hy_bracket_strings: true,
                ..common
            },
            Language::Janet => Self {
                comment_char: '#',
                janet_long_strings: true,
                ..common
            },
            Language::Lisp => Self {
                lisp_vline_symbols: true,
                lisp_block_comments: true,
                ..common
            },
            Language::Picolisp => Self {
                comment_char: '#',
                lisp_vline_symbols: true,
                lisp_block_comments: true,
                ..common
            },
            Language::Racket => Self {
                lisp_vline_symbols: true,
                lisp_block_comments: true,
                scheme_sexp_comments: true,
                ..common
            },
            Language::Scheme => Self {
                lisp_vline_symbols: true,
                lisp_block_comments: true,
                scheme_sexp_comments: true,
                ..common
            },
        }
    }
}

impl Options {
    pub fn parse(args: &[String]) -> Result<Options, String> {
        options()
            .parse(args)
            .map(|m| Options { matches: m })
            .map_err(|e| e.to_string())
    }

    pub fn want_help(&self) -> bool {
        self.matches.opt_present("h")
    }

    fn mode(&self) -> &'static str {
        match self.matches.opt_str("m") {
            None => "smart",
            Some(ref s) if s == "i" || s == "indent" => "indent",
            Some(ref s) if s == "p" || s == "paren" => "paren",
            Some(ref s) if s == "s" || s == "smart" => "smart",
            _ => panic!("invalid mode specified for `-m`"),
        }
    }

    fn input_type(&self) -> InputType {
        match self.matches.opt_str("input-format") {
            None => InputType::Text,
            Some(ref s) if s == "text" => InputType::Text,
            Some(ref s) if s == "json" => InputType::Json,
            Some(ref s) if s == "kakoune" => InputType::Kakoune,
            Some(ref s) => panic!("unknown input format `{}`", s),
        }
    }

    pub fn output_type(&self) -> OutputType {
        match self.matches.opt_str("output-format") {
            None => OutputType::Text,
            Some(ref s) if s == "text" => OutputType::Text,
            Some(ref s) if s == "json" => OutputType::Json,
            Some(ref s) if s == "kakoune" => OutputType::Kakoune,
            Some(ref s) => panic!("unknown output fomrat `{}`", s),
        }
    }

    fn comment_char(&self) -> Option<char> {
        match self.matches.opt_str("comment-char") {
            None => None,
            Some(ref s) if s.chars().count() == 1 => Some(s.chars().next().unwrap()),
            Some(ref _s) => panic!("comment character must be a single character"),
        }
    }

    fn string_delimiters(&self) -> Vec<String> {
        let delims = self.matches.opt_strs("string-delimiters");
        if delims.is_empty() {
            vec!["\"".to_string()]
        } else {
            delims
        }
    }

    fn invertible_flag(&self, name: &str) -> Option<bool> {
        if self.matches.opt_present(name) {
            Some(true)
        } else if self.matches.opt_present(&format!("no-{}", name)) {
            Some(false)
        } else {
            None
        }
    }

    fn janet_long_strings(&self) -> Option<bool> {
        self.invertible_flag("janet-long-strings")
    }

    fn lisp_vline_symbols(&self) -> Option<bool> {
        self.invertible_flag("lisp-vline-symbols")
    }

    fn lisp_block_comments(&self) -> Option<bool> {
        self.invertible_flag("lisp-block-comments")
    }

    fn guile_block_comments(&self) -> Option<bool> {
        self.invertible_flag("guile-block-comments")
    }

    fn scheme_sexp_comments(&self) -> Option<bool> {
        self.invertible_flag("scheme-sexp-comments")
    }

    fn hy_bracket_strings(&self) -> Option<bool> {
        self.invertible_flag("hy-bracket-strings")
    }

    pub fn request(&self, input: &mut dyn Read) -> io::Result<Request> {
        match self.input_type() {
            InputType::Text => {
                let LanguageFeatures {
                    comment_char,
                    lisp_vline_symbols,
                    lisp_block_comments,
                    guile_block_comments,
                    scheme_sexp_comments,
                    janet_long_strings,
                    hy_bracket_strings,
                } = LanguageFeatures::for_language(parse_language(self.matches.opt_str("language")));
                let mut text = String::new();
                input.read_to_string(&mut text)?;
                Ok(Request {
                    mode: String::from(self.mode()),
                    text,
                    options: types::Options {
                        changes: vec![],
                        cursor_x: None,
                        cursor_line: None,
                        prev_text: None,
                        prev_cursor_x: None,
                        prev_cursor_line: None,
                        comment_char: self.comment_char().unwrap_or(comment_char),
                        string_delimiters: self.string_delimiters(),
                        selection_start_line: None,
                        lisp_vline_symbols: self.lisp_vline_symbols().unwrap_or(lisp_vline_symbols),
                        lisp_block_comments: self
                            .lisp_block_comments()
                            .unwrap_or(lisp_block_comments),
                        guile_block_comments: self
                            .guile_block_comments()
                            .unwrap_or(guile_block_comments),
                        scheme_sexp_comments: self
                            .scheme_sexp_comments()
                            .unwrap_or(scheme_sexp_comments),
                        janet_long_strings: self.janet_long_strings().unwrap_or(janet_long_strings),
                        hy_bracket_strings: self.hy_bracket_strings().unwrap_or(hy_bracket_strings),
                    },
                })
            }
            InputType::Kakoune => {
                let LanguageFeatures {
                    comment_char,
                    lisp_vline_symbols,
                    lisp_block_comments,
                    guile_block_comments,
                    scheme_sexp_comments,
                    janet_long_strings,
                    hy_bracket_strings,
                } = LanguageFeatures::for_language(parse_language(env::var("kak_opt_filetype").ok()));
                Ok(Request {
                    mode: String::from(self.mode()),
                    text: env::var("kak_selection").unwrap(),
                    options: types::Options {
                        changes: vec![],
                        cursor_x: env::var("kak_opt_parinfer_cursor_char_column")
                            .map(|s| s.parse::<Column>().unwrap() - 1)
                            .ok(),
                        cursor_line: env::var("kak_opt_parinfer_cursor_line")
                            .map(|s| s.parse::<LineNumber>().unwrap() - 1)
                            .ok(),
                        prev_text: env::var("kak_opt_parinfer_previous_text").ok(),
                        prev_cursor_x: env::var("kak_opt_parinfer_previous_cursor_char_column")
                            .map(|s| s.parse::<Column>().unwrap() - 1)
                            .ok(),
                        prev_cursor_line: env::var("kak_opt_parinfer_previous_cursor_line")
                            .map(|s| s.parse::<LineNumber>().unwrap() - 1)
                            .ok(),
                        comment_char: self.comment_char().unwrap_or(comment_char),
                        string_delimiters: self.string_delimiters(),
                        selection_start_line: None,
                        lisp_vline_symbols,
                        lisp_block_comments,
                        guile_block_comments,
                        scheme_sexp_comments,
                        janet_long_strings,
                        hy_bracket_strings,
                    },
                })
            }
            InputType::Json => {
                let mut text = String::new();
                input.read_to_string(&mut text)?;
                Ok(serde_json::from_str(&text)?)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn for_args(args: &[&str]) -> Request {
        let input = Vec::new();
        let string_args = args
            .iter()
            .map(|&s| String::from(s))
            .collect::<Vec<String>>();
        let request = Options::parse(&string_args)
            .expect("unable to parse options")
            .request(&mut input.as_slice())
            .expect("unable to make request");
        request
    }

    #[test]
    fn language_option_sets_defaults() {
        let clojure = for_args(&["--language=clojure"]);
        let scheme = for_args(&["--language=scheme"]);
        let janet = for_args(&["--language=janet"]);
        let hy = for_args(&["--language=hy"]);

        assert!(!clojure.options.lisp_vline_symbols);
        assert!(scheme.options.lisp_vline_symbols);
        assert!(!hy.options.lisp_vline_symbols);

        assert!(!clojure.options.janet_long_strings);
        assert!(!scheme.options.janet_long_strings);
        assert!(janet.options.janet_long_strings);
        assert!(!hy.options.janet_long_strings);

        assert!(!clojure.options.hy_bracket_strings);
        assert!(!scheme.options.hy_bracket_strings);
        assert!(!janet.options.hy_bracket_strings);
        assert!(hy.options.hy_bracket_strings);
    }

    #[test]
    fn lisp_vline_symbols() {
        assert!(!for_args(&[]).options.lisp_vline_symbols);
        assert!(
            for_args(&["--language=lisp"]).options.lisp_vline_symbols
        );
        assert!(
            for_args(&["--lisp-vline-symbols"])
                .options
                .lisp_vline_symbols
        );
        assert!(
            !for_args(&["--language=lisp", "--no-lisp-vline-symbols"])
                .options
                .lisp_vline_symbols
        );
    }

    #[test]
    fn lisp_block_comments() {
        assert!(!for_args(&[]).options.lisp_block_comments);
        assert!(
            for_args(&["--language=lisp"]).options.lisp_block_comments
        );
        assert!(
            for_args(&["--lisp-block-comments"])
                .options
                .lisp_block_comments
        );
        assert!(
            !for_args(&["--language=lisp", "--no-lisp-block-comments"])
                .options
                .lisp_block_comments
        );
    }
}