fn expand_to_full_line(content: &str, start: usize, end: usize) -> (usize, usize) {
let bytes = content.as_bytes();
let line_start = bytes[..start]
.iter()
.rposition(|&b| b == b'\n')
.map_or(0, |pos| pos + 1);
let line_end = bytes[end..]
.iter()
.position(|&b| b == b'\n')
.map_or(content.len(), |pos| end + pos + 1);
let before_match = &content[line_start..start];
if before_match.trim().is_empty() {
(line_start, line_end)
} else {
let content_end = if line_end > 0 && bytes[line_end - 1] == b'\n' {
line_end - 1
} else {
line_end
};
let trim_start = content[line_start..start]
.rfind(|c: char| !c.is_whitespace())
.map_or(start, |pos| line_start + pos + 1);
(trim_start, content_end)
}
}
fn apply_scope_action<'a>(
text: &'a str,
delete: bool,
action: Option<ScopeAction>,
replace_with: Option<&str>,
) -> std::borrow::Cow<'a, str> {
if delete {
return std::borrow::Cow::Owned(String::new());
}
if let Some(replacement) = replace_with {
return std::borrow::Cow::Owned(replacement.to_owned());
}
match action {
Some(ScopeAction::Upper) => std::borrow::Cow::Owned(text.to_uppercase()),
Some(ScopeAction::Lower) => std::borrow::Cow::Owned(text.to_lowercase()),
Some(ScopeAction::Titlecase) => std::borrow::Cow::Owned(titlecase(text)),
Some(ScopeAction::Squeeze) => std::borrow::Cow::Owned(squeeze(text)),
Some(ScopeAction::Symbols) => std::borrow::Cow::Owned(symbolize(text)),
Some(ScopeAction::Normalize) => std::borrow::Cow::Owned(text.nfc().collect::<String>()),
None => std::borrow::Cow::Borrowed(text),
}
}
fn titlecase(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = true;
for c in s.chars() {
if capitalize_next && c.is_alphabetic() {
result.extend(c.to_uppercase());
capitalize_next = false;
} else {
result.push(c);
if c.is_whitespace() || c == '_' || c == '-' {
capitalize_next = true;
}
}
}
result
}
fn squeeze(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut prev: Option<char> = None;
for c in s.chars() {
if Some(c) != prev || !c.is_whitespace() {
result.push(c);
}
prev = Some(c);
}
result
}
fn symbolize(s: &str) -> String {
s.replace("=>", "⇒")
.replace("->", "→")
.replace("<-", "←")
.replace("!=", "≠")
.replace(">=", "≥")
.replace("<=", "≤")
.replace("...", "…")
.replace("--", "—")
}
fn resolve_patterns(
query_name: &Option<String>,
custom_pattern: &Option<String>,
lang_str: &str,
) -> Result<Vec<String>> {
if let Some(p) = custom_pattern {
return Ok(vec![p.clone()]);
}
let name = query_name
.as_deref()
.ok_or_else(|| AtomwriteError::InvalidInput {
reason: "either --query or --pattern is required".into(),
})?;
lookup_prepared_queries(name, lang_str)
}
fn parse_language(lang_str: &str) -> Result<SupportLang> {
lang_str.parse().map_err(|_| {
AtomwriteError::InvalidInput {
reason: format!("unsupported language: {lang_str}"),
}
.into()
})
}
enum ScopeEvent {
Result(Box<ScopeResult>),
Failed(std::path::PathBuf, anyhow::Error),
ReadFailed(std::path::PathBuf, anyhow::Error),
}
pub(crate) type ScopeSummary = crate::ndjson_types::FailureSummary;
#[derive(Default)]
struct ScopeFailures {
items: Vec<crate::ndjson_types::FileFailure>,
first_write: Option<anyhow::Error>,
first_read: Option<anyhow::Error>,
}
impl ScopeFailures {
fn push_write(&mut self, path: &std::path::Path, err: anyhow::Error) {
self.record(path, &err);
self.first_write.get_or_insert(err);
}
fn push_read(&mut self, path: &std::path::Path, err: anyhow::Error) {
self.record(path, &err);
self.first_read.get_or_insert(err);
}
fn record(&mut self, path: &std::path::Path, err: &anyhow::Error) {
if self.items.len() < crate::config::summary_errors_max_entries() {
self.items.push(crate::ndjson_types::FileFailure {
path: path.display().to_string(),
code: crate::output::failure_identity(err).0,
message: crate::output::error_message(err),
});
}
}
}
fn drain_scope_events(
rx: &crossbeam_channel::Receiver<ScopeEvent>,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
) -> Result<(ScopeFailures, u64)> {
let mut failures = ScopeFailures::default();
let mut bytes_after = 0u64;
for event in rx {
if shutdown.is_shutdown() {
break;
}
match event {
ScopeEvent::Result(r) => {
bytes_after = bytes_after.saturating_add(r.bytes_after);
writer.write_event(&r)?;
}
ScopeEvent::Failed(path, err) => failures.push_write(&path, err),
ScopeEvent::ReadFailed(path, err) => failures.push_read(&path, err),
}
}
Ok((failures, bytes_after))
}