1
0
mirror of https://github.com/sharkdp/bat.git synced 2025-09-02 03:12:25 +01:00

Merge branch 'master' into fix_654_stdin_filename

This commit is contained in:
Kyle Criddle
2020-03-24 19:08:43 -06:00
118 changed files with 1836 additions and 1762 deletions

View File

@@ -1,41 +1,35 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::path::Path;
use syntect::dumps::{dump_to_file, from_binary, from_reader};
use syntect::highlighting::{Theme, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet, SyntaxSetBuilder};
use crate::dirs::PROJECT_DIRS;
use crate::errors::*;
use crate::inputfile::{InputFile, InputFileReader};
use crate::syntax_mapping::SyntaxMapping;
pub const BAT_THEME_DEFAULT: &str = "Monokai Extended";
use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
#[derive(Debug)]
pub struct HighlightingAssets {
pub syntax_set: SyntaxSet,
pub theme_set: ThemeSet,
pub(crate) syntax_set: SyntaxSet,
pub(crate) theme_set: ThemeSet,
fallback_theme: Option<&'static str>,
}
impl HighlightingAssets {
pub fn new() -> Self {
Self::from_cache().unwrap_or_else(|_| Self::from_binary())
pub fn default_theme() -> &'static str {
"Monokai Extended"
}
pub fn from_files(dir: Option<&Path>, start_empty: bool) -> Result<Self> {
let source_dir = dir.unwrap_or_else(|| PROJECT_DIRS.config_dir());
let mut theme_set = if start_empty {
pub fn from_files(source_dir: &Path, include_integrated_assets: bool) -> Result<Self> {
let mut theme_set = if include_integrated_assets {
Self::get_integrated_themeset()
} else {
ThemeSet {
themes: BTreeMap::new(),
}
} else {
Self::get_integrated_themeset()
};
let theme_dir = source_dir.join("themes");
@@ -48,7 +42,7 @@ impl HighlightingAssets {
);
}
let mut syntax_set_builder = if start_empty {
let mut syntax_set_builder = if !include_integrated_assets {
let mut builder = SyntaxSetBuilder::new();
builder.add_plain_text_syntax();
builder
@@ -69,15 +63,15 @@ impl HighlightingAssets {
Ok(HighlightingAssets {
syntax_set: syntax_set_builder.build(),
theme_set,
fallback_theme: None,
})
}
fn from_cache() -> Result<Self> {
let theme_set_path = theme_set_path();
let syntax_set_file = File::open(&syntax_set_path()).chain_err(|| {
pub fn from_cache(theme_set_path: &Path, syntax_set_path: &Path) -> Result<Self> {
let syntax_set_file = File::open(syntax_set_path).chain_err(|| {
format!(
"Could not load cached syntax set '{}'",
syntax_set_path().to_string_lossy()
syntax_set_path.to_string_lossy()
)
})?;
let syntax_set: SyntaxSet = from_reader(BufReader::new(syntax_set_file))
@@ -95,6 +89,7 @@ impl HighlightingAssets {
Ok(HighlightingAssets {
syntax_set,
theme_set,
fallback_theme: None,
})
}
@@ -106,18 +101,18 @@ impl HighlightingAssets {
from_binary(include_bytes!("../assets/themes.bin"))
}
fn from_binary() -> Self {
pub fn from_binary() -> Self {
let syntax_set = Self::get_integrated_syntaxset();
let theme_set = Self::get_integrated_themeset();
HighlightingAssets {
syntax_set,
theme_set,
fallback_theme: None,
}
}
pub fn save(&self, dir: Option<&Path>) -> Result<()> {
let target_dir = dir.unwrap_or_else(|| PROJECT_DIRS.cache_dir());
pub fn save_to_cache(&self, target_dir: &Path) -> Result<()> {
let _ = fs::create_dir_all(target_dir);
let theme_set_path = target_dir.join("themes.bin");
let syntax_set_path = target_dir.join("syntaxes.bin");
@@ -149,22 +144,36 @@ impl HighlightingAssets {
Ok(())
}
pub fn get_theme(&self, theme: &str) -> &Theme {
pub fn set_fallback_theme(&mut self, theme: &'static str) {
self.fallback_theme = Some(theme);
}
pub fn syntaxes(&self) -> &[SyntaxReference] {
self.syntax_set.syntaxes()
}
pub fn themes(&self) -> impl Iterator<Item = &String> {
self.theme_set.themes.keys()
}
pub(crate) fn get_theme(&self, theme: &str) -> &Theme {
match self.theme_set.themes.get(theme) {
Some(theme) => theme,
None => {
use ansi_term::Colour::Yellow;
eprintln!(
"{}: Unknown theme '{}', using default.",
Yellow.paint("[bat warning]"),
theme
);
&self.theme_set.themes[BAT_THEME_DEFAULT]
if theme != "" {
use ansi_term::Colour::Yellow;
eprintln!(
"{}: Unknown theme '{}', using default.",
Yellow.paint("[bat warning]"),
theme
);
}
&self.theme_set.themes[self.fallback_theme.unwrap_or(Self::default_theme())]
}
}
}
pub fn get_syntax(
pub(crate) fn get_syntax(
&self,
language: Option<&str>,
filename: InputFile,
@@ -175,25 +184,28 @@ impl HighlightingAssets {
(Some(language), _) => self.syntax_set.find_syntax_by_token(language),
(None, InputFile::Ordinary(filename)) => {
let path = Path::new(filename);
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
let file_name = mapping.replace(file_name);
let extension = mapping.replace(extension);
let ext_syntax = self
.syntax_set
.find_syntax_by_extension(&file_name)
.or_else(|| self.syntax_set.find_syntax_by_extension(&extension));
let line_syntax = if ext_syntax.is_none() {
String::from_utf8(reader.first_line.clone())
.ok()
.and_then(|l| self.syntax_set.find_syntax_by_first_line(&l))
} else {
None
};
let line_syntax = String::from_utf8(reader.first_line.clone())
.ok()
.and_then(|l| self.syntax_set.find_syntax_by_first_line(&l));
ext_syntax.or(line_syntax)
let absolute_path = path.canonicalize().ok().unwrap_or(path.to_owned());
match mapping.get_syntax_for(absolute_path) {
Some(MappingTarget::MapTo(syntax_name)) => {
// TODO: we should probably return an error here if this syntax can not be
// found. Currently, we just fall back to 'plain'.
self.syntax_set.find_syntax_by_name(syntax_name)
}
Some(MappingTarget::MapToUnknown) => line_syntax,
None => ext_syntax.or(line_syntax),
}
}
(None, InputFile::StdIn) => String::from_utf8(reader.first_line.clone())
.ok()
@@ -205,28 +217,120 @@ impl HighlightingAssets {
}
}
fn theme_set_path() -> PathBuf {
PROJECT_DIRS.cache_dir().join("themes.bin")
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::fs::File;
use std::io;
use std::io::Write;
fn syntax_set_path() -> PathBuf {
PROJECT_DIRS.cache_dir().join("syntaxes.bin")
}
use tempdir::TempDir;
pub fn config_dir() -> Cow<'static, str> {
PROJECT_DIRS.config_dir().to_string_lossy()
}
use crate::assets::HighlightingAssets;
use crate::inputfile::InputFile;
use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
pub fn cache_dir() -> Cow<'static, str> {
PROJECT_DIRS.cache_dir().to_string_lossy()
}
struct SyntaxDetectionTest<'a> {
assets: HighlightingAssets,
pub syntax_mapping: SyntaxMapping<'a>,
temp_dir: TempDir,
}
pub fn clear_assets() {
print!("Clearing theme set cache ... ");
fs::remove_file(theme_set_path()).ok();
println!("okay");
impl<'a> SyntaxDetectionTest<'a> {
fn new() -> Self {
SyntaxDetectionTest {
assets: HighlightingAssets::from_binary(),
syntax_mapping: SyntaxMapping::builtin(),
temp_dir: TempDir::new("bat_syntax_detection_tests")
.expect("creation of temporary directory"),
}
}
print!("Clearing syntax set cache ... ");
fs::remove_file(syntax_set_path()).ok();
println!("okay");
fn synax_for_file_with_content(&self, file_name: &str, first_line: &str) -> String {
let file_path = self.temp_dir.path().join(file_name);
{
let mut temp_file = File::create(&file_path).unwrap();
writeln!(temp_file, "{}", first_line).unwrap();
}
let input_file = InputFile::Ordinary(OsStr::new(&file_path));
let syntax = self.assets.get_syntax(
None,
input_file,
&mut input_file.get_reader(&io::stdin()).unwrap(),
&self.syntax_mapping,
);
syntax.name.clone()
}
fn syntax_for_file(&self, file_name: &str) -> String {
self.synax_for_file_with_content(file_name, "")
}
}
#[test]
fn syntax_detection_basic() {
let test = SyntaxDetectionTest::new();
assert_eq!(test.syntax_for_file("test.rs"), "Rust");
assert_eq!(test.syntax_for_file("test.cpp"), "C++");
assert_eq!(test.syntax_for_file("test.build"), "NAnt Build File");
assert_eq!(
test.syntax_for_file("PKGBUILD"),
"Bourne Again Shell (bash)"
);
assert_eq!(test.syntax_for_file(".bashrc"), "Bourne Again Shell (bash)");
assert_eq!(test.syntax_for_file("Makefile"), "Makefile");
}
#[test]
fn syntax_detection_well_defined_mapping_for_duplicate_extensions() {
let test = SyntaxDetectionTest::new();
assert_eq!(test.syntax_for_file("test.h"), "C++");
assert_eq!(test.syntax_for_file("test.sass"), "Sass");
assert_eq!(test.syntax_for_file("test.hs"), "Haskell (improved)");
assert_eq!(test.syntax_for_file("test.js"), "JavaScript (Babel)");
}
#[test]
fn syntax_detection_first_line() {
let test = SyntaxDetectionTest::new();
assert_eq!(
test.synax_for_file_with_content("my_script", "#!/bin/bash"),
"Bourne Again Shell (bash)"
);
assert_eq!(
test.synax_for_file_with_content("build", "#!/bin/bash"),
"Bourne Again Shell (bash)"
);
assert_eq!(
test.synax_for_file_with_content("my_script", "<?php"),
"PHP"
);
}
#[test]
fn syntax_detection_with_custom_mapping() {
let mut test = SyntaxDetectionTest::new();
assert_eq!(test.syntax_for_file("test.h"), "C++");
test.syntax_mapping
.insert("*.h", MappingTarget::MapTo("C"))
.ok();
assert_eq!(test.syntax_for_file("test.h"), "C");
}
#[test]
fn syntax_detection_is_case_sensitive() {
let mut test = SyntaxDetectionTest::new();
assert_ne!(test.syntax_for_file("README.MD"), "Markdown");
test.syntax_mapping
.insert("*.MD", MappingTarget::MapTo("Markdown"))
.ok();
assert_eq!(test.syntax_for_file("README.MD"), "Markdown");
}
}

View File

@@ -17,13 +17,12 @@ use console::Term;
use ansi_term;
use bat::{
assets::BAT_THEME_DEFAULT,
config::{
Config, HighlightedLineRanges, InputFile, LineRange, LineRanges, MappingTarget, OutputWrap,
PagingMode, StyleComponent, StyleComponents, SyntaxMapping,
},
errors::*,
inputfile::InputFile,
line_range::{LineRange, LineRanges},
style::{OutputComponent, OutputComponents, OutputWrap},
syntax_mapping::SyntaxMapping,
Config, PagingMode,
HighlightingAssets,
};
fn is_truecolor_terminal() -> bool {
@@ -79,7 +78,7 @@ impl App {
pub fn config(&self) -> Result<Config> {
let files = self.files();
let output_components = self.output_components()?;
let style_components = self.style_components()?;
let paging_mode = match self.matches.value_of("paging") {
Some("always") => PagingMode::Always,
@@ -105,17 +104,17 @@ impl App {
}
};
let mut syntax_mapping = SyntaxMapping::new();
let mut syntax_mapping = SyntaxMapping::builtin();
if let Some(values) = self.matches.values_of("map-syntax") {
for from_to in values {
let parts: Vec<_> = from_to.split(':').collect();
if parts.len() != 2 {
return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into());
return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is '<glob-pattern>:<syntax-name>'. For example: '*.cpp:C++'.".into());
}
syntax_mapping.insert(parts[0], parts[1]);
syntax_mapping.insert(parts[0], MappingTarget::MapTo(parts[1]))?;
}
}
@@ -158,7 +157,7 @@ impl App {
Some("character") => OutputWrap::Character,
Some("never") => OutputWrap::None,
Some("auto") | _ => {
if output_components.plain() {
if style_components.plain() {
OutputWrap::None
} else {
OutputWrap::Character
@@ -188,7 +187,7 @@ impl App {
.or_else(|| env::var("BAT_TABS").ok())
.and_then(|t| t.parse().ok())
.unwrap_or(
if output_components.plain() && paging_mode == PagingMode::Never {
if style_components.plain() && paging_mode == PagingMode::Never {
0
} else {
4
@@ -201,33 +200,34 @@ impl App {
.or_else(|| env::var("BAT_THEME").ok())
.map(|s| {
if s == "default" {
String::from(BAT_THEME_DEFAULT)
String::from(HighlightingAssets::default_theme())
} else {
s
}
})
.unwrap_or_else(|| String::from(BAT_THEME_DEFAULT)),
line_ranges: LineRanges::from(
self.matches
.values_of("line-range")
.map(|vs| vs.map(LineRange::from).collect())
.transpose()?
.unwrap_or_else(|| vec![]),
),
output_components,
.unwrap_or_else(|| String::from(HighlightingAssets::default_theme())),
line_ranges: self
.matches
.values_of("line-range")
.map(|vs| vs.map(LineRange::from).collect())
.transpose()?
.map(LineRanges::from)
.unwrap_or_default(),
style_components,
syntax_mapping,
pager: self.matches.value_of("pager"),
use_italic_text: match self.matches.value_of("italic-text") {
Some("always") => true,
_ => false,
},
highlight_lines: LineRanges::from(
self.matches
.values_of("highlight-line")
.map(|ws| ws.map(LineRange::from).collect())
.transpose()?
.unwrap_or_else(|| vec![LineRange { lower: 0, upper: 0 }]),
),
highlighted_lines: self
.matches
.values_of("highlight-line")
.map(|ws| ws.map(LineRange::from).collect())
.transpose()?
.map(LineRanges::from)
.map(|lr| HighlightedLineRanges(lr))
.unwrap_or_default(),
filenames: self
.matches
.values_of("file-name")
@@ -252,23 +252,23 @@ impl App {
.unwrap_or_else(|| vec![InputFile::StdIn])
}
fn output_components(&self) -> Result<OutputComponents> {
fn style_components(&self) -> Result<StyleComponents> {
let matches = &self.matches;
Ok(OutputComponents(
Ok(StyleComponents(
if matches.value_of("decorations") == Some("never") {
HashSet::new()
} else if matches.is_present("number") {
[OutputComponent::Numbers].iter().cloned().collect()
[StyleComponent::Numbers].iter().cloned().collect()
} else if matches.is_present("plain") {
[OutputComponent::Plain].iter().cloned().collect()
[StyleComponent::Plain].iter().cloned().collect()
} else {
let env_style_components: Option<Vec<OutputComponent>> = env::var("BAT_STYLE")
let env_style_components: Option<Vec<StyleComponent>> = env::var("BAT_STYLE")
.ok()
.map(|style_str| {
style_str
.split(',')
.map(|x| OutputComponent::from_str(&x))
.collect::<Result<Vec<OutputComponent>>>()
.map(|x| StyleComponent::from_str(&x))
.collect::<Result<Vec<StyleComponent>>>()
})
.transpose()?;
@@ -277,12 +277,12 @@ impl App {
.map(|styles| {
styles
.split(',')
.map(|style| style.parse::<OutputComponent>())
.map(|style| style.parse::<StyleComponent>())
.filter_map(|style| style.ok())
.collect::<Vec<_>>()
})
.or(env_style_components)
.unwrap_or_else(|| vec![OutputComponent::Full])
.unwrap_or_else(|| vec![StyleComponent::Full])
.into_iter()
.map(|style| style.components(self.interactive_output))
.fold(HashSet::new(), |mut acc, components| {

38
src/bin/bat/assets.rs Normal file
View File

@@ -0,0 +1,38 @@
use std::borrow::Cow;
use std::fs;
use std::path::PathBuf;
use crate::directories::PROJECT_DIRS;
use bat::HighlightingAssets;
fn theme_set_path() -> PathBuf {
PROJECT_DIRS.cache_dir().join("themes.bin")
}
fn syntax_set_path() -> PathBuf {
PROJECT_DIRS.cache_dir().join("syntaxes.bin")
}
pub fn config_dir() -> Cow<'static, str> {
PROJECT_DIRS.config_dir().to_string_lossy()
}
pub fn cache_dir() -> Cow<'static, str> {
PROJECT_DIRS.cache_dir().to_string_lossy()
}
pub fn clear_assets() {
print!("Clearing theme set cache ... ");
fs::remove_file(theme_set_path()).ok();
println!("okay");
print!("Clearing syntax set cache ... ");
fs::remove_file(syntax_set_path()).ok();
println!("okay");
}
pub fn assets_from_cache_or_binary() -> HighlightingAssets {
HighlightingAssets::from_cache(&theme_set_path(), &syntax_set_path())
.unwrap_or(HighlightingAssets::from_binary())
}

View File

@@ -260,13 +260,13 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
.multiple(true)
.takes_value(true)
.number_of_values(1)
.value_name("from:to")
.help("Map a file extension or name to an existing syntax.")
.value_name("glob:syntax")
.help("Use the specified syntax for files matching the glob pattern ('*.cpp:C++').")
.long_help(
"Map a file extension or file name to an existing syntax (specified by a file \
extension or file name). For example, to highlight *.build files with the \
Python syntax, use '-m build:py'. To highlight files named '.myignore' with \
the Git Ignore syntax, use '-m .myignore:gitignore'.",
"Map a glob pattern to an existing syntax name. The glob pattern is matched \
on the full path and the filename. For example, to highlight *.build files \
with the Python syntax, use -m '*.build:Python'. To highlight files named \
'.myignore' with the Git Ignore syntax, use -m '.myignore:Git Ignore'.",
)
.takes_value(true),
)
@@ -293,7 +293,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
.arg(
Arg::with_name("style")
.long("style")
.value_name("style-components")
.value_name("components")
// Need to turn this off for overrides_with to work as we want. See the bottom most
// example at https://docs.rs/clap/2.32.0/clap/struct.Arg.html#method.overrides_with
.use_delimiter(false)

View File

@@ -5,7 +5,7 @@ use std::path::PathBuf;
use shell_words;
use bat::dirs::PROJECT_DIRS;
use crate::directories::PROJECT_DIRS;
pub fn config_file() -> PathBuf {
env::var("BAT_CONFIG_PATH")

View File

@@ -1,8 +1,8 @@
use crate::dirs_rs;
use std::env;
use std::path::{Path, PathBuf};
#[cfg(target_os = "macos")]
use std::env;
use dirs;
use lazy_static::lazy_static;
/// Wrapper for 'dirs' that treats MacOS more like Linux, by following the XDG specification.
/// This means that the `XDG_CACHE_HOME` and `XDG_CONFIG_HOME` environment variables are
@@ -14,25 +14,16 @@ pub struct BatProjectDirs {
impl BatProjectDirs {
fn new() -> Option<BatProjectDirs> {
#[cfg(target_os = "macos")]
let cache_dir_op = env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| dirs_rs::home_dir().map(|d| d.join(".cache")));
#[cfg(not(target_os = "macos"))]
let cache_dir_op = dirs_rs::cache_dir();
let cache_dir = cache_dir_op.map(|d| d.join("bat"))?;
let cache_dir = BatProjectDirs::get_cache_dir()?;
#[cfg(target_os = "macos")]
let config_dir_op = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| dirs_rs::home_dir().map(|d| d.join(".config")));
.or_else(|| dirs::home_dir().map(|d| d.join(".config")));
#[cfg(not(target_os = "macos"))]
let config_dir_op = dirs_rs::config_dir();
let config_dir_op = dirs::config_dir();
let config_dir = config_dir_op.map(|d| d.join("bat"))?;
@@ -42,6 +33,25 @@ impl BatProjectDirs {
})
}
pub fn get_cache_dir() -> Option<PathBuf> {
// on all OS prefer BAT_CACHE_PATH if set
let cache_dir_op = env::var_os("BAT_CACHE_PATH").map(PathBuf::from);
if cache_dir_op.is_some() {
return cache_dir_op;
}
#[cfg(target_os = "macos")]
let cache_dir_op = env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| dirs::home_dir().map(|d| d.join(".cache")));
#[cfg(not(target_os = "macos"))]
let cache_dir_op = dirs::cache_dir();
cache_dir_op.map(|d| d.join("bat"))
}
pub fn cache_dir(&self) -> &Path {
&self.cache_dir
}

View File

@@ -4,9 +4,13 @@
#[macro_use]
extern crate clap;
extern crate dirs as dirs_rs;
mod app;
mod assets;
mod clap_app;
mod config;
mod directories;
use std::collections::HashSet;
use std::ffi::OsStr;
@@ -19,25 +23,31 @@ use ansi_term::Colour::Green;
use ansi_term::Style;
use crate::{app::App, config::config_file};
use bat::controller::Controller;
use assets::{assets_from_cache_or_binary, cache_dir, clear_assets, config_dir};
use bat::Controller;
use directories::PROJECT_DIRS;
use bat::{
assets::{cache_dir, clear_assets, config_dir, HighlightingAssets},
config::{Config, InputFile, StyleComponent, StyleComponents},
errors::*,
inputfile::InputFile,
style::{OutputComponent, OutputComponents},
Config,
HighlightingAssets,
};
fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> {
if matches.is_present("build") {
let source_dir = matches.value_of("source").map(Path::new);
let target_dir = matches.value_of("target").map(Path::new);
let source_dir = matches
.value_of("source")
.map(Path::new)
.unwrap_or_else(|| PROJECT_DIRS.config_dir());
let target_dir = matches
.value_of("target")
.map(Path::new)
.unwrap_or_else(|| PROJECT_DIRS.cache_dir());
let blank = matches.is_present("blank");
let assets = HighlightingAssets::from_files(source_dir, blank)?;
assets.save(target_dir)?;
let assets = HighlightingAssets::from_files(source_dir, !blank)?;
assets.save_to_cache(target_dir)?;
} else if matches.is_present("clear") {
clear_assets();
}
@@ -46,9 +56,8 @@ fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> {
}
pub fn list_languages(config: &Config) -> Result<()> {
let assets = HighlightingAssets::new();
let assets = assets_from_cache_or_binary();
let mut languages = assets
.syntax_set
.syntaxes()
.iter()
.filter(|syntax| !syntax.hidden && !syntax.file_extensions.is_empty())
@@ -109,19 +118,18 @@ pub fn list_languages(config: &Config) -> Result<()> {
}
pub fn list_themes(cfg: &Config) -> Result<()> {
let assets = HighlightingAssets::new();
let themes = &assets.theme_set.themes;
let assets = assets_from_cache_or_binary();
let mut config = cfg.clone();
let mut style = HashSet::new();
style.insert(OutputComponent::Plain);
style.insert(StyleComponent::Plain);
config.files = vec![InputFile::ThemePreviewFile];
config.output_components = OutputComponents(style);
config.style_components = StyleComponents(style);
let stdout = io::stdout();
let mut stdout = stdout.lock();
if config.colored_output {
for (theme, _) in themes.iter() {
for theme in assets.themes() {
writeln!(
stdout,
"Theme: {}\n",
@@ -132,7 +140,7 @@ pub fn list_themes(cfg: &Config) -> Result<()> {
writeln!(stdout)?;
}
} else {
for (theme, _) in themes.iter() {
for theme in assets.themes() {
writeln!(stdout, "{}", theme)?;
}
}
@@ -141,7 +149,7 @@ pub fn list_themes(cfg: &Config) -> Result<()> {
}
fn run_controller(config: &Config) -> Result<bool> {
let assets = HighlightingAssets::new();
let assets = assets_from_cache_or_binary();
let controller = Controller::new(&config, &assets);
controller.run()
}
@@ -199,7 +207,7 @@ fn main() {
match result {
Err(error) => {
handle_error(&error);
default_error_handler(&error);
process::exit(1);
}
Ok(false) => {

96
src/config.rs Normal file
View File

@@ -0,0 +1,96 @@
pub use crate::inputfile::InputFile;
pub use crate::line_range::{HighlightedLineRanges, LineRange, LineRanges};
pub use crate::style::{StyleComponent, StyleComponents};
pub use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
pub use crate::wrap::OutputWrap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PagingMode {
Always,
QuitIfOneScreen,
Never,
}
impl Default for PagingMode {
fn default() -> Self {
PagingMode::Never
}
}
#[derive(Debug, Clone, Default)]
pub struct Config<'a> {
/// List of files to print
pub files: Vec<InputFile<'a>>,
/// The explicitly configured language, if any
pub language: Option<&'a str>,
/// Whether or not to show/replace non-printable characters like space, tab and newline.
pub show_nonprintable: bool,
/// The character width of the terminal
pub term_width: usize,
/// The width of tab characters.
/// Currently, a value of 0 will cause tabs to be passed through without expanding them.
pub tab_width: usize,
/// Whether or not to simply loop through all input (`cat` mode)
pub loop_through: bool,
/// Whether or not the output should be colorized
pub colored_output: bool,
/// Whether or not the output terminal supports true color
pub true_color: bool,
/// Style elements (grid, line numbers, ...)
pub style_components: StyleComponents,
/// Text wrapping mode
pub output_wrap: OutputWrap,
/// Pager or STDOUT
pub paging_mode: PagingMode,
/// Specifies the lines that should be printed
pub line_ranges: LineRanges,
/// The syntax highlighting theme
pub theme: String,
/// File extension/name mappings
pub syntax_mapping: SyntaxMapping<'a>,
/// Command to start the pager
pub pager: Option<&'a str>,
/// Whether or not to use ANSI italics
pub use_italic_text: bool,
/// Ranges of lines which should be highlighted with a special background color
pub highlighted_lines: HighlightedLineRanges,
/// Name of file to display when printing
pub filenames: Option<Vec<&'a str>>,
}
#[test]
fn default_config_should_include_all_lines() {
use crate::line_range::RangeCheckResult;
assert_eq!(
Config::default().line_ranges.check(17),
RangeCheckResult::InRange
);
}
#[test]
fn default_config_should_highlight_no_lines() {
use crate::line_range::RangeCheckResult;
assert_ne!(
Config::default().highlighted_lines.0.check(17),
RangeCheckResult::InRange
);
}

View File

@@ -2,12 +2,12 @@ use std::io::{self, Write};
use std::path::Path;
use crate::assets::HighlightingAssets;
use crate::config::{Config, PagingMode};
use crate::errors::*;
use crate::inputfile::{InputFile, InputFileReader};
use crate::line_range::{LineRanges, RangeCheckResult};
use crate::output::OutputType;
use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
use crate::{Config, PagingMode};
pub struct Controller<'a> {
config: &'a Config<'a>,
@@ -20,6 +20,10 @@ impl<'b> Controller<'b> {
}
pub fn run(&self) -> Result<bool> {
self.run_with_error_handler(default_error_handler)
}
pub fn run_with_error_handler(&self, handle_error: impl Fn(&Error)) -> Result<bool> {
// Do not launch the pager if NONE of the input files exist
let mut paging_mode = self.config.paging_mode;
if self.config.paging_mode != PagingMode::Never {
@@ -93,7 +97,7 @@ impl<'b> Controller<'b> {
input_file: InputFile<'a>,
file_name: Option<&str>,
) -> Result<()> {
if !reader.first_line.is_empty() || self.config.output_components.header() {
if !reader.first_line.is_empty() || self.config.style_components.header() {
printer.print_header(writer, input_file, file_name)?;
}
@@ -120,7 +124,7 @@ impl<'b> Controller<'b> {
while reader.read_line(&mut line_buffer)? {
match line_ranges.check(line_number) {
RangeCheckResult::OutsideRange => {
RangeCheckResult::BeforeOrBetweenRanges => {
// Call the printer in case we need to call the syntax highlighter
// for this line. However, set `out_of_range` to `true`.
printer.print_line(true, writer, line_number, &line_buffer)?;
@@ -128,7 +132,7 @@ impl<'b> Controller<'b> {
}
RangeCheckResult::InRange => {
if self.config.output_components.snip() {
if self.config.style_components.snip() {
if first_range {
first_range = false;
mid_range = true;

25
src/errors.rs Normal file
View File

@@ -0,0 +1,25 @@
use error_chain::error_chain;
error_chain! {
foreign_links {
Clap(::clap::Error);
Io(::std::io::Error);
SyntectError(::syntect::LoadingError);
ParseIntError(::std::num::ParseIntError);
GlobParsingError(::globset::Error);
}
}
pub fn default_error_handler(error: &Error) {
match error {
Error(ErrorKind::Io(ref io_error), _)
if io_error.kind() == ::std::io::ErrorKind::BrokenPipe =>
{
::std::process::exit(0);
}
_ => {
use ansi_term::Colour::Red;
eprintln!("{}: {}", Red.paint("[bat error]"), error);
}
};
}

View File

@@ -10,8 +10,8 @@ const THEME_PREVIEW_FILE: &[u8] = include_bytes!("../assets/theme_preview.rs");
pub struct InputFileReader<'a> {
inner: Box<dyn BufRead + 'a>,
pub first_line: Vec<u8>,
pub content_type: Option<ContentType>,
pub(crate) first_line: Vec<u8>,
pub(crate) content_type: Option<ContentType>,
}
impl<'a> InputFileReader<'a> {
@@ -36,7 +36,7 @@ impl<'a> InputFileReader<'a> {
}
}
pub fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
pub(crate) fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
if self.first_line.is_empty() {
let res = self.inner.read_until(b'\n', buf).map(|size| size > 0)?;
@@ -60,7 +60,7 @@ pub enum InputFile<'a> {
}
impl<'a> InputFile<'a> {
pub fn get_reader(&self, stdin: &'a io::Stdin) -> Result<InputFileReader> {
pub(crate) fn get_reader(&self, stdin: &'a io::Stdin) -> Result<InputFileReader> {
match self {
InputFile::StdIn => Ok(InputFileReader::new(stdin.lock())),
InputFile::Ordinary(filename) => {

View File

@@ -1,12 +1,6 @@
// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate lazy_static;
extern crate ansi_term;
extern crate atty;
extern crate console;
@@ -18,118 +12,23 @@ extern crate shell_words;
extern crate syntect;
extern crate wild;
pub mod assets;
pub mod controller;
pub(crate) mod assets;
pub mod config;
pub(crate) mod controller;
mod decorations;
mod diff;
pub mod dirs;
pub mod inputfile;
pub mod errors;
pub(crate) mod inputfile;
mod less;
pub mod line_range;
pub(crate) mod line_range;
mod output;
mod preprocessor;
mod printer;
pub mod style;
pub mod syntax_mapping;
pub(crate) mod printer;
pub(crate) mod style;
pub(crate) mod syntax_mapping;
mod terminal;
pub(crate) mod wrap;
pub mod errors {
error_chain! {
foreign_links {
Clap(::clap::Error);
Io(::std::io::Error);
SyntectError(::syntect::LoadingError);
ParseIntError(::std::num::ParseIntError);
}
}
pub fn handle_error(error: &Error) {
match error {
Error(ErrorKind::Io(ref io_error), _)
if io_error.kind() == ::std::io::ErrorKind::BrokenPipe =>
{
::std::process::exit(0);
}
_ => {
use ansi_term::Colour::Red;
eprintln!("{}: {}", Red.paint("[bat error]"), error);
}
};
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PagingMode {
Always,
QuitIfOneScreen,
Never,
}
impl Default for PagingMode {
fn default() -> Self {
PagingMode::Never
}
}
use inputfile::InputFile;
use line_range::LineRanges;
use style::{OutputComponents, OutputWrap};
use syntax_mapping::SyntaxMapping;
#[derive(Debug, Clone, Default)]
pub struct Config<'a> {
/// List of files to print
pub files: Vec<InputFile<'a>>,
/// The explicitly configured language, if any
pub language: Option<&'a str>,
/// Whether or not to show/replace non-printable characters like space, tab and newline.
pub show_nonprintable: bool,
/// The character width of the terminal
pub term_width: usize,
/// The width of tab characters.
/// Currently, a value of 0 will cause tabs to be passed through without expanding them.
pub tab_width: usize,
/// Whether or not to simply loop through all input (`cat` mode)
pub loop_through: bool,
/// Whether or not the output should be colorized
pub colored_output: bool,
/// Whether or not the output terminal supports true color
pub true_color: bool,
/// Style elements (grid, line numbers, ...)
pub output_components: OutputComponents,
/// Text wrapping mode
pub output_wrap: OutputWrap,
/// Pager or STDOUT
pub paging_mode: PagingMode,
/// Specifies the lines that should be printed
pub line_ranges: LineRanges,
/// The syntax highlighting theme
pub theme: String,
/// File extension/name mappings
pub syntax_mapping: SyntaxMapping,
/// Command to start the pager
pub pager: Option<&'a str>,
/// Whether or not to use ANSI italics
pub use_italic_text: bool,
/// Lines to highlight
pub highlight_lines: LineRanges,
/// Name of file to display when printing
pub filenames: Option<Vec<&'a str>>,
}
pub use assets::HighlightingAssets;
pub use controller::Controller;
pub use printer::{InteractivePrinter, Printer, SimplePrinter};

View File

@@ -6,20 +6,22 @@ pub struct LineRange {
pub upper: usize,
}
impl LineRange {
pub fn from(range_raw: &str) -> Result<LineRange> {
LineRange::parse_range(range_raw)
}
pub fn new() -> LineRange {
impl Default for LineRange {
fn default() -> LineRange {
LineRange {
lower: usize::min_value(),
upper: usize::max_value(),
}
}
}
pub fn parse_range(range_raw: &str) -> Result<LineRange> {
let mut new_range = LineRange::new();
impl LineRange {
pub fn from(range_raw: &str) -> Result<LineRange> {
LineRange::parse_range(range_raw)
}
fn parse_range(range_raw: &str) -> Result<LineRange> {
let mut new_range = LineRange::default();
if range_raw.bytes().nth(0).ok_or("Empty line range")? == b':' {
new_range.upper = range_raw[1..].parse()?;
@@ -48,7 +50,7 @@ impl LineRange {
}
}
pub fn is_inside(&self, line: usize) -> bool {
pub(crate) fn is_inside(&self, line: usize) -> bool {
line >= self.lower && line <= self.upper
}
}
@@ -97,19 +99,27 @@ pub enum RangeCheckResult {
InRange,
// Before the first range or within two ranges
OutsideRange,
BeforeOrBetweenRanges,
// Line number is outside of all ranges and larger than the last range.
AfterLastRange,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct LineRanges {
ranges: Vec<LineRange>,
largest_upper_bound: usize,
}
impl LineRanges {
pub fn none() -> LineRanges {
LineRanges::from(vec![])
}
pub fn all() -> LineRanges {
LineRanges::from(vec![LineRange::default()])
}
pub fn from(ranges: Vec<LineRange>) -> LineRanges {
let largest_upper_bound = ranges
.iter()
@@ -122,17 +132,32 @@ impl LineRanges {
}
}
pub fn check(&self, line: usize) -> RangeCheckResult {
if self.ranges.is_empty() | self.ranges.iter().any(|r| r.is_inside(line)) {
pub(crate) fn check(&self, line: usize) -> RangeCheckResult {
if self.ranges.iter().any(|r| r.is_inside(line)) {
RangeCheckResult::InRange
} else if line < self.largest_upper_bound {
RangeCheckResult::OutsideRange
RangeCheckResult::BeforeOrBetweenRanges
} else {
RangeCheckResult::AfterLastRange
}
}
}
impl Default for LineRanges {
fn default() -> Self {
Self::all()
}
}
#[derive(Debug, Clone)]
pub struct HighlightedLineRanges(pub LineRanges);
impl Default for HighlightedLineRanges {
fn default() -> Self {
HighlightedLineRanges(LineRanges::none())
}
}
#[cfg(test)]
fn ranges(rs: &[&str]) -> LineRanges {
LineRanges::from(rs.iter().map(|r| LineRange::from(r).unwrap()).collect())
@@ -142,7 +167,7 @@ fn ranges(rs: &[&str]) -> LineRanges {
fn test_ranges_simple() {
let ranges = ranges(&["3:8"]);
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(2));
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(2));
assert_eq!(RangeCheckResult::InRange, ranges.check(5));
assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(9));
}
@@ -151,11 +176,11 @@ fn test_ranges_simple() {
fn test_ranges_advanced() {
let ranges = ranges(&["3:8", "11:20", "25:30"]);
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(2));
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(2));
assert_eq!(RangeCheckResult::InRange, ranges.check(5));
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(9));
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(9));
assert_eq!(RangeCheckResult::InRange, ranges.check(11));
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(22));
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(22));
assert_eq!(RangeCheckResult::InRange, ranges.check(28));
assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(31));
}
@@ -174,15 +199,22 @@ fn test_ranges_open_low() {
fn test_ranges_open_high() {
let ranges = ranges(&["3:", "2:5"]);
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(1));
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(1));
assert_eq!(RangeCheckResult::InRange, ranges.check(3));
assert_eq!(RangeCheckResult::InRange, ranges.check(5));
assert_eq!(RangeCheckResult::InRange, ranges.check(9));
}
#[test]
fn test_ranges_empty() {
let ranges = ranges(&[]);
fn test_ranges_all() {
let ranges = LineRanges::all();
assert_eq!(RangeCheckResult::InRange, ranges.check(1));
}
#[test]
fn test_ranges_none() {
let ranges = LineRanges::none();
assert_ne!(RangeCheckResult::InRange, ranges.check(1));
}

View File

@@ -6,9 +6,9 @@ use std::process::{Child, Command, Stdio};
use shell_words;
use crate::config::PagingMode;
use crate::errors::*;
use crate::less::retrieve_less_version;
use crate::PagingMode;
#[derive(Debug)]
pub enum OutputType {

View File

@@ -20,6 +20,7 @@ use encoding::{DecoderTrap, Encoding};
use unicode_width::UnicodeWidthChar;
use crate::assets::HighlightingAssets;
use crate::config::Config;
use crate::decorations::{
Decoration, GridBorderDecoration, LineChangesDecoration, LineNumberDecoration,
};
@@ -29,9 +30,8 @@ use crate::errors::*;
use crate::inputfile::{InputFile, InputFileReader};
use crate::line_range::RangeCheckResult;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::OutputWrap;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::Config;
use crate::wrap::OutputWrap;
pub trait Printer {
fn print_header(
@@ -126,11 +126,11 @@ impl<'a> InteractivePrinter<'a> {
// Create decorations.
let mut decorations: Vec<Box<dyn Decoration>> = Vec::new();
if config.output_components.numbers() {
if config.style_components.numbers() {
decorations.push(Box::new(LineNumberDecoration::new(&colors)));
}
if config.output_components.changes() {
if config.style_components.changes() {
decorations.push(Box::new(LineChangesDecoration::new(&colors)));
}
@@ -140,7 +140,7 @@ impl<'a> InteractivePrinter<'a> {
// The grid border decoration isn't added until after the panel_width calculation, since the
// print_horizontal_line, print_header, and print_footer functions all assume the panel
// width is without the grid border.
if config.output_components.grid() && !decorations.is_empty() {
if config.style_components.grid() && !decorations.is_empty() {
decorations.push(Box::new(GridBorderDecoration::new(&colors)));
}
@@ -162,7 +162,7 @@ impl<'a> InteractivePrinter<'a> {
None
} else {
// Get the Git modifications
line_changes = if config.output_components.changes() {
line_changes = if config.style_components.changes() {
match file {
InputFile::Ordinary(filename) => get_git_diff(filename),
_ => None,
@@ -216,7 +216,7 @@ impl<'a> InteractivePrinter<'a> {
text_truncated,
" ".repeat(self.panel_width - 1 - text_truncated.len())
);
if self.config.output_components.grid() {
if self.config.style_components.grid() {
format!("{}", text_filled)
} else {
format!("{}", text_filled)
@@ -240,7 +240,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
file: InputFile,
file_name: Option<&str>,
) -> Result<()> {
if !self.config.output_components.header() {
if !self.config.style_components.header() {
if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable {
let input = match file {
InputFile::Ordinary(filename) => format!(
@@ -259,14 +259,14 @@ impl<'a> Printer for InteractivePrinter<'a> {
input
)?;
} else {
if self.config.output_components.grid() {
if self.config.style_components.grid() {
self.print_horizontal_line(handle, '┬')?;
}
}
return Ok(());
}
if self.config.output_components.grid() {
if self.config.style_components.grid() {
self.print_horizontal_line(handle, '┬')?;
write!(
@@ -305,7 +305,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
mode
)?;
if self.config.output_components.grid() {
if self.config.style_components.grid() {
if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
self.print_horizontal_line(handle, '┼')?;
} else {
@@ -317,7 +317,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
fn print_footer(&mut self, handle: &mut dyn Write) -> Result<()> {
if self.config.output_components.grid()
if self.config.style_components.grid()
&& (self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable)
{
self.print_horizontal_line(handle, '┴')
@@ -395,7 +395,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
// Line highlighting
let highlight_this_line =
self.config.highlight_lines.check(line_number) == RangeCheckResult::InRange;
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
let background_color = self
.background_color_highlight

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use crate::errors::*;
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub enum OutputComponent {
pub enum StyleComponent {
Auto,
Changes,
Grid,
@@ -15,92 +15,80 @@ pub enum OutputComponent {
Plain,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub enum OutputWrap {
Character,
None,
}
impl Default for OutputWrap {
fn default() -> Self {
OutputWrap::None
}
}
impl OutputComponent {
pub fn components(self, interactive_terminal: bool) -> &'static [OutputComponent] {
impl StyleComponent {
pub fn components(self, interactive_terminal: bool) -> &'static [StyleComponent] {
match self {
OutputComponent::Auto => {
StyleComponent::Auto => {
if interactive_terminal {
OutputComponent::Full.components(interactive_terminal)
StyleComponent::Full.components(interactive_terminal)
} else {
OutputComponent::Plain.components(interactive_terminal)
StyleComponent::Plain.components(interactive_terminal)
}
}
OutputComponent::Changes => &[OutputComponent::Changes],
OutputComponent::Grid => &[OutputComponent::Grid],
OutputComponent::Header => &[OutputComponent::Header],
OutputComponent::Numbers => &[OutputComponent::Numbers],
OutputComponent::Snip => &[OutputComponent::Snip],
OutputComponent::Full => &[
OutputComponent::Changes,
OutputComponent::Grid,
OutputComponent::Header,
OutputComponent::Numbers,
OutputComponent::Snip,
StyleComponent::Changes => &[StyleComponent::Changes],
StyleComponent::Grid => &[StyleComponent::Grid],
StyleComponent::Header => &[StyleComponent::Header],
StyleComponent::Numbers => &[StyleComponent::Numbers],
StyleComponent::Snip => &[StyleComponent::Snip],
StyleComponent::Full => &[
StyleComponent::Changes,
StyleComponent::Grid,
StyleComponent::Header,
StyleComponent::Numbers,
StyleComponent::Snip,
],
OutputComponent::Plain => &[],
StyleComponent::Plain => &[],
}
}
}
impl FromStr for OutputComponent {
impl FromStr for StyleComponent {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"auto" => Ok(OutputComponent::Auto),
"changes" => Ok(OutputComponent::Changes),
"grid" => Ok(OutputComponent::Grid),
"header" => Ok(OutputComponent::Header),
"numbers" => Ok(OutputComponent::Numbers),
"snip" => Ok(OutputComponent::Snip),
"full" => Ok(OutputComponent::Full),
"plain" => Ok(OutputComponent::Plain),
"auto" => Ok(StyleComponent::Auto),
"changes" => Ok(StyleComponent::Changes),
"grid" => Ok(StyleComponent::Grid),
"header" => Ok(StyleComponent::Header),
"numbers" => Ok(StyleComponent::Numbers),
"snip" => Ok(StyleComponent::Snip),
"full" => Ok(StyleComponent::Full),
"plain" => Ok(StyleComponent::Plain),
_ => Err(format!("Unknown style '{}'", s).into()),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OutputComponents(pub HashSet<OutputComponent>);
pub struct StyleComponents(pub HashSet<StyleComponent>);
impl OutputComponents {
pub fn new(components: &[OutputComponent]) -> OutputComponents {
OutputComponents(components.iter().cloned().collect())
impl StyleComponents {
pub fn new(components: &[StyleComponent]) -> StyleComponents {
StyleComponents(components.iter().cloned().collect())
}
pub fn changes(&self) -> bool {
self.0.contains(&OutputComponent::Changes)
self.0.contains(&StyleComponent::Changes)
}
pub fn grid(&self) -> bool {
self.0.contains(&OutputComponent::Grid)
self.0.contains(&StyleComponent::Grid)
}
pub fn header(&self) -> bool {
self.0.contains(&OutputComponent::Header)
self.0.contains(&StyleComponent::Header)
}
pub fn numbers(&self) -> bool {
self.0.contains(&OutputComponent::Numbers)
self.0.contains(&StyleComponent::Numbers)
}
pub fn snip(&self) -> bool {
self.0.contains(&OutputComponent::Snip)
self.0.contains(&StyleComponent::Snip)
}
pub fn plain(&self) -> bool {
self.0.iter().all(|c| c == &OutputComponent::Plain)
self.0.iter().all(|c| c == &StyleComponent::Plain)
}
}

View File

@@ -1,35 +1,111 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
use crate::errors::Result;
use globset::{Candidate, GlobBuilder, GlobMatcher};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MappingTarget<'a> {
MapTo(&'a str),
MapToUnknown,
}
#[derive(Debug, Clone, Default)]
pub struct SyntaxMapping(HashMap<String, String>);
pub struct SyntaxMapping<'a> {
mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
}
impl SyntaxMapping {
pub fn new() -> SyntaxMapping {
impl<'a> SyntaxMapping<'a> {
pub fn empty() -> SyntaxMapping<'a> {
Default::default()
}
pub fn insert(&mut self, from: impl Into<String>, to: impl Into<String>) -> Option<String> {
self.0.insert(from.into(), to.into())
pub fn builtin() -> SyntaxMapping<'a> {
let mut mapping = Self::empty();
mapping.insert("*.h", MappingTarget::MapTo("C++")).unwrap();
mapping
.insert("build", MappingTarget::MapToUnknown)
.unwrap();
mapping
.insert("**/.ssh/config", MappingTarget::MapTo("SSH Config"))
.unwrap();
mapping
.insert(
"/etc/profile",
MappingTarget::MapTo("Bourne Again Shell (bash)"),
)
.unwrap();
mapping
}
pub fn replace<'a>(&self, input: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
let input = input.into();
match self.0.get(input.as_ref()) {
Some(s) => Cow::from(s.clone()),
None => input,
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
let glob = GlobBuilder::new(from)
.case_insensitive(false)
.literal_separator(true)
.build()?;
self.mappings.push((glob.compile_matcher(), to));
Ok(())
}
pub(crate) fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
let candidate = Candidate::new(path.as_ref());
let canddidate_filename = path.as_ref().file_name().map(Candidate::new);
for (ref glob, ref syntax) in self.mappings.iter().rev() {
if glob.is_match_candidate(&candidate)
|| canddidate_filename
.as_ref()
.map_or(false, |filename| glob.is_match_candidate(filename))
{
return Some(*syntax);
}
}
None
}
}
#[test]
fn basic() {
let mut map = SyntaxMapping::new();
map.insert("Cargo.lock", "toml");
map.insert(".ignore", ".gitignore");
let mut map = SyntaxMapping::empty();
map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML"))
.ok();
map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore"))
.ok();
assert_eq!("toml", map.replace("Cargo.lock"));
assert_eq!("other.lock", map.replace("other.lock"));
assert_eq!(
map.get_syntax_for("/path/to/Cargo.lock"),
Some(MappingTarget::MapTo("TOML"))
);
assert_eq!(map.get_syntax_for("/path/to/other.lock"), None);
assert_eq!(".gitignore", map.replace(".ignore"));
assert_eq!(
map.get_syntax_for("/path/to/.ignore"),
Some(MappingTarget::MapTo("Git Ignore"))
);
}
#[test]
fn user_can_override_builtin_mappings() {
let mut map = SyntaxMapping::builtin();
assert_eq!(
map.get_syntax_for("/etc/profile"),
Some(MappingTarget::MapTo("Bourne Again Shell (bash)"))
);
map.insert("/etc/profile", MappingTarget::MapTo("My Syntax"))
.ok();
assert_eq!(
map.get_syntax_for("/etc/profile"),
Some(MappingTarget::MapTo("My Syntax"))
);
}
#[test]
fn builtin_mappings() {
let map = SyntaxMapping::builtin();
assert_eq!(
map.get_syntax_for("/path/to/build"),
Some(MappingTarget::MapToUnknown)
);
}

11
src/wrap.rs Normal file
View File

@@ -0,0 +1,11 @@
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub enum OutputWrap {
Character,
None,
}
impl Default for OutputWrap {
fn default() -> Self {
OutputWrap::None
}
}