mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2024-12-07 11:13:17 +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:
parent
86ef683308
commit
1a50324013
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,7 @@ target
|
||||
/termcolor/Cargo.lock
|
||||
/wincolor/Cargo.lock
|
||||
/deployment
|
||||
/.idea
|
||||
|
||||
# Snapcraft files
|
||||
stage
|
||||
|
69
Cargo.lock
generated
69
Cargo.lock
generated
@ -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"
|
||||
|
@ -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)]"
|
||||
|
@ -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::*;
|
||||
|
@ -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!(
|
||||
|
@ -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")
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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()
|
||||
}
|
||||
|
664
crates/printer/src/hyperlink.rs
Normal file
664
crates/printer/src/hyperlink.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
23
crates/printer/src/hyperlink_aliases.rs
Normal file
23
crates/printer/src/hyperlink_aliases.rs
Normal 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}"),
|
||||
];
|
@ -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")]
|
||||
|
@ -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;
|
||||
|
@ -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)?;
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,6 +380,7 @@ rgtest!(r428_color_context_path, |dir: Dir, mut cmd: TestCommand| {
|
||||
"-N",
|
||||
"--colors=match:none",
|
||||
"--color=always",
|
||||
"--hyperlink-format=",
|
||||
"foo",
|
||||
]);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user