use std::fmt;
use crate::format;
use super::{
ansi_c::process_ansi_c_content, normalize_cmdsub_content, write_escaped_char,
write_escaped_word,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WordSegment {
Literal(String),
AnsiCQuote(String),
LocaleString(String),
ArithmeticSub(String),
CommandSubstitution(String),
ProcessSubstitution(char, String),
ParamExpansion(String),
SimpleVar(String),
BraceExpansion(String),
}
pub fn write_word_segments(f: &mut fmt::Formatter<'_>, segments: &[WordSegment]) -> fmt::Result {
for seg in segments {
write_one_segment(f, seg)?;
}
Ok(())
}
fn write_one_segment(f: &mut fmt::Formatter<'_>, seg: &WordSegment) -> fmt::Result {
match seg {
WordSegment::Literal(text)
| WordSegment::ParamExpansion(text)
| WordSegment::SimpleVar(text)
| WordSegment::BraceExpansion(text) => {
for ch in text.chars() {
write_escaped_char(f, ch)?;
}
Ok(())
}
WordSegment::AnsiCQuote(raw_content) => {
let chars: Vec<char> = raw_content.chars().collect();
let mut pos = 0;
let processed = process_ansi_c_content(&chars, &mut pos);
write_escaped_word(f, "'")?;
write_escaped_word(f, &processed)?;
write_escaped_word(f, "'")
}
WordSegment::LocaleString(content) => {
for ch in content.chars() {
write_escaped_char(f, ch)?;
}
Ok(())
}
WordSegment::ArithmeticSub(inner) => {
write!(f, "$((")?;
for ch in inner.chars() {
write_escaped_char(f, ch)?;
}
write!(f, "))")
}
WordSegment::CommandSubstitution(content) => write_cmdsub_segment(f, content),
WordSegment::ProcessSubstitution(direction, content) => {
write_procsub_segment(f, *direction, content)
}
}
}
fn write_cmdsub_segment(f: &mut fmt::Formatter<'_>, content: &str) -> fmt::Result {
write!(f, "$(")?;
if let Some(reformatted) = format::reformat_bash(content) {
if reformatted.starts_with('(') {
write!(f, " ")?;
}
write_escaped_word(f, &reformatted)?;
} else {
let normalized = normalize_cmdsub_content(content);
if normalized.starts_with('(') {
write!(f, " ")?;
}
write_escaped_word(f, &normalized)?;
}
write!(f, ")")
}
fn write_procsub_segment(
f: &mut fmt::Formatter<'_>,
direction: char,
content: &str,
) -> fmt::Result {
write!(f, "{direction}(")?;
let trimmed = content.trim();
if trimmed.starts_with('(') {
write_escaped_word(f, trimmed)?;
} else if let Some(reformatted) = format::reformat_bash(content) {
write_escaped_word(f, &reformatted)?;
} else {
let normalized = normalize_cmdsub_content(content);
write_escaped_word(f, &normalized)?;
}
write!(f, ")")
}
pub fn segments_from_spans(
value: &str,
spans: &[crate::lexer::word_builder::WordSpan],
) -> Vec<WordSegment> {
build_segments(value, spans, is_sexp_relevant)
}
pub fn segments_with_params(
value: &str,
spans: &[crate::lexer::word_builder::WordSpan],
) -> Vec<WordSegment> {
build_segments(value, spans, is_decomposable)
}
fn build_segments(
value: &str,
spans: &[crate::lexer::word_builder::WordSpan],
filter: fn(&crate::lexer::word_builder::WordSpanKind) -> bool,
) -> Vec<WordSegment> {
let top_level = collect_filtered_spans(spans, filter);
let mut segments = Vec::new();
let mut pos = 0;
for span in &top_level {
if span.start > pos
&& let Some(text) = value.get(pos..span.start)
{
segments.push(WordSegment::Literal(text.to_string()));
}
span_to_segment(&mut segments, value, span);
pos = span.end;
}
if pos < value.len()
&& let Some(text) = value.get(pos..)
{
segments.push(WordSegment::Literal(text.to_string()));
}
segments
}
fn span_to_segment(
segments: &mut Vec<WordSegment>,
value: &str,
span: &crate::lexer::word_builder::WordSpan,
) {
use crate::lexer::word_builder::{QuotingContext, WordSpanKind};
match &span.kind {
WordSpanKind::CommandSub => {
if let Some(c) = value.get(span.start + 2..span.end - 1) {
segments.push(WordSegment::CommandSubstitution(c.to_string()));
}
}
WordSpanKind::ProcessSub(dir) => {
if let Some(c) = value.get(span.start + 2..span.end - 1) {
segments.push(WordSegment::ProcessSubstitution(*dir, c.to_string()));
}
}
WordSpanKind::AnsiCQuote => {
push_ansi_c_span(segments, value, span);
}
WordSpanKind::ArithmeticSub => {
if span.end >= span.start + 5
&& let Some(inner) = value.get(span.start + 3..span.end - 2)
{
segments.push(WordSegment::ArithmeticSub(inner.to_string()));
}
}
WordSpanKind::LocaleString => {
match span.context {
QuotingContext::DoubleQuote => {
if let Some(text) = value.get(span.start..span.end) {
push_literal(segments, text);
}
}
_ => {
if let Some(c) = value.get(span.start + 1..span.end) {
segments.push(WordSegment::LocaleString(c.to_string()));
}
}
}
}
WordSpanKind::ParamExpansion | WordSpanKind::SimpleVar | WordSpanKind::BraceExpansion => {
if let Some(text) = value.get(span.start..span.end) {
let seg = match &span.kind {
WordSpanKind::ParamExpansion => WordSegment::ParamExpansion,
WordSpanKind::SimpleVar => WordSegment::SimpleVar,
WordSpanKind::BraceExpansion => WordSegment::BraceExpansion,
_ => unreachable!(),
};
segments.push(seg(text.to_string()));
}
}
WordSpanKind::Backtick => {
if span.end > span.start + 1
&& let Some(c) = value.get(span.start + 1..span.end - 1)
{
segments.push(WordSegment::CommandSubstitution(c.to_string()));
}
}
_ => {} }
}
fn push_ansi_c_span(
segments: &mut Vec<WordSegment>,
value: &str,
span: &crate::lexer::word_builder::WordSpan,
) {
use crate::lexer::word_builder::QuotingContext;
match span.context {
QuotingContext::DoubleQuote => {
if let Some(text) = value.get(span.start..span.end) {
push_literal(segments, text);
}
}
QuotingContext::ParamExpansion => {
if let Some(raw) = value.get(span.start + 2..span.end - 1) {
let chars: Vec<char> = raw.chars().collect();
let mut pos = 0;
let processed = super::process_ansi_c_content(&chars, &mut pos);
push_literal(segments, &processed);
}
}
_ => {
if let Some(c) = value.get(span.start + 2..span.end - 1) {
segments.push(WordSegment::AnsiCQuote(c.to_string()));
}
}
}
}
fn push_literal(segments: &mut Vec<WordSegment>, text: &str) {
if let Some(WordSegment::Literal(last)) = segments.last_mut() {
last.push_str(text);
} else {
segments.push(WordSegment::Literal(text.to_string()));
}
}
const fn is_sexp_relevant(kind: &crate::lexer::word_builder::WordSpanKind) -> bool {
use crate::lexer::word_builder::WordSpanKind;
matches!(
kind,
WordSpanKind::CommandSub
| WordSpanKind::AnsiCQuote
| WordSpanKind::LocaleString
| WordSpanKind::ProcessSub(_)
)
}
const fn is_decomposable(kind: &crate::lexer::word_builder::WordSpanKind) -> bool {
use crate::lexer::word_builder::WordSpanKind;
if is_sexp_relevant(kind) {
return true;
}
matches!(
kind,
WordSpanKind::ParamExpansion
| WordSpanKind::SimpleVar
| WordSpanKind::BraceExpansion
| WordSpanKind::ArithmeticSub
| WordSpanKind::Backtick
)
}
fn collect_filtered_spans(
spans: &[crate::lexer::word_builder::WordSpan],
filter: fn(&crate::lexer::word_builder::WordSpanKind) -> bool,
) -> Vec<&crate::lexer::word_builder::WordSpan> {
let mut relevant: Vec<_> = spans.iter().filter(|s| filter(&s.kind)).collect();
relevant.sort_by_key(|s| s.start);
let mut result = Vec::new();
let mut covered_until: usize = 0;
for span in relevant {
if span.start >= covered_until {
covered_until = span.end;
result.push(span);
}
}
result
}