mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-05-18 17:20:21 -07:00
printer: add hyperlinks
This commit represents the initial work to get hyperlinks working and was submitted as part of PR #2483. Subsequent commits largely retain the functionality and structure of the hyperlink support added here, but rejigger some things around.
This commit is contained in:
parent
86ef683308
commit
1a50324013
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,7 @@ target
|
|||||||
/termcolor/Cargo.lock
|
/termcolor/Cargo.lock
|
||||||
/wincolor/Cargo.lock
|
/wincolor/Cargo.lock
|
||||||
/deployment
|
/deployment
|
||||||
|
/.idea
|
||||||
|
|
||||||
# Snapcraft files
|
# Snapcraft files
|
||||||
stage
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -208,9 +218,11 @@ version = "0.1.7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bstr",
|
"bstr",
|
||||||
|
"gethostname",
|
||||||
"grep-matcher",
|
"grep-matcher",
|
||||||
"grep-regex",
|
"grep-regex",
|
||||||
"grep-searcher",
|
"grep-searcher",
|
||||||
|
"lazy_static",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
@ -612,3 +624,60 @@ name = "winapi-x86_64-pc-windows-gnu"
|
|||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
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]'
|
'--debug[show debug messages]'
|
||||||
'--field-context-separator[set string to delimit fields in context lines]'
|
'--field-context-separator[set string to delimit fields in context lines]'
|
||||||
'--field-match-separator[set string to delimit fields in matching 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]'
|
'--trace[show more verbose debug messages]'
|
||||||
'--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size (bytes)'
|
'--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 stats)--files[show each file that would be searched (but don't search)]"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use termcolor;
|
use termcolor;
|
||||||
|
use termcolor::HyperlinkSpec;
|
||||||
|
|
||||||
use crate::is_tty_stdout;
|
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]
|
#[inline]
|
||||||
fn set_color(&mut self, spec: &termcolor::ColorSpec) -> io::Result<()> {
|
fn set_color(&mut self, spec: &termcolor::ColorSpec) -> io::Result<()> {
|
||||||
use self::StandardStreamKind::*;
|
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]
|
#[inline]
|
||||||
fn reset(&mut self) -> io::Result<()> {
|
fn reset(&mut self) -> io::Result<()> {
|
||||||
use self::StandardStreamKind::*;
|
use self::StandardStreamKind::*;
|
||||||
|
@ -580,6 +580,7 @@ pub fn all_args_and_flags() -> Vec<RGArg> {
|
|||||||
flag_glob_case_insensitive(&mut args);
|
flag_glob_case_insensitive(&mut args);
|
||||||
flag_heading(&mut args);
|
flag_heading(&mut args);
|
||||||
flag_hidden(&mut args);
|
flag_hidden(&mut args);
|
||||||
|
flag_hyperlink_format(&mut args);
|
||||||
flag_iglob(&mut args);
|
flag_iglob(&mut args);
|
||||||
flag_ignore_case(&mut args);
|
flag_ignore_case(&mut args);
|
||||||
flag_ignore_file(&mut args);
|
flag_ignore_file(&mut args);
|
||||||
@ -1494,6 +1495,26 @@ This flag can be disabled with --no-hidden.
|
|||||||
args.push(arg);
|
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>) {
|
fn flag_iglob(args: &mut Vec<RGArg>) {
|
||||||
const SHORT: &str = "Include or exclude files case insensitively.";
|
const SHORT: &str = "Include or exclude files case insensitively.";
|
||||||
const LONG: &str = long!(
|
const LONG: &str = long!(
|
||||||
|
@ -5,6 +5,7 @@ use std::fs;
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
@ -17,8 +18,8 @@ use grep::pcre2::{
|
|||||||
RegexMatcherBuilder as PCRE2RegexMatcherBuilder,
|
RegexMatcherBuilder as PCRE2RegexMatcherBuilder,
|
||||||
};
|
};
|
||||||
use grep::printer::{
|
use grep::printer::{
|
||||||
default_color_specs, ColorSpecs, JSONBuilder, Standard, StandardBuilder,
|
default_color_specs, ColorSpecs, HyperlinkPattern, JSONBuilder, Standard,
|
||||||
Stats, Summary, SummaryBuilder, SummaryKind, JSON,
|
StandardBuilder, Stats, Summary, SummaryBuilder, SummaryKind, JSON,
|
||||||
};
|
};
|
||||||
use grep::regex::{
|
use grep::regex::{
|
||||||
RegexMatcher as RustRegexMatcher,
|
RegexMatcher as RustRegexMatcher,
|
||||||
@ -235,6 +236,7 @@ impl Args {
|
|||||||
let mut builder = PathPrinterBuilder::new();
|
let mut builder = PathPrinterBuilder::new();
|
||||||
builder
|
builder
|
||||||
.color_specs(self.matches().color_specs()?)
|
.color_specs(self.matches().color_specs()?)
|
||||||
|
.hyperlink_pattern(self.matches().hyperlink_pattern()?)
|
||||||
.separator(self.matches().path_separator()?)
|
.separator(self.matches().path_separator()?)
|
||||||
.terminator(self.matches().path_terminator().unwrap_or(b'\n'));
|
.terminator(self.matches().path_terminator().unwrap_or(b'\n'));
|
||||||
Ok(builder.build(wtr))
|
Ok(builder.build(wtr))
|
||||||
@ -772,6 +774,7 @@ impl ArgMatches {
|
|||||||
let mut builder = StandardBuilder::new();
|
let mut builder = StandardBuilder::new();
|
||||||
builder
|
builder
|
||||||
.color_specs(self.color_specs()?)
|
.color_specs(self.color_specs()?)
|
||||||
|
.hyperlink_pattern(self.hyperlink_pattern()?)
|
||||||
.stats(self.stats())
|
.stats(self.stats())
|
||||||
.heading(self.heading())
|
.heading(self.heading())
|
||||||
.path(self.with_filename(paths))
|
.path(self.with_filename(paths))
|
||||||
@ -811,6 +814,7 @@ impl ArgMatches {
|
|||||||
builder
|
builder
|
||||||
.kind(self.summary_kind().expect("summary format"))
|
.kind(self.summary_kind().expect("summary format"))
|
||||||
.color_specs(self.color_specs()?)
|
.color_specs(self.color_specs()?)
|
||||||
|
.hyperlink_pattern(self.hyperlink_pattern()?)
|
||||||
.stats(self.stats())
|
.stats(self.stats())
|
||||||
.path(self.with_filename(paths))
|
.path(self.with_filename(paths))
|
||||||
.max_matches(self.max_count()?)
|
.max_matches(self.max_count()?)
|
||||||
@ -1118,6 +1122,17 @@ impl ArgMatches {
|
|||||||
self.is_present("hidden") || self.unrestricted_count() >= 2
|
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.
|
/// Returns true if ignore files should be processed case insensitively.
|
||||||
fn ignore_file_case_insensitive(&self) -> bool {
|
fn ignore_file_case_insensitive(&self) -> bool {
|
||||||
self.is_present("ignore-file-case-insensitive")
|
self.is_present("ignore-file-case-insensitive")
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use grep::printer::{ColorSpecs, PrinterPath};
|
use grep::printer::{
|
||||||
|
ColorSpecs, HyperlinkPattern, HyperlinkSpan, PrinterPath,
|
||||||
|
};
|
||||||
use termcolor::WriteColor;
|
use termcolor::WriteColor;
|
||||||
|
|
||||||
/// A configuration for describing how paths should be written.
|
/// A configuration for describing how paths should be written.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct Config {
|
struct Config {
|
||||||
colors: ColorSpecs,
|
colors: ColorSpecs,
|
||||||
|
hyperlink_pattern: HyperlinkPattern,
|
||||||
separator: Option<u8>,
|
separator: Option<u8>,
|
||||||
terminator: u8,
|
terminator: u8,
|
||||||
}
|
}
|
||||||
@ -16,6 +19,7 @@ impl Default for Config {
|
|||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
colors: ColorSpecs::default(),
|
colors: ColorSpecs::default(),
|
||||||
|
hyperlink_pattern: HyperlinkPattern::default(),
|
||||||
separator: None,
|
separator: None,
|
||||||
terminator: b'\n',
|
terminator: b'\n',
|
||||||
}
|
}
|
||||||
@ -37,7 +41,7 @@ impl PathPrinterBuilder {
|
|||||||
/// Create a new path printer with the current configuration that writes
|
/// Create a new path printer with the current configuration that writes
|
||||||
/// paths to the given writer.
|
/// paths to the given writer.
|
||||||
pub fn build<W: WriteColor>(&self, wtr: W) -> PathPrinter<W> {
|
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.
|
/// Set the color specification for this printer.
|
||||||
@ -52,6 +56,17 @@ impl PathPrinterBuilder {
|
|||||||
self
|
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.
|
/// A path separator.
|
||||||
///
|
///
|
||||||
/// When provided, the path's default separator will be replaced with
|
/// When provided, the path's default separator will be replaced with
|
||||||
@ -80,6 +95,7 @@ impl PathPrinterBuilder {
|
|||||||
pub struct PathPrinter<W> {
|
pub struct PathPrinter<W> {
|
||||||
config: Config,
|
config: Config,
|
||||||
wtr: W,
|
wtr: W,
|
||||||
|
buf: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: WriteColor> PathPrinter<W> {
|
impl<W: WriteColor> PathPrinter<W> {
|
||||||
@ -89,10 +105,30 @@ impl<W: WriteColor> PathPrinter<W> {
|
|||||||
if !self.wtr.supports_color() {
|
if !self.wtr.supports_color() {
|
||||||
self.wtr.write_all(ppath.as_bytes())?;
|
self.wtr.write_all(ppath.as_bytes())?;
|
||||||
} else {
|
} else {
|
||||||
|
let mut hyperlink = self.start_hyperlink_span(&ppath)?;
|
||||||
self.wtr.set_color(self.config.colors.path())?;
|
self.wtr.set_color(self.config.colors.path())?;
|
||||||
self.wtr.write_all(ppath.as_bytes())?;
|
self.wtr.write_all(ppath.as_bytes())?;
|
||||||
self.wtr.reset()?;
|
self.wtr.reset()?;
|
||||||
|
hyperlink.end(&mut self.wtr)?;
|
||||||
}
|
}
|
||||||
self.wtr.write_all(&[self.config.terminator])
|
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]
|
[dependencies]
|
||||||
base64 = { version = "0.20.0", optional = true }
|
base64 = { version = "0.20.0", optional = true }
|
||||||
bstr = "1.6.0"
|
bstr = "1.6.0"
|
||||||
|
gethostname = "0.4.3"
|
||||||
grep-matcher = { version = "0.1.6", path = "../matcher" }
|
grep-matcher = { version = "0.1.6", path = "../matcher" }
|
||||||
grep-searcher = { version = "0.1.11", path = "../searcher" }
|
grep-searcher = { version = "0.1.11", path = "../searcher" }
|
||||||
|
lazy_static = "1.1.0"
|
||||||
termcolor = "1.0.4"
|
termcolor = "1.0.4"
|
||||||
serde = { version = "1.0.77", optional = true, features = ["derive"] }
|
serde = { version = "1.0.77", optional = true, features = ["derive"] }
|
||||||
serde_json = { version = "1.0.27", optional = true }
|
serde_json = { version = "1.0.27", optional = true }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::io::{self, Write};
|
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
|
/// A writer that counts the number of bytes that have been successfully
|
||||||
/// written.
|
/// written.
|
||||||
@ -76,10 +76,18 @@ impl<W: WriteColor> WriteColor for CounterWriter<W> {
|
|||||||
self.wtr.supports_color()
|
self.wtr.supports_color()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_hyperlinks(&self) -> bool {
|
||||||
|
self.wtr.supports_hyperlinks()
|
||||||
|
}
|
||||||
|
|
||||||
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
|
fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
|
||||||
self.wtr.set_color(spec)
|
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<()> {
|
fn reset(&mut self) -> io::Result<()> {
|
||||||
self.wtr.reset()
|
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::{
|
pub use crate::color::{
|
||||||
default_color_specs, ColorError, ColorSpecs, UserColorSpec,
|
default_color_specs, ColorError, ColorSpecs, UserColorSpec,
|
||||||
};
|
};
|
||||||
|
pub use crate::hyperlink::{
|
||||||
|
HyperlinkPath, HyperlinkPattern, HyperlinkPatternError, HyperlinkSpan,
|
||||||
|
HyperlinkValues,
|
||||||
|
};
|
||||||
#[cfg(feature = "serde1")]
|
#[cfg(feature = "serde1")]
|
||||||
pub use crate::json::{JSONBuilder, JSONSink, JSON};
|
pub use crate::json::{JSONBuilder, JSONSink, JSON};
|
||||||
pub use crate::standard::{Standard, StandardBuilder, StandardSink};
|
pub use crate::standard::{Standard, StandardBuilder, StandardSink};
|
||||||
@ -90,6 +94,8 @@ mod macros;
|
|||||||
|
|
||||||
mod color;
|
mod color;
|
||||||
mod counter;
|
mod counter;
|
||||||
|
mod hyperlink;
|
||||||
|
mod hyperlink_aliases;
|
||||||
#[cfg(feature = "serde1")]
|
#[cfg(feature = "serde1")]
|
||||||
mod json;
|
mod json;
|
||||||
#[cfg(feature = "serde1")]
|
#[cfg(feature = "serde1")]
|
||||||
|
@ -15,6 +15,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor};
|
|||||||
|
|
||||||
use crate::color::ColorSpecs;
|
use crate::color::ColorSpecs;
|
||||||
use crate::counter::CounterWriter;
|
use crate::counter::CounterWriter;
|
||||||
|
use crate::hyperlink::{HyperlinkPattern, HyperlinkSpan};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::util::{
|
use crate::util::{
|
||||||
find_iter_at_in_context, trim_ascii_prefix, trim_line_terminator,
|
find_iter_at_in_context, trim_ascii_prefix, trim_line_terminator,
|
||||||
@ -29,6 +30,7 @@ use crate::util::{
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Config {
|
struct Config {
|
||||||
colors: ColorSpecs,
|
colors: ColorSpecs,
|
||||||
|
hyperlink_pattern: HyperlinkPattern,
|
||||||
stats: bool,
|
stats: bool,
|
||||||
heading: bool,
|
heading: bool,
|
||||||
path: bool,
|
path: bool,
|
||||||
@ -54,6 +56,7 @@ impl Default for Config {
|
|||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
colors: ColorSpecs::default(),
|
colors: ColorSpecs::default(),
|
||||||
|
hyperlink_pattern: HyperlinkPattern::default(),
|
||||||
stats: false,
|
stats: false,
|
||||||
heading: false,
|
heading: false,
|
||||||
path: true,
|
path: true,
|
||||||
@ -122,6 +125,7 @@ impl StandardBuilder {
|
|||||||
Standard {
|
Standard {
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
wtr: RefCell::new(CounterWriter::new(wtr)),
|
wtr: RefCell::new(CounterWriter::new(wtr)),
|
||||||
|
buf: RefCell::new(vec![]),
|
||||||
matches: vec![],
|
matches: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,6 +164,17 @@ impl StandardBuilder {
|
|||||||
self
|
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.
|
/// Enable the gathering of various aggregate statistics.
|
||||||
///
|
///
|
||||||
/// When this is enabled (it's disabled by default), statistics will be
|
/// When this is enabled (it's disabled by default), statistics will be
|
||||||
@ -467,6 +482,7 @@ impl StandardBuilder {
|
|||||||
pub struct Standard<W> {
|
pub struct Standard<W> {
|
||||||
config: Config,
|
config: Config,
|
||||||
wtr: RefCell<CounterWriter<W>>,
|
wtr: RefCell<CounterWriter<W>>,
|
||||||
|
buf: RefCell<Vec<u8>>,
|
||||||
matches: Vec<Match>,
|
matches: Vec<Match>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1209,23 +1225,25 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
line_number: Option<u64>,
|
line_number: Option<u64>,
|
||||||
column: Option<u64>,
|
column: Option<u64>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let sep = self.separator_field();
|
let mut prelude = PreludeWriter::new(self);
|
||||||
|
prelude.start(line_number, column)?;
|
||||||
|
|
||||||
if !self.config().heading {
|
if !self.config().heading {
|
||||||
self.write_path_field(sep)?;
|
prelude.write_path()?;
|
||||||
}
|
}
|
||||||
if let Some(n) = line_number {
|
if let Some(n) = line_number {
|
||||||
self.write_line_number(n, sep)?;
|
prelude.write_line_number(n)?;
|
||||||
}
|
}
|
||||||
if let Some(n) = column {
|
if let Some(n) = column {
|
||||||
if self.config().column {
|
if self.config().column {
|
||||||
self.write_column_number(n, sep)?;
|
prelude.write_column_number(n)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.config().byte_offset {
|
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)]
|
#[inline(always)]
|
||||||
@ -1386,7 +1404,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
/// terminator.)
|
/// terminator.)
|
||||||
fn write_path_line(&self) -> io::Result<()> {
|
fn write_path_line(&self) -> io::Result<()> {
|
||||||
if let Some(path) = self.path() {
|
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 {
|
if let Some(term) = self.config().path_terminator {
|
||||||
self.write(&[term])?;
|
self.write(&[term])?;
|
||||||
} else {
|
} else {
|
||||||
@ -1396,22 +1414,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
Ok(())
|
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<()> {
|
fn write_search_prelude(&self) -> io::Result<()> {
|
||||||
let this_search_written = self.wtr().borrow().count() > 0;
|
let this_search_written = self.wtr().borrow().count() > 0;
|
||||||
if this_search_written {
|
if this_search_written {
|
||||||
@ -1438,7 +1440,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
let bin = self.searcher.binary_detection();
|
let bin = self.searcher.binary_detection();
|
||||||
if let Some(byte) = bin.quit_byte() {
|
if let Some(byte) = bin.quit_byte() {
|
||||||
if let Some(path) = self.path() {
|
if let Some(path) = self.path() {
|
||||||
self.write_spec(self.config().colors.path(), path.as_bytes())?;
|
self.write_path_hyperlink(path)?;
|
||||||
self.write(b": ")?;
|
self.write(b": ")?;
|
||||||
}
|
}
|
||||||
let remainder = format!(
|
let remainder = format!(
|
||||||
@ -1450,7 +1452,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
self.write(remainder.as_bytes())?;
|
self.write(remainder.as_bytes())?;
|
||||||
} else if let Some(byte) = bin.convert_byte() {
|
} else if let Some(byte) = bin.convert_byte() {
|
||||||
if let Some(path) = self.path() {
|
if let Some(path) = self.path() {
|
||||||
self.write_spec(self.config().colors.path(), path.as_bytes())?;
|
self.write_path_hyperlink(path)?;
|
||||||
self.write(b": ")?;
|
self.write(b": ")?;
|
||||||
}
|
}
|
||||||
let remainder = format!(
|
let remainder = format!(
|
||||||
@ -1471,39 +1473,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
Ok(())
|
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<()> {
|
fn write_line_term(&self) -> io::Result<()> {
|
||||||
self.write(self.searcher.line_terminator().as_bytes())
|
self.write(self.searcher.line_terminator().as_bytes())
|
||||||
}
|
}
|
||||||
@ -1516,6 +1485,40 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
Ok(())
|
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<()> {
|
fn start_color_match(&self) -> io::Result<()> {
|
||||||
if self.in_color_match.get() {
|
if self.in_color_match.get() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -1569,6 +1572,12 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
&self.sink.standard.wtr
|
&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.
|
/// Return the path associated with this printer, if one exists.
|
||||||
fn path(&self) -> Option<&'a PrinterPath<'a>> {
|
fn path(&self) -> Option<&'a PrinterPath<'a>> {
|
||||||
self.sink.path.as_ref()
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use grep_matcher::LineTerminator;
|
use grep_matcher::LineTerminator;
|
||||||
|
@ -10,6 +10,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor};
|
|||||||
|
|
||||||
use crate::color::ColorSpecs;
|
use crate::color::ColorSpecs;
|
||||||
use crate::counter::CounterWriter;
|
use crate::counter::CounterWriter;
|
||||||
|
use crate::hyperlink::{HyperlinkPattern, HyperlinkSpan};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::util::{find_iter_at_in_context, PrinterPath};
|
use crate::util::{find_iter_at_in_context, PrinterPath};
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ use crate::util::{find_iter_at_in_context, PrinterPath};
|
|||||||
struct Config {
|
struct Config {
|
||||||
kind: SummaryKind,
|
kind: SummaryKind,
|
||||||
colors: ColorSpecs,
|
colors: ColorSpecs,
|
||||||
|
hyperlink_pattern: HyperlinkPattern,
|
||||||
stats: bool,
|
stats: bool,
|
||||||
path: bool,
|
path: bool,
|
||||||
max_matches: Option<u64>,
|
max_matches: Option<u64>,
|
||||||
@ -36,6 +38,7 @@ impl Default for Config {
|
|||||||
Config {
|
Config {
|
||||||
kind: SummaryKind::Count,
|
kind: SummaryKind::Count,
|
||||||
colors: ColorSpecs::default(),
|
colors: ColorSpecs::default(),
|
||||||
|
hyperlink_pattern: HyperlinkPattern::default(),
|
||||||
stats: false,
|
stats: false,
|
||||||
path: true,
|
path: true,
|
||||||
max_matches: None,
|
max_matches: None,
|
||||||
@ -160,6 +163,7 @@ impl SummaryBuilder {
|
|||||||
Summary {
|
Summary {
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
wtr: RefCell::new(CounterWriter::new(wtr)),
|
wtr: RefCell::new(CounterWriter::new(wtr)),
|
||||||
|
buf: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,6 +210,17 @@ impl SummaryBuilder {
|
|||||||
self
|
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.
|
/// Enable the gathering of various aggregate statistics.
|
||||||
///
|
///
|
||||||
/// When this is enabled (it's disabled by default), statistics will be
|
/// When this is enabled (it's disabled by default), statistics will be
|
||||||
@ -328,6 +343,7 @@ impl SummaryBuilder {
|
|||||||
pub struct Summary<W> {
|
pub struct Summary<W> {
|
||||||
config: Config,
|
config: Config,
|
||||||
wtr: RefCell<CounterWriter<W>>,
|
wtr: RefCell<CounterWriter<W>>,
|
||||||
|
buf: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: WriteColor> Summary<W> {
|
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.
|
/// 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
|
/// (If a path terminator is set, then that is used instead of the line
|
||||||
/// terminator.)
|
/// terminator.)
|
||||||
fn write_path_line(&self, searcher: &Searcher) -> io::Result<()> {
|
fn write_path_line(&mut self, searcher: &Searcher) -> io::Result<()> {
|
||||||
if let Some(ref path) = self.path {
|
if self.path.is_some() {
|
||||||
self.write_spec(
|
self.write_path()?;
|
||||||
self.summary.config.colors.path(),
|
|
||||||
path.as_bytes(),
|
|
||||||
)?;
|
|
||||||
if let Some(term) = self.summary.config.path_terminator {
|
if let Some(term) = self.summary.config.path_terminator {
|
||||||
self.write(&[term])?;
|
self.write(&[term])?;
|
||||||
} else {
|
} 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
|
/// write that path to the underlying writer followed by the field
|
||||||
/// separator. (If a path terminator is set, then that is used instead of
|
/// separator. (If a path terminator is set, then that is used instead of
|
||||||
/// the field separator.)
|
/// the field separator.)
|
||||||
fn write_path_field(&self) -> io::Result<()> {
|
fn write_path_field(&mut self) -> io::Result<()> {
|
||||||
if let Some(ref path) = self.path {
|
if self.path.is_some() {
|
||||||
self.write_spec(
|
self.write_path()?;
|
||||||
self.summary.config.colors.path(),
|
|
||||||
path.as_bytes(),
|
|
||||||
)?;
|
|
||||||
if let Some(term) = self.summary.config.path_terminator {
|
if let Some(term) = self.summary.config.path_terminator {
|
||||||
self.write(&[term])?;
|
self.write(&[term])?;
|
||||||
} else {
|
} else {
|
||||||
@ -566,6 +576,43 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
|
|||||||
Ok(())
|
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.
|
/// Write the line terminator configured on the given searcher.
|
||||||
fn write_line_term(&self, searcher: &Searcher) -> io::Result<()> {
|
fn write_line_term(&self, searcher: &Searcher) -> io::Result<()> {
|
||||||
self.write(searcher.line_terminator().as_bytes())
|
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 => {
|
SummaryKind::CountMatches => {
|
||||||
if show_count {
|
if show_count {
|
||||||
|
self.write_path_field()?;
|
||||||
let stats = self
|
let stats = self
|
||||||
.stats
|
.stats
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("CountMatches should enable stats tracking");
|
.expect("CountMatches should enable stats tracking");
|
||||||
self.write_path_field()?;
|
|
||||||
self.write(stats.matches().to_string().as_bytes())?;
|
self.write(stats.matches().to_string().as_bytes())?;
|
||||||
self.write_line_term(searcher)?;
|
self.write_line_term(searcher)?;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::cell::OnceCell;
|
||||||
use std::io;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
use bstr::{ByteSlice, ByteVec};
|
use bstr::{ByteSlice, ByteVec};
|
||||||
use grep_matcher::{Captures, LineTerminator, Match, Matcher};
|
use grep_matcher::{Captures, LineTerminator, Match, Matcher};
|
||||||
@ -11,7 +11,9 @@ use grep_searcher::{
|
|||||||
};
|
};
|
||||||
#[cfg(feature = "serde1")]
|
#[cfg(feature = "serde1")]
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
|
use termcolor::HyperlinkSpec;
|
||||||
|
|
||||||
|
use crate::hyperlink::{HyperlinkPath, HyperlinkPattern, HyperlinkValues};
|
||||||
use crate::MAX_LOOK_AHEAD;
|
use crate::MAX_LOOK_AHEAD;
|
||||||
|
|
||||||
/// A type for handling replacements while amortizing allocation.
|
/// 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
|
/// portability with a small cost: on Windows, paths that are not valid UTF-16
|
||||||
/// will not roundtrip correctly.
|
/// will not roundtrip correctly.
|
||||||
#[derive(Clone, Debug)]
|
#[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> {
|
impl<'a> PrinterPath<'a> {
|
||||||
/// Create a new path suitable for printing.
|
/// Create a new path suitable for printing.
|
||||||
pub fn new(path: &'a Path) -> PrinterPath<'a> {
|
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
|
/// 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.
|
/// environments, only `/` is treated as a path separator.
|
||||||
fn replace_separator(&mut self, new_sep: u8) {
|
fn replace_separator(&mut self, new_sep: u8) {
|
||||||
let transformed_path: Vec<u8> = self
|
let transformed_path: Vec<u8> = self
|
||||||
.0
|
.as_bytes()
|
||||||
.bytes()
|
.bytes()
|
||||||
.map(|b| {
|
.map(|b| {
|
||||||
if b == b'/' || (cfg!(windows) && b == b'\\') {
|
if b == b'/' || (cfg!(windows) && b == b'\\') {
|
||||||
@ -313,12 +323,40 @@ impl<'a> PrinterPath<'a> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
self.0 = Cow::Owned(transformed_path);
|
self.bytes = Cow::Owned(transformed_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the raw bytes for this path.
|
/// Return the raw bytes for this path.
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
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",
|
"-N",
|
||||||
"--colors=match:none",
|
"--colors=match:none",
|
||||||
"--color=always",
|
"--color=always",
|
||||||
|
"--hyperlink-format=",
|
||||||
"foo",
|
"foo",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user