Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/format/fragment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Used when `-n` is set (positional selectors or `-e` + `-n`).

use super::{FormatOpts, Formatter, ansi};
use super::{FormatOpts, Formatter, ansi, push_lossy};
use crate::Emit;
use std::io::{self, Write};

Expand Down Expand Up @@ -34,26 +34,30 @@ impl Formatter for FragmentFormatter {
let start = col_idx.saturating_sub(self.char_context);
let end = bytes.len().min(col_idx + self.char_context + 1);

let frag = String::from_utf8_lossy(&bytes[start..end]).to_string();
let frag_bytes = &bytes[start..end];
let prefix = self.opts.prefix(emit.line.no);

// Highlight the target span within the fragment if regex spans exist.
let rendered = if let Some(span) = emit.match_info.spans.first() {
if self.opts.color {
let hs = span.start.saturating_sub(start).min(frag.len());
let he = (span.end.saturating_sub(start)).min(frag.len());
let hs = span.start.max(start).min(end) - start;
let he = span.end.max(start).min(end) - start;
if hs < he {
let (a, rest) = frag.split_at(hs);
let (b, c) = rest.split_at(he - hs);
format!("{a}{}{b}{}{c}", ansi::INVERSE, ansi::RESET)
let mut out = String::new();
push_lossy(&mut out, &frag_bytes[..hs]);
out.push_str(ansi::INVERSE);
push_lossy(&mut out, &frag_bytes[hs..he]);
out.push_str(ansi::RESET);
push_lossy(&mut out, &frag_bytes[he..]);
out
} else {
frag.clone()
String::from_utf8_lossy(frag_bytes).to_string()
}
} else {
frag.clone()
String::from_utf8_lossy(frag_bytes).to_string()
}
} else {
frag.clone()
String::from_utf8_lossy(frag_bytes).to_string()
};

writeln!(sink, "{prefix}{rendered}")?;
Expand Down
4 changes: 4 additions & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ impl FormatOpts {
pub fn digits(n: u64) -> usize {
n.to_string().len()
}

fn push_lossy(out: &mut String, bytes: &[u8]) {
out.push_str(&String::from_utf8_lossy(bytes));
}
16 changes: 7 additions & 9 deletions src/format/plain.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Plain-line formatter with optional line number, filename, and highlight.

use super::{FormatOpts, Formatter, ansi};
use super::{FormatOpts, Formatter, ansi, push_lossy};
use crate::{Emit, Role};
use std::io::{self, Write};
use std::ops::Range;
Expand All @@ -15,28 +15,26 @@ impl PlainFormatter {
}

fn render_content(&self, bytes: &[u8], spans: &[Range<usize>]) -> String {
let text = String::from_utf8_lossy(bytes);
if !self.opts.color || spans.is_empty() {
return text.to_string();
return String::from_utf8_lossy(bytes).to_string();
}
let mut sorted = spans.to_vec();
sorted.sort_by_key(|r| r.start);
let mut out = String::new();
let mut cursor = 0usize;
let t: &str = text.as_ref();
for r in sorted {
let s = r.start.min(t.len());
let e = r.end.min(t.len());
let s = r.start.min(bytes.len());
let e = r.end.min(bytes.len());
if s < cursor {
continue;
}
out.push_str(&t[cursor..s]);
push_lossy(&mut out, &bytes[cursor..s]);
out.push_str(ansi::INVERSE);
out.push_str(&t[s..e]);
push_lossy(&mut out, &bytes[s..e]);
out.push_str(ansi::RESET);
cursor = e;
}
out.push_str(&t[cursor..]);
push_lossy(&mut out, &bytes[cursor..]);
out
}
}
Expand Down
46 changes: 46 additions & 0 deletions tests/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use std::io::Write;
use std::process::Command;
use tempfile::NamedTempFile;

fn sel_bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_sel"))
}

/// Helper to run sel with arguments and return stdout.
fn run_sel(args: &[&str]) -> String {
let output = Command::new("cargo")
Expand Down Expand Up @@ -348,6 +352,48 @@ fn test_regex_with_filename_flag() {
assert!(output.contains(filename.as_ref()));
}

#[test]
fn test_byte_regex_highlight_invalid_utf8_does_not_panic() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(&[0xff, b'\n']).unwrap();

let output = sel_bin()
.args([
"--color=always",
"-e",
"(?-u:.)",
file.path().to_str().unwrap(),
])
.output()
.unwrap();

assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("\u{fffd}"));
}

#[test]
fn test_byte_regex_fragment_highlight_invalid_utf8_does_not_panic() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(&[0xff, b'\n']).unwrap();

let output = sel_bin()
.args([
"--color=always",
"-n",
"1",
"-e",
"(?-u:.)",
file.path().to_str().unwrap(),
])
.output()
.unwrap();

assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("\u{fffd}"));
assert!(stdout.contains('^'));
}

#[test]
fn test_regex_long_line_match() {
let long_line = "a".repeat(1000) + "TARGET" + &"b".repeat(1000);
Expand Down
Loading