1
0
mirror of https://github.com/BurntSushi/ripgrep.git synced 2024-12-02 02:56:32 +02: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:
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

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ target
/termcolor/Cargo.lock
/wincolor/Cargo.lock
/deployment
/.idea
# Snapcraft files
stage

69
Cargo.lock generated
View File

@ -136,6 +136,16 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "gethostname"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
dependencies = [
"libc",
"windows-targets",
]
[[package]]
name = "glob"
version = "0.3.1"
@ -208,9 +218,11 @@ version = "0.1.7"
dependencies = [
"base64",
"bstr",
"gethostname",
"grep-matcher",
"grep-regex",
"grep-searcher",
"lazy_static",
"serde",
"serde_json",
"termcolor",
@ -612,3 +624,60 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"

View File

@ -305,6 +305,7 @@ _rg() {
'--debug[show debug messages]'
'--field-context-separator[set string to delimit fields in context lines]'
'--field-match-separator[set string to delimit fields in matching lines]'
'--hyperlink-format=[specify pattern for hyperlinks]:pattern'
'--trace[show more verbose debug messages]'
'--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size (bytes)'
"(1 stats)--files[show each file that would be searched (but don't search)]"

View File

@ -1,6 +1,7 @@
use std::io;
use termcolor;
use termcolor::HyperlinkSpec;
use crate::is_tty_stdout;
@ -101,6 +102,16 @@ impl termcolor::WriteColor for StandardStream {
}
}
#[inline]
fn supports_hyperlinks(&self) -> bool {
use self::StandardStreamKind::*;
match self.0 {
LineBuffered(ref w) => w.supports_hyperlinks(),
BlockBuffered(ref w) => w.supports_hyperlinks(),
}
}
#[inline]
fn set_color(&mut self, spec: &termcolor::ColorSpec) -> io::Result<()> {
use self::StandardStreamKind::*;
@ -111,6 +122,16 @@ impl termcolor::WriteColor for StandardStream {
}
}
#[inline]
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
use self::StandardStreamKind::*;
match self.0 {
LineBuffered(ref mut w) => w.set_hyperlink(link),
BlockBuffered(ref mut w) => w.set_hyperlink(link),
}
}
#[inline]
fn reset(&mut self) -> io::Result<()> {
use self::StandardStreamKind::*;

View File

@ -580,6 +580,7 @@ pub fn all_args_and_flags() -> Vec<RGArg> {
flag_glob_case_insensitive(&mut args);
flag_heading(&mut args);
flag_hidden(&mut args);
flag_hyperlink_format(&mut args);
flag_iglob(&mut args);
flag_ignore_case(&mut args);
flag_ignore_file(&mut args);
@ -1494,6 +1495,26 @@ This flag can be disabled with --no-hidden.
args.push(arg);
}
fn flag_hyperlink_format(args: &mut Vec<RGArg>) {
const SHORT: &str = "Set the format of hyperlinks to match results.";
const LONG: &str = long!(
"\
Set the format of hyperlinks to match results. This defines a pattern which
can contain the following placeholders: {file}, {line}, {column}, and {host}.
An empty pattern or 'none' disables hyperlinks.
The {file} placeholder is required, and will be replaced with the absolute
file path with a few adjustments: The leading '/' on Unix is removed,
and '\\' is replaced with '/' on Windows.
As an example, the default pattern on Unix systems is: 'file://{host}/{file}'
"
);
let arg =
RGArg::flag("hyperlink-format", "FORMAT").help(SHORT).long_help(LONG);
args.push(arg);
}
fn flag_iglob(args: &mut Vec<RGArg>) {
const SHORT: &str = "Include or exclude files case insensitively.";
const LONG: &str = long!(

View File

@ -5,6 +5,7 @@ use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
@ -17,8 +18,8 @@ use grep::pcre2::{
RegexMatcherBuilder as PCRE2RegexMatcherBuilder,
};
use grep::printer::{
default_color_specs, ColorSpecs, JSONBuilder, Standard, StandardBuilder,
Stats, Summary, SummaryBuilder, SummaryKind, JSON,
default_color_specs, ColorSpecs, HyperlinkPattern, JSONBuilder, Standard,
StandardBuilder, Stats, Summary, SummaryBuilder, SummaryKind, JSON,
};
use grep::regex::{
RegexMatcher as RustRegexMatcher,
@ -235,6 +236,7 @@ impl Args {
let mut builder = PathPrinterBuilder::new();
builder
.color_specs(self.matches().color_specs()?)
.hyperlink_pattern(self.matches().hyperlink_pattern()?)
.separator(self.matches().path_separator()?)
.terminator(self.matches().path_terminator().unwrap_or(b'\n'));
Ok(builder.build(wtr))
@ -772,6 +774,7 @@ impl ArgMatches {
let mut builder = StandardBuilder::new();
builder
.color_specs(self.color_specs()?)
.hyperlink_pattern(self.hyperlink_pattern()?)
.stats(self.stats())
.heading(self.heading())
.path(self.with_filename(paths))
@ -811,6 +814,7 @@ impl ArgMatches {
builder
.kind(self.summary_kind().expect("summary format"))
.color_specs(self.color_specs()?)
.hyperlink_pattern(self.hyperlink_pattern()?)
.stats(self.stats())
.path(self.with_filename(paths))
.max_matches(self.max_count()?)
@ -1118,6 +1122,17 @@ impl ArgMatches {
self.is_present("hidden") || self.unrestricted_count() >= 2
}
/// Returns the hyperlink pattern to use. A default pattern suitable
/// for the current system is used if the value is not set.
///
/// If an invalid pattern is provided, then an error is returned.
fn hyperlink_pattern(&self) -> Result<HyperlinkPattern> {
Ok(match self.value_of_lossy("hyperlink-format") {
Some(pattern) => HyperlinkPattern::from_str(&pattern)?,
None => HyperlinkPattern::default_file_scheme(),
})
}
/// Returns true if ignore files should be processed case insensitively.
fn ignore_file_case_insensitive(&self) -> bool {
self.is_present("ignore-file-case-insensitive")

View File

@ -1,13 +1,16 @@
use std::io;
use std::path::Path;
use grep::printer::{ColorSpecs, PrinterPath};
use grep::printer::{
ColorSpecs, HyperlinkPattern, HyperlinkSpan, PrinterPath,
};
use termcolor::WriteColor;
/// A configuration for describing how paths should be written.
#[derive(Clone, Debug)]
struct Config {
colors: ColorSpecs,
hyperlink_pattern: HyperlinkPattern,
separator: Option<u8>,
terminator: u8,
}
@ -16,6 +19,7 @@ impl Default for Config {
fn default() -> Config {
Config {
colors: ColorSpecs::default(),
hyperlink_pattern: HyperlinkPattern::default(),
separator: None,
terminator: b'\n',
}
@ -37,7 +41,7 @@ impl PathPrinterBuilder {
/// Create a new path printer with the current configuration that writes
/// paths to the given writer.
pub fn build<W: WriteColor>(&self, wtr: W) -> PathPrinter<W> {
PathPrinter { config: self.config.clone(), wtr }
PathPrinter { config: self.config.clone(), wtr, buf: vec![] }
}
/// Set the color specification for this printer.
@ -52,6 +56,17 @@ impl PathPrinterBuilder {
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 PathPrinterBuilder {
self.config.hyperlink_pattern = pattern;
self
}
/// A path separator.
///
/// When provided, the path's default separator will be replaced with
@ -80,6 +95,7 @@ impl PathPrinterBuilder {
pub struct PathPrinter<W> {
config: Config,
wtr: W,
buf: Vec<u8>,
}
impl<W: WriteColor> PathPrinter<W> {
@ -89,10 +105,30 @@ impl<W: WriteColor> PathPrinter<W> {
if !self.wtr.supports_color() {
self.wtr.write_all(ppath.as_bytes())?;
} else {
let mut hyperlink = self.start_hyperlink_span(&ppath)?;
self.wtr.set_color(self.config.colors.path())?;
self.wtr.write_all(ppath.as_bytes())?;
self.wtr.reset()?;
hyperlink.end(&mut self.wtr)?;
}
self.wtr.write_all(&[self.config.terminator])
}
/// Starts a hyperlink span when applicable.
fn start_hyperlink_span(
&mut self,
path: &PrinterPath,
) -> io::Result<HyperlinkSpan> {
if self.wtr.supports_hyperlinks() {
if let Some(spec) = path.create_hyperlink_spec(
&self.config.hyperlink_pattern,
None,
None,
&mut self.buf,
) {
return Ok(HyperlinkSpan::start(&mut self.wtr, &spec)?);
}
}
Ok(HyperlinkSpan::default())
}
}

View File

@ -21,8 +21,10 @@ serde1 = ["base64", "serde", "serde_json"]
[dependencies]
base64 = { version = "0.20.0", optional = true }
bstr = "1.6.0"
gethostname = "0.4.3"
grep-matcher = { version = "0.1.6", path = "../matcher" }
grep-searcher = { version = "0.1.11", path = "../searcher" }
lazy_static = "1.1.0"
termcolor = "1.0.4"
serde = { version = "1.0.77", optional = true, features = ["derive"] }
serde_json = { version = "1.0.27", optional = true }

View File

@ -1,6 +1,6 @@
use std::io::{self, Write};
use termcolor::{ColorSpec, WriteColor};
use termcolor::{ColorSpec, HyperlinkSpec, WriteColor};
/// A writer that counts the number of bytes that have been successfully
/// written.
@ -76,10 +76,18 @@ impl<W: WriteColor> WriteColor for CounterWriter<W> {
self.wtr.supports_color()
}
fn supports_hyperlinks(&self) -> bool {
self.wtr.supports_hyperlinks()
}
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
self.wtr.set_color(spec)
}
fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> {
self.wtr.set_hyperlink(link)
}
fn reset(&mut self) -> io::Result<()> {
self.wtr.reset()
}

View File

@ -0,0 +1,664 @@
use crate::hyperlink_aliases::HYPERLINK_PATTERN_ALIASES;
use bstr::ByteSlice;
use std::error::Error;
use std::fmt::Display;
use std::io;
use std::io::Write;
use std::path::Path;
use std::str::FromStr;
use termcolor::{HyperlinkSpec, WriteColor};
/// A builder for `HyperlinkPattern`.
///
/// Once a `HyperlinkPattern` is built, it is immutable.
#[derive(Debug)]
pub struct HyperlinkPatternBuilder {
parts: Vec<Part>,
}
/// A hyperlink pattern with placeholders.
///
/// This can be created with `HyperlinkPatternBuilder` or from a string
/// using `HyperlinkPattern::from_str`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HyperlinkPattern {
parts: Vec<Part>,
is_line_dependent: bool,
}
/// A hyperlink pattern part.
#[derive(Clone, Debug, Eq, PartialEq)]
enum Part {
/// Static text. Can include invariant values such as the hostname.
Text(Vec<u8>),
/// Placeholder for the file path.
File,
/// Placeholder for the line number.
Line,
/// Placeholder for the column number.
Column,
}
/// An error that can occur when parsing a hyperlink pattern.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HyperlinkPatternError {
/// This occurs when the pattern syntax is not valid.
InvalidSyntax,
/// This occurs when the {file} placeholder is missing.
NoFilePlaceholder,
/// This occurs when the {line} placeholder is missing,
/// while the {column} placeholder is present.
NoLinePlaceholder,
/// This occurs when an unknown placeholder is used.
InvalidPlaceholder(String),
/// The pattern doesn't start with a valid scheme.
InvalidScheme,
}
/// The values to replace the pattern placeholders with.
#[derive(Clone, Debug)]
pub struct HyperlinkValues<'a> {
file: &'a HyperlinkPath,
line: u64,
column: u64,
}
/// Represents the {file} part of a hyperlink.
///
/// This is the value to use as-is in the hyperlink, converted from an OS file path.
#[derive(Clone, Debug)]
pub struct HyperlinkPath(Vec<u8>);
impl HyperlinkPatternBuilder {
/// Creates a new hyperlink pattern builder.
pub fn new() -> Self {
Self { parts: vec![] }
}
/// Appends static text.
pub fn append_text(&mut self, text: &[u8]) -> &mut Self {
if let Some(Part::Text(contents)) = self.parts.last_mut() {
contents.extend_from_slice(text);
} else if !text.is_empty() {
self.parts.push(Part::Text(text.to_vec()));
}
self
}
/// Appends the hostname.
///
/// On WSL, appends `wsl$/{distro}` instead.
pub fn append_hostname(&mut self) -> &mut Self {
self.append_text(Self::get_hostname().as_bytes())
}
/// Returns the hostname to use in the pattern.
///
/// On WSL, returns `wsl$/{distro}`.
fn get_hostname() -> String {
if cfg!(unix) {
if let Ok(mut wsl_distro) = std::env::var("WSL_DISTRO_NAME") {
wsl_distro.insert_str(0, "wsl$/");
return wsl_distro;
}
}
gethostname::gethostname().to_string_lossy().to_string()
}
/// Appends a placeholder for the file path.
pub fn append_file(&mut self) -> &mut Self {
self.parts.push(Part::File);
self
}
/// Appends a placeholder for the line number.
pub fn append_line(&mut self) -> &mut Self {
self.parts.push(Part::Line);
self
}
/// Appends a placeholder for the column number.
pub fn append_column(&mut self) -> &mut Self {
self.parts.push(Part::Column);
self
}
/// Builds the pattern.
pub fn build(&self) -> Result<HyperlinkPattern, HyperlinkPatternError> {
self.validate()?;
Ok(HyperlinkPattern {
parts: self.parts.clone(),
is_line_dependent: self.parts.contains(&Part::Line),
})
}
/// Validate that the pattern is well-formed.
fn validate(&self) -> Result<(), HyperlinkPatternError> {
if self.parts.is_empty() {
return Ok(());
}
if !self.parts.contains(&Part::File) {
return Err(HyperlinkPatternError::NoFilePlaceholder);
}
if self.parts.contains(&Part::Column)
&& !self.parts.contains(&Part::Line)
{
return Err(HyperlinkPatternError::NoLinePlaceholder);
}
self.validate_scheme()
}
/// Validate that the pattern starts with a valid scheme.
///
/// A valid scheme starts with an alphabetic character, continues with
/// a sequence of alphanumeric characters, periods, hyphens or plus signs,
/// and ends with a colon.
fn validate_scheme(&self) -> Result<(), HyperlinkPatternError> {
if let Some(Part::Text(value)) = self.parts.first() {
if let Some(colon_index) = value.find_byte(b':') {
if value[0].is_ascii_alphabetic()
&& value.iter().take(colon_index).all(|c| {
c.is_ascii_alphanumeric()
|| matches!(c, b'.' | b'-' | b'+')
})
{
return Ok(());
}
}
}
Err(HyperlinkPatternError::InvalidScheme)
}
}
impl HyperlinkPattern {
/// Creates an empty hyperlink pattern.
pub fn empty() -> Self {
HyperlinkPattern::default()
}
/// Creates a default pattern suitable for Unix.
///
/// The returned pattern is `file://{host}/{file}`
#[cfg(unix)]
pub fn default_file_scheme() -> Self {
HyperlinkPatternBuilder::new()
.append_text(b"file://")
.append_hostname()
.append_text(b"/")
.append_file()
.build()
.unwrap()
}
/// Creates a default pattern suitable for Windows.
///
/// The returned pattern is `file:///{file}`
#[cfg(windows)]
pub fn default_file_scheme() -> Self {
HyperlinkPatternBuilder::new()
.append_text(b"file:///")
.append_file()
.build()
.unwrap()
}
/// Returns true if this pattern is empty.
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
/// Returns true if the pattern can produce line-dependent hyperlinks.
pub fn is_line_dependent(&self) -> bool {
self.is_line_dependent
}
/// Renders this pattern with the given values to the given output.
pub fn render(
&self,
values: &HyperlinkValues,
output: &mut impl Write,
) -> io::Result<()> {
for part in &self.parts {
part.render(values, output)?;
}
Ok(())
}
}
impl FromStr for HyperlinkPattern {
type Err = HyperlinkPatternError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut builder = HyperlinkPatternBuilder::new();
let mut input = s.as_bytes();
if let Ok(index) = HYPERLINK_PATTERN_ALIASES
.binary_search_by_key(&input, |&(name, _)| name.as_bytes())
{
input = HYPERLINK_PATTERN_ALIASES[index].1.as_bytes();
}
while !input.is_empty() {
if input[0] == b'{' {
// Placeholder
let end = input
.find_byte(b'}')
.ok_or(HyperlinkPatternError::InvalidSyntax)?;
match &input[1..end] {
b"file" => builder.append_file(),
b"line" => builder.append_line(),
b"column" => builder.append_column(),
b"host" => builder.append_hostname(),
other => {
return Err(HyperlinkPatternError::InvalidPlaceholder(
String::from_utf8_lossy(other).to_string(),
))
}
};
input = &input[(end + 1)..];
} else {
// Static text
let end = input.find_byte(b'{').unwrap_or(input.len());
builder.append_text(&input[..end]);
input = &input[end..];
}
}
builder.build()
}
}
impl ToString for HyperlinkPattern {
fn to_string(&self) -> String {
self.parts.iter().map(|p| p.to_string()).collect()
}
}
impl Part {
fn render(
&self,
values: &HyperlinkValues,
output: &mut impl Write,
) -> io::Result<()> {
match self {
Part::Text(text) => output.write_all(text),
Part::File => output.write_all(&values.file.0),
Part::Line => write!(output, "{}", values.line),
Part::Column => write!(output, "{}", values.column),
}
}
}
impl ToString for Part {
fn to_string(&self) -> String {
match self {
Part::Text(text) => String::from_utf8_lossy(text).to_string(),
Part::File => "{file}".to_string(),
Part::Line => "{line}".to_string(),
Part::Column => "{column}".to_string(),
}
}
}
impl Display for HyperlinkPatternError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HyperlinkPatternError::InvalidSyntax => {
write!(f, "invalid hyperlink pattern syntax")
}
HyperlinkPatternError::NoFilePlaceholder => {
write!(f, "the {{file}} placeholder is required in hyperlink patterns")
}
HyperlinkPatternError::NoLinePlaceholder => {
write!(f, "the hyperlink pattern contains a {{column}} placeholder, \
but no {{line}} placeholder is present")
}
HyperlinkPatternError::InvalidPlaceholder(name) => {
write!(
f,
"invalid hyperlink pattern placeholder: '{}', choose from: \
file, line, column, host",
name
)
}
HyperlinkPatternError::InvalidScheme => {
write!(
f,
"the hyperlink pattern must start with a valid URL scheme"
)
}
}
}
}
impl Error for HyperlinkPatternError {}
impl<'a> HyperlinkValues<'a> {
/// Creates a new set of hyperlink values.
pub fn new(
file: &'a HyperlinkPath,
line: Option<u64>,
column: Option<u64>,
) -> Self {
HyperlinkValues {
file,
line: line.unwrap_or(1),
column: column.unwrap_or(1),
}
}
}
impl HyperlinkPath {
/// Returns a hyperlink path from an OS path.
#[cfg(unix)]
pub fn from_path(path: &Path) -> Option<Self> {
// On Unix, this function returns the absolute file path without the leading slash,
// as it makes for more natural hyperlink patterns, for instance:
// file://{host}/{file} instead of file://{host}{file}
// vscode://file/{file} instead of vscode://file{file}
// It also allows for patterns to be multi-platform.
let path = path.canonicalize().ok()?;
let path = path.to_str()?.as_bytes();
let path = if path.starts_with(b"/") { &path[1..] } else { path };
Some(Self::encode(path))
}
/// Returns a hyperlink path from an OS path.
#[cfg(windows)]
pub fn from_path(path: &Path) -> Option<Self> {
// On Windows, Path::canonicalize returns the result of
// GetFinalPathNameByHandleW with VOLUME_NAME_DOS,
// which produces paths such as the following:
// \\?\C:\dir\file.txt (local path)
// \\?\UNC\server\dir\file.txt (network share)
//
// The \\?\ prefix comes from VOLUME_NAME_DOS and is constant.
// It is followed either by the drive letter, or by UNC\
// (universal naming convention), which denotes a network share.
//
// Given that the default URL pattern on Windows is file:///{file}
// we need to return the following from this function:
// C:/dir/file.txt (local path)
// /server/dir/file.txt (network share)
//
// Which produces the following links:
// file:///C:/dir/file.txt (local path)
// file:////server/dir/file.txt (network share)
//
// This substitutes the {file} placeholder with the expected value
// for the most common DOS paths, but on the other hand,
// network paths start with a single slash, which may be unexpected.
// It produces correct URLs though.
//
// Note that the following URL syntax is also valid for network shares:
// file://server/dir/file.txt
// It is also more consistent with the Unix case, but in order to
// use it, the pattern would have to be file://{file} and
// the {file} placeholder would have to be replaced with
// /C:/dir/file.txt
// for local files, which is not ideal, and it is certainly unexpected.
//
// Also note that the file://C:/dir/file.txt syntax is not correct,
// even though it often works in practice.
//
// In the end, this choice was confirmed by VSCode, whose pattern
// is vscode://file/{file}:{line}:{column} and which correctly understands
// the following URL format for network drives:
// vscode://file//server/dir/file.txt:1:1
// It doesn't parse any other number of slashes in "file//server" as a network path.
const WIN32_NAMESPACE_PREFIX: &[u8] = br"\\?\";
const UNC_PREFIX: &[u8] = br"UNC\";
let path = path.canonicalize().ok()?;
let mut path = path.to_str()?.as_bytes();
if path.starts_with(WIN32_NAMESPACE_PREFIX) {
path = &path[WIN32_NAMESPACE_PREFIX.len()..];
if path.starts_with(UNC_PREFIX) {
path = &path[(UNC_PREFIX.len() - 1)..];
}
} else {
return None;
}
Some(Self::encode(path))
}
/// Percent-encodes a path.
///
/// The alphanumeric ASCII characters and "-", ".", "_", "~" are unreserved
/// as per section 2.3 of RFC 3986 (Uniform Resource Identifier (URI): Generic Syntax),
/// and are not encoded. The other ASCII characters except "/" and ":" are percent-encoded,
/// and "\" is replaced by "/" on Windows.
///
/// Section 4 of RFC 8089 (The "file" URI Scheme) does not mandate precise encoding
/// requirements for non-ASCII characters, and this implementation leaves them unencoded.
/// On Windows, the UrlCreateFromPathW function does not encode non-ASCII characters.
/// Doing so with UTF-8 encoded paths creates invalid file:// URLs on that platform.
fn encode(input: &[u8]) -> HyperlinkPath {
let mut result = Vec::with_capacity(input.len());
for &c in input {
match c {
b'0'..=b'9'
| b'A'..=b'Z'
| b'a'..=b'z'
| b'/'
| b':'
| b'-'
| b'.'
| b'_'
| b'~'
| 128.. => {
result.push(c);
}
#[cfg(windows)]
b'\\' => {
result.push(b'/');
}
_ => {
const HEX: &[u8] = b"0123456789ABCDEF";
result.push(b'%');
result.push(HEX[(c >> 4) as usize]);
result.push(HEX[(c & 0xF) as usize]);
}
}
}
Self(result)
}
}
impl Display for HyperlinkPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
std::str::from_utf8(&self.0).unwrap_or("invalid utf-8")
)
}
}
/// A simple abstraction over a hyperlink span written to the terminal.
/// This helps tracking whether a hyperlink has been started, and should be ended.
#[derive(Debug, Default)]
pub struct HyperlinkSpan {
active: bool,
}
impl HyperlinkSpan {
/// Starts a hyperlink and returns a span which tracks whether it is still in effect.
pub fn start(
wtr: &mut impl WriteColor,
hyperlink: &HyperlinkSpec,
) -> io::Result<Self> {
if wtr.supports_hyperlinks() && hyperlink.uri().is_some() {
wtr.set_hyperlink(hyperlink)?;
Ok(HyperlinkSpan { active: true })
} else {
Ok(HyperlinkSpan { active: false })
}
}
/// Ends the hyperlink span if it is active.
pub fn end(&mut self, wtr: &mut impl WriteColor) -> io::Result<()> {
if self.is_active() {
wtr.set_hyperlink(&HyperlinkSpec::close())?;
self.active = false;
}
Ok(())
}
/// Returns true if there is currently an active hyperlink.
pub fn is_active(&self) -> bool {
self.active
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_pattern() {
let pattern = HyperlinkPatternBuilder::new()
.append_text(b"foo://")
.append_text(b"bar-")
.append_text(b"baz")
.append_file()
.build()
.unwrap();
assert_eq!(pattern.to_string(), "foo://bar-baz{file}");
assert_eq!(pattern.parts[0], Part::Text(b"foo://bar-baz".to_vec()));
assert!(!pattern.is_empty());
}
#[test]
fn build_empty_pattern() {
let pattern = HyperlinkPatternBuilder::new().build().unwrap();
assert!(pattern.is_empty());
assert_eq!(pattern, HyperlinkPattern::empty());
assert_eq!(pattern, HyperlinkPattern::default());
}
#[test]
fn handle_alias() {
assert!(HyperlinkPattern::from_str("file").is_ok());
assert!(HyperlinkPattern::from_str("none").is_ok());
assert!(HyperlinkPattern::from_str("none").unwrap().is_empty());
}
#[test]
fn parse_pattern() {
let pattern = HyperlinkPattern::from_str(
"foo://{host}/bar/{file}:{line}:{column}",
)
.unwrap();
assert_eq!(
pattern.to_string(),
"foo://{host}/bar/{file}:{line}:{column}"
.replace("{host}", &HyperlinkPatternBuilder::get_hostname())
);
assert_eq!(pattern.parts.len(), 6);
assert!(pattern.parts.contains(&Part::File));
assert!(pattern.parts.contains(&Part::Line));
assert!(pattern.parts.contains(&Part::Column));
}
#[test]
fn parse_valid() {
assert!(HyperlinkPattern::from_str("").unwrap().is_empty());
assert_eq!(
HyperlinkPattern::from_str("foo://{file}").unwrap().to_string(),
"foo://{file}"
);
assert_eq!(
HyperlinkPattern::from_str("foo://{file}/bar")
.unwrap()
.to_string(),
"foo://{file}/bar"
);
HyperlinkPattern::from_str("f://{file}").unwrap();
HyperlinkPattern::from_str("f:{file}").unwrap();
HyperlinkPattern::from_str("f-+.:{file}").unwrap();
HyperlinkPattern::from_str("f42:{file}").unwrap();
}
#[test]
fn parse_invalid() {
assert_eq!(
HyperlinkPattern::from_str("foo://bar").unwrap_err(),
HyperlinkPatternError::NoFilePlaceholder
);
assert_eq!(
HyperlinkPattern::from_str("foo://{bar}").unwrap_err(),
HyperlinkPatternError::InvalidPlaceholder("bar".to_string())
);
assert_eq!(
HyperlinkPattern::from_str("foo://{file").unwrap_err(),
HyperlinkPatternError::InvalidSyntax
);
assert_eq!(
HyperlinkPattern::from_str("foo://{file}:{column}").unwrap_err(),
HyperlinkPatternError::NoLinePlaceholder
);
assert_eq!(
HyperlinkPattern::from_str("{file}").unwrap_err(),
HyperlinkPatternError::InvalidScheme
);
assert_eq!(
HyperlinkPattern::from_str(":{file}").unwrap_err(),
HyperlinkPatternError::InvalidScheme
);
assert_eq!(
HyperlinkPattern::from_str("f*:{file}").unwrap_err(),
HyperlinkPatternError::InvalidScheme
);
}
#[test]
fn aliases_are_valid() {
for (name, definition) in HYPERLINK_PATTERN_ALIASES {
assert!(
HyperlinkPattern::from_str(definition).is_ok(),
"invalid hyperlink alias: {}",
name
);
}
}
#[test]
fn aliases_are_sorted() {
let mut names = HYPERLINK_PATTERN_ALIASES.iter().map(|(name, _)| name);
let Some(mut previous_name) = names.next() else {
return;
};
for name in names {
assert!(
name > previous_name,
r#""{}" should be sorted before "{}" in `HYPERLINK_PATTERN_ALIASES`"#,
name,
previous_name
);
previous_name = name;
}
}
}

View File

@ -0,0 +1,23 @@
/// Aliases to well-known hyperlink schemes.
///
/// These need to be sorted by name.
pub const HYPERLINK_PATTERN_ALIASES: &[(&str, &str)] = &[
#[cfg(unix)]
("file", "file://{host}/{file}"),
#[cfg(windows)]
("file", "file:///{file}"),
// https://github.com/misaki-web/grepp
("grep+", "grep+:///{file}:{line}"),
("kitty", "file://{host}/{file}#{line}"),
// https://macvim.org/docs/gui_mac.txt.html#mvim%3A%2F%2F
("macvim", "mvim://open?url=file:///{file}&line={line}&column={column}"),
("none", ""),
// https://github.com/inopinatus/sublime_url
("subl", "subl://open?url=file:///{file}&line={line}&column={column}"),
// https://macromates.com/blog/2007/the-textmate-url-scheme/
("textmate", "txmt://open?url=file:///{file}&line={line}&column={column}"),
// https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls
("vscode", "vscode://file/{file}:{line}:{column}"),
("vscode-insiders", "vscode-insiders://file/{file}:{line}:{column}"),
("vscodium", "vscodium://file/{file}:{line}:{column}"),
];

View File

@ -67,6 +67,10 @@ fn example() -> Result<(), Box<Error>> {
pub use crate::color::{
default_color_specs, ColorError, ColorSpecs, UserColorSpec,
};
pub use crate::hyperlink::{
HyperlinkPath, HyperlinkPattern, HyperlinkPatternError, HyperlinkSpan,
HyperlinkValues,
};
#[cfg(feature = "serde1")]
pub use crate::json::{JSONBuilder, JSONSink, JSON};
pub use crate::standard::{Standard, StandardBuilder, StandardSink};
@ -90,6 +94,8 @@ mod macros;
mod color;
mod counter;
mod hyperlink;
mod hyperlink_aliases;
#[cfg(feature = "serde1")]
mod json;
#[cfg(feature = "serde1")]

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;

View File

@ -10,6 +10,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, PrinterPath};
@ -22,6 +23,7 @@ use crate::util::{find_iter_at_in_context, PrinterPath};
struct Config {
kind: SummaryKind,
colors: ColorSpecs,
hyperlink_pattern: HyperlinkPattern,
stats: bool,
path: bool,
max_matches: Option<u64>,
@ -36,6 +38,7 @@ impl Default for Config {
Config {
kind: SummaryKind::Count,
colors: ColorSpecs::default(),
hyperlink_pattern: HyperlinkPattern::default(),
stats: false,
path: true,
max_matches: None,
@ -160,6 +163,7 @@ impl SummaryBuilder {
Summary {
config: self.config.clone(),
wtr: RefCell::new(CounterWriter::new(wtr)),
buf: vec![],
}
}
@ -206,6 +210,17 @@ impl SummaryBuilder {
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 SummaryBuilder {
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
@ -328,6 +343,7 @@ impl SummaryBuilder {
pub struct Summary<W> {
config: Config,
wtr: RefCell<CounterWriter<W>>,
buf: Vec<u8>,
}
impl<W: WriteColor> Summary<W> {
@ -532,12 +548,9 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
/// write that path to the underlying writer followed by a line terminator.
/// (If a path terminator is set, then that is used instead of the line
/// terminator.)
fn write_path_line(&self, searcher: &Searcher) -> io::Result<()> {
if let Some(ref path) = self.path {
self.write_spec(
self.summary.config.colors.path(),
path.as_bytes(),
)?;
fn write_path_line(&mut self, searcher: &Searcher) -> io::Result<()> {
if self.path.is_some() {
self.write_path()?;
if let Some(term) = self.summary.config.path_terminator {
self.write(&[term])?;
} else {
@ -551,12 +564,9 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
/// write that path to the underlying writer followed by the field
/// separator. (If a path terminator is set, then that is used instead of
/// the field separator.)
fn write_path_field(&self) -> io::Result<()> {
if let Some(ref path) = self.path {
self.write_spec(
self.summary.config.colors.path(),
path.as_bytes(),
)?;
fn write_path_field(&mut self) -> io::Result<()> {
if self.path.is_some() {
self.write_path()?;
if let Some(term) = self.summary.config.path_terminator {
self.write(&[term])?;
} else {
@ -566,6 +576,43 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
Ok(())
}
/// If this printer has a file path associated with it, then this will
/// write that path to the underlying writer in the appropriate style
/// (color and hyperlink).
fn write_path(&mut self) -> io::Result<()> {
if self.path.is_some() {
let mut hyperlink = self.start_hyperlink_span()?;
self.write_spec(
self.summary.config.colors.path(),
self.path.as_ref().unwrap().as_bytes(),
)?;
if hyperlink.is_active() {
hyperlink.end(&mut *self.summary.wtr.borrow_mut())?;
}
}
Ok(())
}
/// Starts a hyperlink span when applicable.
fn start_hyperlink_span(&mut self) -> io::Result<HyperlinkSpan> {
if let Some(ref path) = self.path {
let mut wtr = self.summary.wtr.borrow_mut();
if wtr.supports_hyperlinks() {
if let Some(spec) = path.create_hyperlink_spec(
&self.summary.config.hyperlink_pattern,
None,
None,
&mut self.summary.buf,
) {
return Ok(HyperlinkSpan::start(&mut *wtr, &spec)?);
}
}
}
Ok(HyperlinkSpan::default())
}
/// Write the line terminator configured on the given searcher.
fn write_line_term(&self, searcher: &Searcher) -> io::Result<()> {
self.write(searcher.line_terminator().as_bytes())
@ -704,11 +751,11 @@ impl<'p, 's, M: Matcher, W: WriteColor> Sink for SummarySink<'p, 's, M, W> {
}
SummaryKind::CountMatches => {
if show_count {
self.write_path_field()?;
let stats = self
.stats
.as_ref()
.expect("CountMatches should enable stats tracking");
self.write_path_field()?;
self.write(stats.matches().to_string().as_bytes())?;
self.write_line_term(searcher)?;
}

View File

@ -1,8 +1,8 @@
use std::borrow::Cow;
use std::fmt;
use std::io;
use std::cell::OnceCell;
use std::path::Path;
use std::time;
use std::{fmt, io};
use bstr::{ByteSlice, ByteVec};
use grep_matcher::{Captures, LineTerminator, Match, Matcher};
@ -11,7 +11,9 @@ use grep_searcher::{
};
#[cfg(feature = "serde1")]
use serde::{Serialize, Serializer};
use termcolor::HyperlinkSpec;
use crate::hyperlink::{HyperlinkPath, HyperlinkPattern, HyperlinkValues};
use crate::MAX_LOOK_AHEAD;
/// A type for handling replacements while amortizing allocation.
@ -276,12 +278,20 @@ impl<'a> Sunk<'a> {
/// portability with a small cost: on Windows, paths that are not valid UTF-16
/// will not roundtrip correctly.
#[derive(Clone, Debug)]
pub struct PrinterPath<'a>(Cow<'a, [u8]>);
pub struct PrinterPath<'a> {
path: &'a Path,
bytes: Cow<'a, [u8]>,
hyperlink_path: OnceCell<Option<HyperlinkPath>>,
}
impl<'a> PrinterPath<'a> {
/// Create a new path suitable for printing.
pub fn new(path: &'a Path) -> PrinterPath<'a> {
PrinterPath(Vec::from_path_lossy(path))
PrinterPath {
path,
bytes: Vec::from_path_lossy(path),
hyperlink_path: OnceCell::new(),
}
}
/// Create a new printer path from the given path which can be efficiently
@ -303,7 +313,7 @@ impl<'a> PrinterPath<'a> {
/// environments, only `/` is treated as a path separator.
fn replace_separator(&mut self, new_sep: u8) {
let transformed_path: Vec<u8> = self
.0
.as_bytes()
.bytes()
.map(|b| {
if b == b'/' || (cfg!(windows) && b == b'\\') {
@ -313,12 +323,40 @@ impl<'a> PrinterPath<'a> {
}
})
.collect();
self.0 = Cow::Owned(transformed_path);
self.bytes = Cow::Owned(transformed_path);
}
/// Return the raw bytes for this path.
pub fn as_bytes(&self) -> &[u8] {
&self.0
&self.bytes
}
/// Creates a hyperlink for this path and the given line and column, using the specified
/// pattern. Uses the given buffer to store the hyperlink.
pub fn create_hyperlink_spec<'b>(
&self,
pattern: &HyperlinkPattern,
line_number: Option<u64>,
column: Option<u64>,
buffer: &'b mut Vec<u8>,
) -> Option<HyperlinkSpec<'b>> {
if pattern.is_empty() {
return None;
}
let file_path = self.hyperlink_path()?;
let values = HyperlinkValues::new(file_path, line_number, column);
buffer.clear();
pattern.render(&values, buffer).ok()?;
Some(HyperlinkSpec::open(buffer))
}
/// Returns the file path to use in hyperlinks, if any.
///
/// This is what the {file} placeholder will be substituted with.
fn hyperlink_path(&self) -> Option<&HyperlinkPath> {
self.hyperlink_path
.get_or_init(|| HyperlinkPath::from_path(self.path))
.as_ref()
}
}

View File

@ -380,6 +380,7 @@ rgtest!(r428_color_context_path, |dir: Dir, mut cmd: TestCommand| {
"-N",
"--colors=match:none",
"--color=always",
"--hyperlink-format=",
"foo",
]);