printer: add hyperlinks

This commit represents the initial work to get hyperlinks working and
was submitted as part of PR #2483. Subsequent commits largely retain the
functionality and structure of the hyperlink support added here, but
rejigger some things around.
This commit is contained in:
Lucas Trzesniewski
2023-07-08 00:56:50 +02:00
committed by Andrew Gallant
parent 86ef683308
commit 1a50324013
16 changed files with 1178 additions and 83 deletions

View File

@@ -15,6 +15,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor};
use crate::color::ColorSpecs;
use crate::counter::CounterWriter;
use crate::hyperlink::{HyperlinkPattern, HyperlinkSpan};
use crate::stats::Stats;
use crate::util::{
find_iter_at_in_context, trim_ascii_prefix, trim_line_terminator,
@@ -29,6 +30,7 @@ use crate::util::{
#[derive(Debug, Clone)]
struct Config {
colors: ColorSpecs,
hyperlink_pattern: HyperlinkPattern,
stats: bool,
heading: bool,
path: bool,
@@ -54,6 +56,7 @@ impl Default for Config {
fn default() -> Config {
Config {
colors: ColorSpecs::default(),
hyperlink_pattern: HyperlinkPattern::default(),
stats: false,
heading: false,
path: true,
@@ -122,6 +125,7 @@ impl StandardBuilder {
Standard {
config: self.config.clone(),
wtr: RefCell::new(CounterWriter::new(wtr)),
buf: RefCell::new(vec![]),
matches: vec![],
}
}
@@ -160,6 +164,17 @@ impl StandardBuilder {
self
}
/// Set the hyperlink pattern to use for hyperlinks output by this printer.
///
/// Colors need to be enabled for hyperlinks to be output.
pub fn hyperlink_pattern(
&mut self,
pattern: HyperlinkPattern,
) -> &mut StandardBuilder {
self.config.hyperlink_pattern = pattern;
self
}
/// Enable the gathering of various aggregate statistics.
///
/// When this is enabled (it's disabled by default), statistics will be
@@ -467,6 +482,7 @@ impl StandardBuilder {
pub struct Standard<W> {
config: Config,
wtr: RefCell<CounterWriter<W>>,
buf: RefCell<Vec<u8>>,
matches: Vec<Match>,
}
@@ -1209,23 +1225,25 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
line_number: Option<u64>,
column: Option<u64>,
) -> io::Result<()> {
let sep = self.separator_field();
let mut prelude = PreludeWriter::new(self);
prelude.start(line_number, column)?;
if !self.config().heading {
self.write_path_field(sep)?;
prelude.write_path()?;
}
if let Some(n) = line_number {
self.write_line_number(n, sep)?;
prelude.write_line_number(n)?;
}
if let Some(n) = column {
if self.config().column {
self.write_column_number(n, sep)?;
prelude.write_column_number(n)?;
}
}
if self.config().byte_offset {
self.write_byte_offset(absolute_byte_offset, sep)?;
prelude.write_byte_offset(absolute_byte_offset)?;
}
Ok(())
prelude.end()
}
#[inline(always)]
@@ -1386,7 +1404,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
/// terminator.)
fn write_path_line(&self) -> io::Result<()> {
if let Some(path) = self.path() {
self.write_spec(self.config().colors.path(), path.as_bytes())?;
self.write_path_hyperlink(path)?;
if let Some(term) = self.config().path_terminator {
self.write(&[term])?;
} else {
@@ -1396,22 +1414,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
Ok(())
}
/// If this printer has a file path associated with it, then this will
/// write that path to the underlying writer followed by the given field
/// separator. (If a path terminator is set, then that is used instead of
/// the field separator.)
fn write_path_field(&self, field_separator: &[u8]) -> io::Result<()> {
if let Some(path) = self.path() {
self.write_spec(self.config().colors.path(), path.as_bytes())?;
if let Some(term) = self.config().path_terminator {
self.write(&[term])?;
} else {
self.write(field_separator)?;
}
}
Ok(())
}
fn write_search_prelude(&self) -> io::Result<()> {
let this_search_written = self.wtr().borrow().count() > 0;
if this_search_written {
@@ -1438,7 +1440,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
let bin = self.searcher.binary_detection();
if let Some(byte) = bin.quit_byte() {
if let Some(path) = self.path() {
self.write_spec(self.config().colors.path(), path.as_bytes())?;
self.write_path_hyperlink(path)?;
self.write(b": ")?;
}
let remainder = format!(
@@ -1450,7 +1452,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
self.write(remainder.as_bytes())?;
} else if let Some(byte) = bin.convert_byte() {
if let Some(path) = self.path() {
self.write_spec(self.config().colors.path(), path.as_bytes())?;
self.write_path_hyperlink(path)?;
self.write(b": ")?;
}
let remainder = format!(
@@ -1471,39 +1473,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
Ok(())
}
fn write_line_number(
&self,
line_number: u64,
field_separator: &[u8],
) -> io::Result<()> {
let n = line_number.to_string();
self.write_spec(self.config().colors.line(), n.as_bytes())?;
self.write(field_separator)?;
Ok(())
}
fn write_column_number(
&self,
column_number: u64,
field_separator: &[u8],
) -> io::Result<()> {
let n = column_number.to_string();
self.write_spec(self.config().colors.column(), n.as_bytes())?;
self.write(field_separator)?;
Ok(())
}
fn write_byte_offset(
&self,
offset: u64,
field_separator: &[u8],
) -> io::Result<()> {
let n = offset.to_string();
self.write_spec(self.config().colors.column(), n.as_bytes())?;
self.write(field_separator)?;
Ok(())
}
fn write_line_term(&self) -> io::Result<()> {
self.write(self.searcher.line_terminator().as_bytes())
}
@@ -1516,6 +1485,40 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
Ok(())
}
fn write_path(&self, path: &PrinterPath) -> io::Result<()> {
let mut wtr = self.wtr().borrow_mut();
wtr.set_color(self.config().colors.path())?;
wtr.write_all(path.as_bytes())?;
wtr.reset()
}
fn write_path_hyperlink(&self, path: &PrinterPath) -> io::Result<()> {
let mut hyperlink = self.start_hyperlink_span(path, None, None)?;
self.write_path(path)?;
hyperlink.end(&mut *self.wtr().borrow_mut())
}
fn start_hyperlink_span(
&self,
path: &PrinterPath,
line_number: Option<u64>,
column: Option<u64>,
) -> io::Result<HyperlinkSpan> {
let mut wtr = self.wtr().borrow_mut();
if wtr.supports_hyperlinks() {
let mut buf = self.buf().borrow_mut();
if let Some(spec) = path.create_hyperlink_spec(
&self.config().hyperlink_pattern,
line_number,
column,
&mut buf,
) {
return HyperlinkSpan::start(&mut *wtr, &spec);
}
}
Ok(HyperlinkSpan::default())
}
fn start_color_match(&self) -> io::Result<()> {
if self.in_color_match.get() {
return Ok(());
@@ -1569,6 +1572,12 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
&self.sink.standard.wtr
}
/// Return a temporary buffer, which may be used for anything.
/// It is not necessarily empty when returned.
fn buf(&self) -> &'a RefCell<Vec<u8>> {
&self.sink.standard.buf
}
/// Return the path associated with this printer, if one exists.
fn path(&self) -> Option<&'a PrinterPath<'a>> {
self.sink.path.as_ref()
@@ -1615,6 +1624,139 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
}
}
/// A writer for the prelude (the beginning part of a matching line).
///
/// This encapsulates the state needed to print the prelude.
struct PreludeWriter<'a, M: Matcher, W> {
std: &'a StandardImpl<'a, M, W>,
next_separator: PreludeSeparator,
field_separator: &'a [u8],
hyperlink: HyperlinkSpan,
}
/// A type of separator used in the prelude
enum PreludeSeparator {
/// No separator.
None,
/// The field separator, either for a matching or contextual line.
FieldSeparator,
/// The path terminator.
PathTerminator,
}
impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
/// Creates a new prelude printer.
fn new(std: &'a StandardImpl<'a, M, W>) -> PreludeWriter<'a, M, W> {
Self {
std,
next_separator: PreludeSeparator::None,
field_separator: std.separator_field(),
hyperlink: HyperlinkSpan::default(),
}
}
/// Starts the prelude with a hyperlink when applicable.
///
/// If a heading was written, and the hyperlink pattern is invariant on the line number,
/// then this doesn't hyperlink each line prelude, as it wouldn't point to the line anyway.
/// The hyperlink on the heading should be sufficient and less confusing.
fn start(
&mut self,
line_number: Option<u64>,
column: Option<u64>,
) -> io::Result<()> {
if let Some(path) = self.std.path() {
if self.config().hyperlink_pattern.is_line_dependent()
|| !self.config().heading
{
self.hyperlink = self.std.start_hyperlink_span(
path,
line_number,
column,
)?;
}
}
Ok(())
}
/// Ends the prelude and writes the remaining output.
fn end(&mut self) -> io::Result<()> {
if self.hyperlink.is_active() {
self.hyperlink.end(&mut *self.std.wtr().borrow_mut())?;
}
self.write_separator()
}
/// If this printer has a file path associated with it, then this will
/// write that path to the underlying writer followed by the given field
/// separator. (If a path terminator is set, then that is used instead of
/// the field separator.)
fn write_path(&mut self) -> io::Result<()> {
if let Some(path) = self.std.path() {
self.write_separator()?;
self.std.write_path(path)?;
self.next_separator = if self.config().path_terminator.is_some() {
PreludeSeparator::PathTerminator
} else {
PreludeSeparator::FieldSeparator
};
}
Ok(())
}
/// Writes the line number field.
fn write_line_number(&mut self, line_number: u64) -> io::Result<()> {
self.write_separator()?;
let n = line_number.to_string();
self.std.write_spec(self.config().colors.line(), n.as_bytes())?;
self.next_separator = PreludeSeparator::FieldSeparator;
Ok(())
}
/// Writes the column number field.
fn write_column_number(&mut self, column_number: u64) -> io::Result<()> {
self.write_separator()?;
let n = column_number.to_string();
self.std.write_spec(self.config().colors.column(), n.as_bytes())?;
self.next_separator = PreludeSeparator::FieldSeparator;
Ok(())
}
/// Writes the byte offset field.
fn write_byte_offset(&mut self, offset: u64) -> io::Result<()> {
self.write_separator()?;
let n = offset.to_string();
self.std.write_spec(self.config().colors.column(), n.as_bytes())?;
self.next_separator = PreludeSeparator::FieldSeparator;
Ok(())
}
/// Writes the separator defined by the preceding field.
///
/// This is called before writing the contents of a field, and at
/// the end of the prelude.
fn write_separator(&mut self) -> io::Result<()> {
match self.next_separator {
PreludeSeparator::None => {}
PreludeSeparator::FieldSeparator => {
self.std.write(self.field_separator)?;
}
PreludeSeparator::PathTerminator => {
if let Some(term) = self.config().path_terminator {
self.std.write(&[term])?;
}
}
}
self.next_separator = PreludeSeparator::None;
Ok(())
}
fn config(&self) -> &Config {
self.std.config()
}
}
#[cfg(test)]
mod tests {
use grep_matcher::LineTerminator;