mirror of
https://github.com/starship/starship.git
synced 2025-01-23 06:39:47 +01:00
feat(format_string): Add syntax for positional segments
This commit is contained in:
parent
d81c353124
commit
71020b0397
@ -1,4 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub trait VariableHolder<T> {
|
||||
fn get_variables(&self) -> BTreeSet<T>;
|
||||
}
|
||||
|
||||
pub struct TextGroup<'a> {
|
||||
pub format: Vec<FormatElement<'a>>,
|
||||
@ -9,9 +14,34 @@ pub enum FormatElement<'a> {
|
||||
Text(Cow<'a, str>),
|
||||
Variable(Cow<'a, str>),
|
||||
TextGroup(TextGroup<'a>),
|
||||
Positional(Vec<FormatElement<'a>>),
|
||||
}
|
||||
|
||||
pub enum StyleElement<'a> {
|
||||
Text(Cow<'a, str>),
|
||||
Variable(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl<'a> VariableHolder<Cow<'a, str>> for FormatElement<'a> {
|
||||
fn get_variables(&self) -> BTreeSet<Cow<'a, str>> {
|
||||
match self {
|
||||
FormatElement::Variable(var) => {
|
||||
let mut variables = BTreeSet::new();
|
||||
variables.insert(var.clone());
|
||||
variables
|
||||
}
|
||||
FormatElement::TextGroup(textgroup) => textgroup.format.get_variables(),
|
||||
FormatElement::Positional(format) => format.get_variables(),
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> VariableHolder<Cow<'a, str>> for Vec<FormatElement<'a>> {
|
||||
fn get_variables(&self) -> BTreeSet<Cow<'a, str>> {
|
||||
self.iter().fold(BTreeSet::new(), |mut acc, el| {
|
||||
acc.extend(el.get_variables());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,18 @@ use super::model::*;
|
||||
#[grammar = "formatter/spec.pest"]
|
||||
struct IdentParser;
|
||||
|
||||
fn _parse_value(value: Pair<Rule>) -> FormatElement {
|
||||
match value.as_rule() {
|
||||
Rule::text => FormatElement::Text(_parse_text(value).into()),
|
||||
Rule::variable => FormatElement::Variable(_parse_variable(value).into()),
|
||||
Rule::textgroup => FormatElement::TextGroup(_parse_textgroup(value)),
|
||||
Rule::positional => {
|
||||
FormatElement::Positional(_parse_format(value.into_inner().next().unwrap()))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn _parse_textgroup(textgroup: Pair<Rule>) -> TextGroup {
|
||||
let mut inner_rules = textgroup.into_inner();
|
||||
let format = inner_rules.next().unwrap();
|
||||
@ -29,15 +41,7 @@ fn _parse_text(text: Pair<Rule>) -> String {
|
||||
}
|
||||
|
||||
fn _parse_format(format: Pair<Rule>) -> Vec<FormatElement> {
|
||||
format
|
||||
.into_inner()
|
||||
.map(|pair| match pair.as_rule() {
|
||||
Rule::text => FormatElement::Text(_parse_text(pair).into()),
|
||||
Rule::variable => FormatElement::Variable(_parse_variable(pair).into()),
|
||||
Rule::textgroup => FormatElement::TextGroup(_parse_textgroup(pair)),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect()
|
||||
format.into_inner().map(_parse_value).collect()
|
||||
}
|
||||
|
||||
fn _parse_style(style: Pair<Rule>) -> Vec<StyleElement> {
|
||||
@ -55,12 +59,7 @@ pub fn parse(format: &str) -> Result<Vec<FormatElement>, Error<Rule>> {
|
||||
IdentParser::parse(Rule::expression, format).map(|pairs| {
|
||||
pairs
|
||||
.take_while(|pair| pair.as_rule() != Rule::EOI)
|
||||
.map(|pair| match pair.as_rule() {
|
||||
Rule::text => FormatElement::Text(_parse_text(pair).into()),
|
||||
Rule::variable => FormatElement::Variable(_parse_variable(pair).into()),
|
||||
Rule::textgroup => FormatElement::TextGroup(_parse_textgroup(pair)),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.map(_parse_value)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
//
|
||||
// Should be started with SOI and ended with EOI, with a format string in it.
|
||||
expression = _{ SOI ~ value* ~ EOI }
|
||||
value = _{ text | variable | textgroup }
|
||||
value = _{ text | variable | textgroup | positional }
|
||||
|
||||
// Variable
|
||||
//
|
||||
@ -44,3 +44,8 @@ escaped_char = { "[" | "]" | "(" | ")" | "\\" | "$" }
|
||||
textgroup = { "[" ~ format ~ "]" ~ "(" ~ style ~ ")" }
|
||||
format = { value* }
|
||||
style = { (variable | string)* }
|
||||
|
||||
// Positional
|
||||
//
|
||||
// A positional format string that won't render if all the containing variables are empty.
|
||||
positional = { "(" ~ format ~ ")" }
|
||||
|
@ -2,6 +2,7 @@ use ansi_term::Style;
|
||||
use pest::error::Error;
|
||||
use rayon::prelude::*;
|
||||
use std::collections::BTreeMap;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use crate::config::parse_style_string;
|
||||
use crate::segment::Segment;
|
||||
@ -33,16 +34,22 @@ impl<'a> StringFormatter<'a> {
|
||||
pub fn new(format: &'a str) -> Result<Self, Error<Rule>> {
|
||||
parse(format)
|
||||
.map(|format| {
|
||||
let variables = _get_variables(&format);
|
||||
let variables = VariableMapType::from_iter(
|
||||
format
|
||||
.get_variables()
|
||||
.into_iter()
|
||||
.map(|key| (key.to_string(), None))
|
||||
.collect::<Vec<(String, Option<_>)>>(),
|
||||
);
|
||||
(format, variables)
|
||||
})
|
||||
.map(|(format, variables)| Self { format, variables })
|
||||
}
|
||||
|
||||
/// Maps variable name to its value
|
||||
pub fn map(mut self, mapper: impl Fn(&str) -> Option<String> + Sync) -> Self {
|
||||
pub fn map<T: Into<String>>(mut self, mapper: impl Fn(&str) -> Option<T> + Sync) -> Self {
|
||||
self.variables.par_iter_mut().for_each(|(key, value)| {
|
||||
*value = mapper(key).map(VariableValue::Plain);
|
||||
*value = mapper(key).map(|var| var.into()).map(VariableValue::Plain);
|
||||
});
|
||||
self
|
||||
}
|
||||
@ -105,9 +112,9 @@ impl<'a> StringFormatter<'a> {
|
||||
}
|
||||
FormatElement::Variable(name) => variables
|
||||
.get(name.as_ref())
|
||||
.map(|segments| {
|
||||
let value = segments.clone().unwrap_or_default();
|
||||
match value {
|
||||
.and_then(|segments| {
|
||||
let value = segments.clone()?;
|
||||
Some(match value {
|
||||
VariableValue::Styled(segments) => segments
|
||||
.into_iter()
|
||||
.map(|mut segment| {
|
||||
@ -122,9 +129,23 @@ impl<'a> StringFormatter<'a> {
|
||||
VariableValue::Plain(text) => {
|
||||
vec![_new_segment(name.to_string(), text, style)]
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
FormatElement::Positional(format) => {
|
||||
let should_show: bool = format.get_variables().iter().any(|var| {
|
||||
variables
|
||||
.get(var.as_ref())
|
||||
.map(|segments| segments.is_some())
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
if should_show {
|
||||
_parse_format(format, style, variables)
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -133,47 +154,6 @@ impl<'a> StringFormatter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract variable names from an array of `FormatElement` into a `BTreeMap`
|
||||
fn _get_variables<'a>(format: &[FormatElement<'a>]) -> VariableMapType {
|
||||
let mut variables: VariableMapType = Default::default();
|
||||
|
||||
fn _push_variables_from_textgroup<'a>(
|
||||
variables: &mut VariableMapType,
|
||||
textgroup: &'a TextGroup<'a>,
|
||||
) {
|
||||
for el in &textgroup.format {
|
||||
match el {
|
||||
FormatElement::Variable(name) => _push_variable(variables, name.as_ref()),
|
||||
FormatElement::TextGroup(textgroup) => {
|
||||
_push_variables_from_textgroup(variables, &textgroup)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for el in &textgroup.style {
|
||||
if let StyleElement::Variable(name) = el {
|
||||
_push_variable(variables, name.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _push_variable<'a>(variables: &mut VariableMapType, name: &'a str) {
|
||||
variables.insert(name.to_owned(), None);
|
||||
}
|
||||
|
||||
for el in format {
|
||||
match el {
|
||||
FormatElement::Variable(name) => _push_variable(&mut variables, name.as_ref()),
|
||||
FormatElement::TextGroup(textgroup) => {
|
||||
_push_variables_from_textgroup(&mut variables, &textgroup)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
variables
|
||||
}
|
||||
|
||||
/// Helper function to create a new segment
|
||||
fn _new_segment(name: String, value: String, style: Option<Style>) -> Segment {
|
||||
Segment {
|
||||
@ -317,6 +297,42 @@ mod tests {
|
||||
match_next!(result_iter, "styled_no_modifier", styled_no_modifier_style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_positional() {
|
||||
const FORMAT_STR: &str = "($some) should render but ($none) shouldn't";
|
||||
|
||||
let formatter = StringFormatter::new(FORMAT_STR)
|
||||
.unwrap()
|
||||
.map(|var| match var {
|
||||
"some" => Some("$some"),
|
||||
_ => None,
|
||||
});
|
||||
let result = formatter.parse(None);
|
||||
let mut result_iter = result.iter();
|
||||
match_next!(result_iter, "$some", None);
|
||||
match_next!(result_iter, " should render but ", None);
|
||||
match_next!(result_iter, " shouldn't", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_positional() {
|
||||
const FORMAT_STR: &str = "($some ($none)) and ($none ($some))";
|
||||
|
||||
let formatter = StringFormatter::new(FORMAT_STR)
|
||||
.unwrap()
|
||||
.map(|var| match var {
|
||||
"some" => Some("$some"),
|
||||
_ => None,
|
||||
});
|
||||
let result = formatter.parse(None);
|
||||
let mut result_iter = result.iter();
|
||||
match_next!(result_iter, "$some", None);
|
||||
match_next!(result_iter, " ", None);
|
||||
match_next!(result_iter, " and ", None);
|
||||
match_next!(result_iter, " ", None);
|
||||
match_next!(result_iter, "$some", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_error() {
|
||||
// brackets without escape
|
||||
|
Loading…
Reference in New Issue
Block a user