mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-08-14 03:35:47 -07:00
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:
committed by
Andrew Gallant
parent
86ef683308
commit
1a50324013
@@ -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;
|
||||
|
Reference in New Issue
Block a user