mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-04-19 09:02:15 +02:00
hyperlink: rejigger how hyperlinks work
This essentially takes the work done in #2483 and does a bit of a facelift. A brief summary: * We reduce the hyperlink API we expose to just the format, a configuration and an environment. * We move buffer management into a hyperlink-specific interpolator. * We expand the documentation on --hyperlink-format. * We rewrite the hyperlink format parser to be a simple state machine with support for escaping '{{' and '}}'. * We remove the 'gethostname' dependency and instead insist on the caller to provide the hostname. (So grep-printer doesn't get it itself, but the application will.) Similarly for the WSL prefix. * Probably some other things. Overall, the general structure of #2483 was kept. The biggest change is probably requiring the caller to pass in things like a hostname instead of having the crate do it. I did this for a couple reasons: 1. I feel uncomfortable with code deep inside the printing logic reaching out into the environment to assume responsibility for retrieving the hostname. This feels more like an application-level responsibility. Arguably, path canonicalization falls into this same bucket, but it is more difficult to rip that out. (And we can do it in the future in a backwards compatible fashion I think.) 2. I wanted to permit end users to tell ripgrep about their system's hostname in their own way, e.g., by running a custom executable. I want this because I know at least for my own use cases, I sometimes log into systems using an SSH hostname that is distinct from the system's actual hostname (usually because the system is shared in some way or changing its hostname is not allowed/practical). I think that's about it. Closes #665, Closes #2483
This commit is contained in:
parent
23e21133ba
commit
f608d4d9b3
69
Cargo.lock
generated
69
Cargo.lock
generated
@ -136,16 +136,6 @@ 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"
|
||||||
@ -216,10 +206,10 @@ version = "0.1.7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bstr",
|
"bstr",
|
||||||
"gethostname",
|
|
||||||
"grep-matcher",
|
"grep-matcher",
|
||||||
"grep-regex",
|
"grep-regex",
|
||||||
"grep-searcher",
|
"grep-searcher",
|
||||||
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
@ -621,60 +611,3 @@ 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]'
|
||||||
|
'--hostname-bin=[executable for getting system hostname]:hostname executable:_command_names -e'
|
||||||
'--hyperlink-format=[specify pattern for hyperlinks]:pattern'
|
'--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)'
|
||||||
|
@ -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_hostname_bin(&mut args);
|
||||||
flag_hyperlink_format(&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);
|
||||||
@ -1495,19 +1496,93 @@ This flag can be disabled with --no-hidden.
|
|||||||
args.push(arg);
|
args.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flag_hostname_bin(args: &mut Vec<RGArg>) {
|
||||||
|
const SHORT: &str = "Run a program to get this system's hostname.";
|
||||||
|
const LONG: &str = long!(
|
||||||
|
"\
|
||||||
|
This flag controls how ripgrep determines this system's hostname. The flag's
|
||||||
|
value should correspond to an executable (either a path or something that can
|
||||||
|
be found via your system's *PATH* environment variable). When set, ripgrep will
|
||||||
|
run this executable, with no arguments, and treat its output (with leading and
|
||||||
|
trailing whitespace stripped) as your system's hostname.
|
||||||
|
|
||||||
|
When not set (the default, or the empty string), ripgrep will try to
|
||||||
|
automatically detect your system's hostname. On Unix, this corresponds
|
||||||
|
to calling *gethostname*. On Windows, this corresponds to calling
|
||||||
|
*GetComputerNameExW* to fetch the system's \"physical DNS hostname.\"
|
||||||
|
|
||||||
|
ripgrep uses your system's hostname for producing hyperlinks.
|
||||||
|
"
|
||||||
|
);
|
||||||
|
let arg =
|
||||||
|
RGArg::flag("hostname-bin", "COMMAND").help(SHORT).long_help(LONG);
|
||||||
|
args.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
fn flag_hyperlink_format(args: &mut Vec<RGArg>) {
|
fn flag_hyperlink_format(args: &mut Vec<RGArg>) {
|
||||||
const SHORT: &str = "Set the format of hyperlinks to match results.";
|
const SHORT: &str = "Set the format of hyperlinks to match results.";
|
||||||
const LONG: &str = long!(
|
const LONG: &str = long!(
|
||||||
"\
|
"\
|
||||||
Set the format of hyperlinks to match results. This defines a pattern which
|
Set the format of hyperlinks to match results. Hyperlinks make certain elements
|
||||||
can contain the following placeholders: {file}, {line}, {column}, and {host}.
|
of ripgrep's output, such as file paths, clickable. This generally only works
|
||||||
An empty pattern or 'none' disables hyperlinks.
|
in terminal emulators that support OSC-8 hyperlinks. For example, the format
|
||||||
|
*file://{host}{file}* will emit an RFC 8089 hyperlink.
|
||||||
|
|
||||||
The {file} placeholder is required, and will be replaced with the absolute
|
The following variables are available in the format string:
|
||||||
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}'
|
*{path}*: Required. This is replaced with a path to a matching file. The
|
||||||
|
path is guaranteed to be absolute and percent encoded such that it is valid to
|
||||||
|
put into a URI. Note that a path is guaranteed to start with a */*.
|
||||||
|
|
||||||
|
*{host}*: Optional. This is replaced with your system's hostname. On Unix,
|
||||||
|
this corresponds to calling *gethostname*. On Windows, this corresponds to
|
||||||
|
calling *GetComputerNameExW* to fetch the system's \"physical DNS hostname.\"
|
||||||
|
Alternatively, if --hostname-bin was provided, then the hostname returned from
|
||||||
|
the output of that program will be returned. If no hostname could be found,
|
||||||
|
then this variable is replaced with the empty string.
|
||||||
|
|
||||||
|
*{line}*: Optional. If appropriate, this is replaced with the line number of
|
||||||
|
a match. If no line number is available (for example, if --no-line-number was
|
||||||
|
given), then it is automatically replaced with the value *1*.
|
||||||
|
|
||||||
|
*{column}*: Optional, but requires the presence of **{line}**. If appropriate,
|
||||||
|
this is replaced with the column number of a match. If no column number is
|
||||||
|
available (for example, if --no-column was given), then it is automatically
|
||||||
|
replaced with the value *1*.
|
||||||
|
|
||||||
|
*{wslprefix}*: Optional. This is a special value that is set to
|
||||||
|
*wsl$/WSL_DISTRO_NAME*, where *WSL_DISTRO_NAME* corresponds to the value of
|
||||||
|
the equivalent environment variable. If the system is not Unix or if the
|
||||||
|
*WSL_DISTRO_NAME* environment variable is not set, then this is replaced with
|
||||||
|
the empty string.
|
||||||
|
|
||||||
|
Alternatively, a format string may correspond to one of the following
|
||||||
|
aliases: default, file, grep+, kitty, macvim, none, subl, textmate, vscode,
|
||||||
|
vscode-insiders, vscodium.
|
||||||
|
|
||||||
|
A format string may be empty. An empty format string is equivalent to the
|
||||||
|
*none* alias. In this case, hyperlinks will be disabled.
|
||||||
|
|
||||||
|
At present, the default format when ripgrep detects a tty on stdout all systems
|
||||||
|
is *default*. This is an alias that expands to *file://{host}{path}* on Unix
|
||||||
|
and *file://{path}* on Windows. When stdout is not a tty, then the default
|
||||||
|
format behaves as if it were *none*. That is, hyperlinks are disabled.
|
||||||
|
|
||||||
|
Note that hyperlinks are only written when colors are enabled. To write
|
||||||
|
hyperlinks without colors, you'll need to configure ripgrep to not colorize
|
||||||
|
anything without actually disabling all ANSI escape codes completely:
|
||||||
|
|
||||||
|
--colors 'path:none' --colors 'line:none' --colors 'column:none' --colors 'match:none'
|
||||||
|
|
||||||
|
ripgrep works this way because it treats the *--color=(never|always|auto)* flag
|
||||||
|
as a proxy for whether ANSI escape codes should be used at all. This means
|
||||||
|
that environment variables like *NO_COLOR=1* and *TERM=dumb* not only disable
|
||||||
|
colors, but hyperlinks as well. Similarly, colors and hyperlinks are disabled
|
||||||
|
when ripgrep is not writing to a tty. (Unless one forces the issue by setting
|
||||||
|
*--color=always*.)
|
||||||
|
|
||||||
|
For more information on hyperlinks in terminal emulators, see:
|
||||||
|
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
let arg =
|
let arg =
|
||||||
|
@ -18,9 +18,9 @@ use grep::pcre2::{
|
|||||||
RegexMatcherBuilder as PCRE2RegexMatcherBuilder,
|
RegexMatcherBuilder as PCRE2RegexMatcherBuilder,
|
||||||
};
|
};
|
||||||
use grep::printer::{
|
use grep::printer::{
|
||||||
default_color_specs, ColorSpecs, HyperlinkPattern, JSONBuilder,
|
default_color_specs, ColorSpecs, HyperlinkConfig, HyperlinkEnvironment,
|
||||||
PathPrinter, PathPrinterBuilder, Standard, StandardBuilder, Stats,
|
HyperlinkFormat, JSONBuilder, PathPrinter, PathPrinterBuilder, Standard,
|
||||||
Summary, SummaryBuilder, SummaryKind, JSON,
|
StandardBuilder, Stats, Summary, SummaryBuilder, SummaryKind, JSON,
|
||||||
};
|
};
|
||||||
use grep::regex::{
|
use grep::regex::{
|
||||||
RegexMatcher as RustRegexMatcher,
|
RegexMatcher as RustRegexMatcher,
|
||||||
@ -236,7 +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()?)
|
.hyperlink(self.matches().hyperlink_config()?)
|
||||||
.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))
|
||||||
@ -774,7 +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()?)
|
.hyperlink(self.hyperlink_config()?)
|
||||||
.stats(self.stats())
|
.stats(self.stats())
|
||||||
.heading(self.heading())
|
.heading(self.heading())
|
||||||
.path(self.with_filename(paths))
|
.path(self.with_filename(paths))
|
||||||
@ -814,7 +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()?)
|
.hyperlink(self.hyperlink_config()?)
|
||||||
.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()?)
|
||||||
@ -1126,11 +1126,21 @@ impl ArgMatches {
|
|||||||
/// for the current system is used if the value is not set.
|
/// for the current system is used if the value is not set.
|
||||||
///
|
///
|
||||||
/// If an invalid pattern is provided, then an error is returned.
|
/// If an invalid pattern is provided, then an error is returned.
|
||||||
fn hyperlink_pattern(&self) -> Result<HyperlinkPattern> {
|
fn hyperlink_config(&self) -> Result<HyperlinkConfig> {
|
||||||
Ok(match self.value_of_lossy("hyperlink-format") {
|
let mut env = HyperlinkEnvironment::new();
|
||||||
Some(pattern) => HyperlinkPattern::from_str(&pattern)?,
|
env.host(hostname(self.value_of_os("hostname-bin")))
|
||||||
None => HyperlinkPattern::default_file_scheme(),
|
.wsl_prefix(wsl_prefix());
|
||||||
})
|
let fmt = match self.value_of_lossy("hyperlink-format") {
|
||||||
|
None => HyperlinkFormat::from_str("default").unwrap(),
|
||||||
|
Some(format) => match HyperlinkFormat::from_str(&format) {
|
||||||
|
Ok(format) => format,
|
||||||
|
Err(err) => {
|
||||||
|
let msg = format!("invalid hyperlink format: {err}");
|
||||||
|
return Err(msg.into());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(HyperlinkConfig::new(env, fmt))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if ignore files should be processed case insensitively.
|
/// Returns true if ignore files should be processed case insensitively.
|
||||||
@ -1838,6 +1848,107 @@ fn current_dir() -> Result<PathBuf> {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the hostname that ripgrep should use wherever a hostname is
|
||||||
|
/// required. Currently, that's just in the hyperlink format.
|
||||||
|
///
|
||||||
|
/// This works by first running the given binary program (if present and with
|
||||||
|
/// no arguments) to get the hostname after trimming leading and trailing
|
||||||
|
/// whitespace. If that fails for any reason, then it falls back to getting
|
||||||
|
/// the hostname via platform specific means (e.g., `gethostname` on Unix).
|
||||||
|
///
|
||||||
|
/// The purpose of `bin` is to make it possible for end users to override how
|
||||||
|
/// ripgrep determines the hostname.
|
||||||
|
fn hostname(bin: Option<&OsStr>) -> Option<String> {
|
||||||
|
let Some(bin) = bin else { return platform_hostname() };
|
||||||
|
let bin = match grep::cli::resolve_binary(bin) {
|
||||||
|
Ok(bin) => bin,
|
||||||
|
Err(err) => {
|
||||||
|
log::debug!(
|
||||||
|
"failed to run command '{bin:?}' to get hostname \
|
||||||
|
(falling back to platform hostname): {err}",
|
||||||
|
);
|
||||||
|
return platform_hostname();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut cmd = process::Command::new(&bin);
|
||||||
|
cmd.stdin(process::Stdio::null());
|
||||||
|
let rdr = match grep::cli::CommandReader::new(&mut cmd) {
|
||||||
|
Ok(rdr) => rdr,
|
||||||
|
Err(err) => {
|
||||||
|
log::debug!(
|
||||||
|
"failed to spawn command '{bin:?}' to get \
|
||||||
|
hostname (falling back to platform hostname): {err}",
|
||||||
|
);
|
||||||
|
return platform_hostname();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let out = match io::read_to_string(rdr) {
|
||||||
|
Ok(out) => out,
|
||||||
|
Err(err) => {
|
||||||
|
log::debug!(
|
||||||
|
"failed to read output from command '{bin:?}' to get \
|
||||||
|
hostname (falling back to platform hostname): {err}",
|
||||||
|
);
|
||||||
|
return platform_hostname();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let hostname = out.trim();
|
||||||
|
if hostname.is_empty() {
|
||||||
|
log::debug!(
|
||||||
|
"output from command '{bin:?}' is empty after trimming \
|
||||||
|
leading and trailing whitespace (falling back to \
|
||||||
|
platform hostname)",
|
||||||
|
);
|
||||||
|
return platform_hostname();
|
||||||
|
}
|
||||||
|
Some(hostname.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to get the hostname by using platform specific routines. For
|
||||||
|
/// example, this will do `gethostname` on Unix and `GetComputerNameExW` on
|
||||||
|
/// Windows.
|
||||||
|
fn platform_hostname() -> Option<String> {
|
||||||
|
let hostname_os = match grep::cli::hostname() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(err) => {
|
||||||
|
log::debug!("could not get hostname: {}", err);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(hostname) = hostname_os.to_str() else {
|
||||||
|
log::debug!(
|
||||||
|
"got hostname {:?}, but it's not valid UTF-8",
|
||||||
|
hostname_os
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(hostname.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a value that is meant to fill in the `{wslprefix}` variable for
|
||||||
|
/// a user given hyperlink format. A WSL prefix is a share/network like thing
|
||||||
|
/// that is meant to permit Windows applications to open files stored within
|
||||||
|
/// a WSL drive.
|
||||||
|
///
|
||||||
|
/// If a WSL distro name is unavailable, not valid UTF-8 or this isn't running
|
||||||
|
/// in a Unix environment, then this returns None.
|
||||||
|
///
|
||||||
|
/// See: <https://learn.microsoft.com/en-us/windows/wsl/filesystems>
|
||||||
|
fn wsl_prefix() -> Option<String> {
|
||||||
|
if !cfg!(unix) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let distro_os = env::var_os("WSL_DISTRO_NAME")?;
|
||||||
|
let Some(distro) = distro_os.to_str() else {
|
||||||
|
log::debug!(
|
||||||
|
"found WSL_DISTRO_NAME={:?}, but value is not UTF-8",
|
||||||
|
distro_os
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(format!("wsl$/{distro}"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Tries to assign a timestamp to every `Subject` in the vector to help with
|
/// Tries to assign a timestamp to every `Subject` in the vector to help with
|
||||||
/// sorting Subjects by time.
|
/// sorting Subjects by time.
|
||||||
fn load_timestamps<G>(
|
fn load_timestamps<G>(
|
||||||
|
@ -21,9 +21,9 @@ serde = ["dep:base64", "dep:serde", "dep:serde_json"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { version = "0.21.4", optional = true }
|
base64 = { version = "0.21.4", optional = true }
|
||||||
bstr = "1.6.2"
|
bstr = "1.6.2"
|
||||||
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" }
|
||||||
|
log = "0.4.5"
|
||||||
termcolor = "1.3.0"
|
termcolor = "1.3.0"
|
||||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||||
serde_json = { version = "1.0.107", optional = true }
|
serde_json = { version = "1.0.107", optional = true }
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,87 @@
|
|||||||
/// Aliases to well-known hyperlink schemes.
|
/// Aliases to well-known hyperlink schemes.
|
||||||
///
|
///
|
||||||
/// These need to be sorted by name.
|
/// These need to be sorted by name.
|
||||||
pub(crate) const HYPERLINK_PATTERN_ALIASES: &[(&str, &str)] = &[
|
const HYPERLINK_PATTERN_ALIASES: &[(&str, &str)] = &[
|
||||||
#[cfg(unix)]
|
#[cfg(not(windows))]
|
||||||
("file", "file://{host}/{file}"),
|
("default", "file://{host}{path}"),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
("file", "file:///{file}"),
|
("default", "file://{path}"),
|
||||||
|
("file", "file://{host}{path}"),
|
||||||
// https://github.com/misaki-web/grepp
|
// https://github.com/misaki-web/grepp
|
||||||
("grep+", "grep+:///{file}:{line}"),
|
("grep+", "grep+://{path}:{line}"),
|
||||||
("kitty", "file://{host}/{file}#{line}"),
|
("kitty", "file://{host}{path}#{line}"),
|
||||||
// https://macvim.org/docs/gui_mac.txt.html#mvim%3A%2F%2F
|
// https://macvim.org/docs/gui_mac.txt.html#mvim%3A%2F%2F
|
||||||
("macvim", "mvim://open?url=file:///{file}&line={line}&column={column}"),
|
("macvim", "mvim://open?url=file://{path}&line={line}&column={column}"),
|
||||||
("none", ""),
|
("none", ""),
|
||||||
// https://github.com/inopinatus/sublime_url
|
// https://github.com/inopinatus/sublime_url
|
||||||
("subl", "subl://open?url=file:///{file}&line={line}&column={column}"),
|
("subl", "subl://open?url=file://{path}&line={line}&column={column}"),
|
||||||
// https://macromates.com/blog/2007/the-textmate-url-scheme/
|
// https://macromates.com/blog/2007/the-textmate-url-scheme/
|
||||||
("textmate", "txmt://open?url=file:///{file}&line={line}&column={column}"),
|
("textmate", "txmt://open?url=file://{path}&line={line}&column={column}"),
|
||||||
// https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls
|
// https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls
|
||||||
("vscode", "vscode://file/{file}:{line}:{column}"),
|
("vscode", "vscode://file{path}:{line}:{column}"),
|
||||||
("vscode-insiders", "vscode-insiders://file/{file}:{line}:{column}"),
|
("vscode-insiders", "vscode-insiders://file{path}:{line}:{column}"),
|
||||||
("vscodium", "vscodium://file/{file}:{line}:{column}"),
|
("vscodium", "vscodium://file{path}:{line}:{column}"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Look for the hyperlink format defined by the given alias name.
|
||||||
|
///
|
||||||
|
/// If one does not exist, `None` is returned.
|
||||||
|
pub(crate) fn find(name: &str) -> Option<&str> {
|
||||||
|
HYPERLINK_PATTERN_ALIASES
|
||||||
|
.binary_search_by_key(&name, |&(name, _)| name)
|
||||||
|
.map(|i| HYPERLINK_PATTERN_ALIASES[i].1)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return an iterator over all available alias names and their definitions.
|
||||||
|
pub(crate) fn iter() -> impl Iterator<Item = (&'static str, &'static str)> {
|
||||||
|
HYPERLINK_PATTERN_ALIASES.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::HyperlinkFormat;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_sorted() {
|
||||||
|
let mut prev = HYPERLINK_PATTERN_ALIASES
|
||||||
|
.get(0)
|
||||||
|
.expect("aliases should be non-empty")
|
||||||
|
.0;
|
||||||
|
for &(name, _) in HYPERLINK_PATTERN_ALIASES.iter().skip(1) {
|
||||||
|
assert!(
|
||||||
|
name > prev,
|
||||||
|
"'{prev}' should come before '{name}' in \
|
||||||
|
HYPERLINK_PATTERN_ALIASES",
|
||||||
|
);
|
||||||
|
prev = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alias_names_are_reasonable() {
|
||||||
|
for &(name, _) in HYPERLINK_PATTERN_ALIASES.iter() {
|
||||||
|
// There's no hard rule here, but if we want to define an alias
|
||||||
|
// with a name that doesn't pass this assert, then we should
|
||||||
|
// probably flag it as worthy of consideration. For example, we
|
||||||
|
// really do not want to define an alias that contains `{` or `}`,
|
||||||
|
// which might confuse it for a variable.
|
||||||
|
assert!(name.chars().all(|c| c.is_alphanumeric()
|
||||||
|
|| c == '+'
|
||||||
|
|| c == '-'
|
||||||
|
|| c == '.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aliases_are_valid_formats() {
|
||||||
|
for (name, definition) in HYPERLINK_PATTERN_ALIASES {
|
||||||
|
assert!(
|
||||||
|
definition.parse::<HyperlinkFormat>().is_ok(),
|
||||||
|
"invalid hyperlink alias '{name}': {definition}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -60,12 +60,13 @@ assert_eq!(output, expected);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
#![cfg_attr(feature = "pattern", feature(pattern))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
color::{default_color_specs, ColorError, ColorSpecs, UserColorSpec},
|
color::{default_color_specs, ColorError, ColorSpecs, UserColorSpec},
|
||||||
hyperlink::{
|
hyperlink::{
|
||||||
HyperlinkPattern, HyperlinkPatternBuilder, HyperlinkPatternError,
|
HyperlinkConfig, HyperlinkEnvironment, HyperlinkFormat,
|
||||||
|
HyperlinkFormatError,
|
||||||
},
|
},
|
||||||
path::{PathPrinter, PathPrinterBuilder},
|
path::{PathPrinter, PathPrinterBuilder},
|
||||||
standard::{Standard, StandardBuilder, StandardSink},
|
standard::{Standard, StandardBuilder, StandardSink},
|
||||||
|
@ -4,7 +4,7 @@ use termcolor::WriteColor;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
color::ColorSpecs,
|
color::ColorSpecs,
|
||||||
hyperlink::{HyperlinkPattern, HyperlinkSpan},
|
hyperlink::{self, HyperlinkConfig},
|
||||||
util::PrinterPath,
|
util::PrinterPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ use crate::{
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct Config {
|
struct Config {
|
||||||
colors: ColorSpecs,
|
colors: ColorSpecs,
|
||||||
hyperlink_pattern: HyperlinkPattern,
|
hyperlink: HyperlinkConfig,
|
||||||
separator: Option<u8>,
|
separator: Option<u8>,
|
||||||
terminator: u8,
|
terminator: u8,
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ impl Default for Config {
|
|||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
colors: ColorSpecs::default(),
|
colors: ColorSpecs::default(),
|
||||||
hyperlink_pattern: HyperlinkPattern::default(),
|
hyperlink: HyperlinkConfig::default(),
|
||||||
separator: None,
|
separator: None,
|
||||||
terminator: b'\n',
|
terminator: b'\n',
|
||||||
}
|
}
|
||||||
@ -43,7 +43,9 @@ 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, buf: vec![] }
|
let interpolator =
|
||||||
|
hyperlink::Interpolator::new(&self.config.hyperlink);
|
||||||
|
PathPrinter { config: self.config.clone(), wtr, interpolator }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the user color specifications to use for coloring in this printer.
|
/// Set the user color specifications to use for coloring in this printer.
|
||||||
@ -73,7 +75,7 @@ impl PathPrinterBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the hyperlink pattern to use for hyperlinks output by this printer.
|
/// Set the configuration to use for hyperlinks output by this printer.
|
||||||
///
|
///
|
||||||
/// Regardless of the hyperlink format provided here, whether hyperlinks
|
/// Regardless of the hyperlink format provided here, whether hyperlinks
|
||||||
/// are actually used or not is determined by the implementation of
|
/// are actually used or not is determined by the implementation of
|
||||||
@ -83,12 +85,12 @@ impl PathPrinterBuilder {
|
|||||||
///
|
///
|
||||||
/// This completely overrides any previous hyperlink format.
|
/// This completely overrides any previous hyperlink format.
|
||||||
///
|
///
|
||||||
/// The default pattern format results in not emitting any hyperlinks.
|
/// The default configuration results in not emitting any hyperlinks.
|
||||||
pub fn hyperlink_pattern(
|
pub fn hyperlink(
|
||||||
&mut self,
|
&mut self,
|
||||||
pattern: HyperlinkPattern,
|
config: HyperlinkConfig,
|
||||||
) -> &mut PathPrinterBuilder {
|
) -> &mut PathPrinterBuilder {
|
||||||
self.config.hyperlink_pattern = pattern;
|
self.config.hyperlink = config;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,40 +142,35 @@ impl PathPrinterBuilder {
|
|||||||
pub struct PathPrinter<W> {
|
pub struct PathPrinter<W> {
|
||||||
config: Config,
|
config: Config,
|
||||||
wtr: W,
|
wtr: W,
|
||||||
buf: Vec<u8>,
|
interpolator: hyperlink::Interpolator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: WriteColor> PathPrinter<W> {
|
impl<W: WriteColor> PathPrinter<W> {
|
||||||
/// Write the given path to the underlying writer.
|
/// Write the given path to the underlying writer.
|
||||||
pub fn write(&mut self, path: &Path) -> io::Result<()> {
|
pub fn write(&mut self, path: &Path) -> io::Result<()> {
|
||||||
let ppath = PrinterPath::with_separator(path, self.config.separator);
|
let ppath = PrinterPath::new(path.as_ref())
|
||||||
|
.with_separator(self.config.separator);
|
||||||
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)?;
|
let status = self.start_hyperlink(&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.interpolator.finish(status, &mut self.wtr)?;
|
||||||
}
|
}
|
||||||
self.wtr.write_all(&[self.config.terminator])
|
self.wtr.write_all(&[self.config.terminator])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a hyperlink span when applicable.
|
/// Starts a hyperlink span when applicable.
|
||||||
fn start_hyperlink_span(
|
fn start_hyperlink(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: &PrinterPath,
|
path: &PrinterPath,
|
||||||
) -> io::Result<HyperlinkSpan> {
|
) -> io::Result<hyperlink::InterpolatorStatus> {
|
||||||
if self.wtr.supports_hyperlinks() {
|
let Some(hyperpath) = path.as_hyperlink() else {
|
||||||
if let Some(spec) = path.create_hyperlink_spec(
|
return Ok(hyperlink::InterpolatorStatus::inactive());
|
||||||
&self.config.hyperlink_pattern,
|
};
|
||||||
None,
|
let values = hyperlink::Values::new(hyperpath);
|
||||||
None,
|
self.interpolator.begin(&values, &mut self.wtr)
|
||||||
&mut self.buf,
|
|
||||||
) {
|
|
||||||
return Ok(HyperlinkSpan::start(&mut self.wtr, &spec)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(HyperlinkSpan::default())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ use {
|
|||||||
use crate::{
|
use crate::{
|
||||||
color::ColorSpecs,
|
color::ColorSpecs,
|
||||||
counter::CounterWriter,
|
counter::CounterWriter,
|
||||||
hyperlink::{HyperlinkPattern, HyperlinkSpan},
|
hyperlink::{self, HyperlinkConfig},
|
||||||
stats::Stats,
|
stats::Stats,
|
||||||
util::{
|
util::{
|
||||||
find_iter_at_in_context, trim_ascii_prefix, trim_line_terminator,
|
find_iter_at_in_context, trim_ascii_prefix, trim_line_terminator,
|
||||||
@ -36,7 +36,7 @@ use crate::{
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Config {
|
struct Config {
|
||||||
colors: ColorSpecs,
|
colors: ColorSpecs,
|
||||||
hyperlink_pattern: HyperlinkPattern,
|
hyperlink: HyperlinkConfig,
|
||||||
stats: bool,
|
stats: bool,
|
||||||
heading: bool,
|
heading: bool,
|
||||||
path: bool,
|
path: bool,
|
||||||
@ -62,7 +62,7 @@ impl Default for Config {
|
|||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
colors: ColorSpecs::default(),
|
colors: ColorSpecs::default(),
|
||||||
hyperlink_pattern: HyperlinkPattern::default(),
|
hyperlink: HyperlinkConfig::default(),
|
||||||
stats: false,
|
stats: false,
|
||||||
heading: false,
|
heading: false,
|
||||||
path: true,
|
path: true,
|
||||||
@ -131,7 +131,6 @@ 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![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,7 +169,7 @@ impl StandardBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the hyperlink pattern to use for hyperlinks output by this printer.
|
/// Set the configuration to use for hyperlinks output by this printer.
|
||||||
///
|
///
|
||||||
/// Regardless of the hyperlink format provided here, whether hyperlinks
|
/// Regardless of the hyperlink format provided here, whether hyperlinks
|
||||||
/// are actually used or not is determined by the implementation of
|
/// are actually used or not is determined by the implementation of
|
||||||
@ -180,12 +179,12 @@ impl StandardBuilder {
|
|||||||
///
|
///
|
||||||
/// This completely overrides any previous hyperlink format.
|
/// This completely overrides any previous hyperlink format.
|
||||||
///
|
///
|
||||||
/// The default pattern format results in not emitting any hyperlinks.
|
/// The default configuration results in not emitting any hyperlinks.
|
||||||
pub fn hyperlink_pattern(
|
pub fn hyperlink(
|
||||||
&mut self,
|
&mut self,
|
||||||
pattern: HyperlinkPattern,
|
config: HyperlinkConfig,
|
||||||
) -> &mut StandardBuilder {
|
) -> &mut StandardBuilder {
|
||||||
self.config.hyperlink_pattern = pattern;
|
self.config.hyperlink = config;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,7 +495,6 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,12 +531,15 @@ impl<W: WriteColor> Standard<W> {
|
|||||||
&'s mut self,
|
&'s mut self,
|
||||||
matcher: M,
|
matcher: M,
|
||||||
) -> StandardSink<'static, 's, M, W> {
|
) -> StandardSink<'static, 's, M, W> {
|
||||||
|
let interpolator =
|
||||||
|
hyperlink::Interpolator::new(&self.config.hyperlink);
|
||||||
let stats = if self.config.stats { Some(Stats::new()) } else { None };
|
let stats = if self.config.stats { Some(Stats::new()) } else { None };
|
||||||
let needs_match_granularity = self.needs_match_granularity();
|
let needs_match_granularity = self.needs_match_granularity();
|
||||||
StandardSink {
|
StandardSink {
|
||||||
matcher,
|
matcher,
|
||||||
standard: self,
|
standard: self,
|
||||||
replacer: Replacer::new(),
|
replacer: Replacer::new(),
|
||||||
|
interpolator,
|
||||||
path: None,
|
path: None,
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
match_count: 0,
|
match_count: 0,
|
||||||
@ -565,16 +566,17 @@ impl<W: WriteColor> Standard<W> {
|
|||||||
if !self.config.path {
|
if !self.config.path {
|
||||||
return self.sink(matcher);
|
return self.sink(matcher);
|
||||||
}
|
}
|
||||||
|
let interpolator =
|
||||||
|
hyperlink::Interpolator::new(&self.config.hyperlink);
|
||||||
let stats = if self.config.stats { Some(Stats::new()) } else { None };
|
let stats = if self.config.stats { Some(Stats::new()) } else { None };
|
||||||
let ppath = PrinterPath::with_separator(
|
let ppath = PrinterPath::new(path.as_ref())
|
||||||
path.as_ref(),
|
.with_separator(self.config.separator_path);
|
||||||
self.config.separator_path,
|
|
||||||
);
|
|
||||||
let needs_match_granularity = self.needs_match_granularity();
|
let needs_match_granularity = self.needs_match_granularity();
|
||||||
StandardSink {
|
StandardSink {
|
||||||
matcher,
|
matcher,
|
||||||
standard: self,
|
standard: self,
|
||||||
replacer: Replacer::new(),
|
replacer: Replacer::new(),
|
||||||
|
interpolator,
|
||||||
path: Some(ppath),
|
path: Some(ppath),
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
match_count: 0,
|
match_count: 0,
|
||||||
@ -659,6 +661,7 @@ pub struct StandardSink<'p, 's, M: Matcher, W> {
|
|||||||
matcher: M,
|
matcher: M,
|
||||||
standard: &'s mut Standard<W>,
|
standard: &'s mut Standard<W>,
|
||||||
replacer: Replacer<M>,
|
replacer: Replacer<M>,
|
||||||
|
interpolator: hyperlink::Interpolator,
|
||||||
path: Option<PrinterPath<'p>>,
|
path: Option<PrinterPath<'p>>,
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
match_count: u64,
|
match_count: u64,
|
||||||
@ -1241,22 +1244,10 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut prelude = PreludeWriter::new(self);
|
let mut prelude = PreludeWriter::new(self);
|
||||||
prelude.start(line_number, column)?;
|
prelude.start(line_number, column)?;
|
||||||
|
|
||||||
if !self.config().heading {
|
|
||||||
prelude.write_path()?;
|
prelude.write_path()?;
|
||||||
}
|
prelude.write_line_number(line_number)?;
|
||||||
if let Some(n) = line_number {
|
prelude.write_column_number(column)?;
|
||||||
prelude.write_line_number(n)?;
|
|
||||||
}
|
|
||||||
if let Some(n) = column {
|
|
||||||
if self.config().column {
|
|
||||||
prelude.write_column_number(n)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.config().byte_offset {
|
|
||||||
prelude.write_byte_offset(absolute_byte_offset)?;
|
prelude.write_byte_offset(absolute_byte_offset)?;
|
||||||
}
|
|
||||||
|
|
||||||
prelude.end()
|
prelude.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1507,30 +1498,30 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_path_hyperlink(&self, path: &PrinterPath) -> io::Result<()> {
|
fn write_path_hyperlink(&self, path: &PrinterPath) -> io::Result<()> {
|
||||||
let mut hyperlink = self.start_hyperlink_span(path, None, None)?;
|
let status = self.start_hyperlink(path, None, None)?;
|
||||||
self.write_path(path)?;
|
self.write_path(path)?;
|
||||||
hyperlink.end(&mut *self.wtr().borrow_mut())
|
self.end_hyperlink(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_hyperlink_span(
|
fn start_hyperlink(
|
||||||
&self,
|
&self,
|
||||||
path: &PrinterPath,
|
path: &PrinterPath,
|
||||||
line_number: Option<u64>,
|
line_number: Option<u64>,
|
||||||
column: Option<u64>,
|
column: Option<u64>,
|
||||||
) -> io::Result<HyperlinkSpan> {
|
) -> io::Result<hyperlink::InterpolatorStatus> {
|
||||||
let mut wtr = self.wtr().borrow_mut();
|
let Some(hyperpath) = path.as_hyperlink() else {
|
||||||
if wtr.supports_hyperlinks() {
|
return Ok(hyperlink::InterpolatorStatus::inactive());
|
||||||
let mut buf = self.buf().borrow_mut();
|
};
|
||||||
if let Some(spec) = path.create_hyperlink_spec(
|
let values =
|
||||||
&self.config().hyperlink_pattern,
|
hyperlink::Values::new(hyperpath).line(line_number).column(column);
|
||||||
line_number,
|
self.sink.interpolator.begin(&values, &mut *self.wtr().borrow_mut())
|
||||||
column,
|
|
||||||
&mut buf,
|
|
||||||
) {
|
|
||||||
return HyperlinkSpan::start(&mut *wtr, &spec);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(HyperlinkSpan::default())
|
fn end_hyperlink(
|
||||||
|
&self,
|
||||||
|
status: hyperlink::InterpolatorStatus,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
self.sink.interpolator.finish(status, &mut *self.wtr().borrow_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_color_match(&self) -> io::Result<()> {
|
fn start_color_match(&self) -> io::Result<()> {
|
||||||
@ -1586,12 +1577,6 @@ 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()
|
||||||
@ -1645,7 +1630,7 @@ struct PreludeWriter<'a, M: Matcher, W> {
|
|||||||
std: &'a StandardImpl<'a, M, W>,
|
std: &'a StandardImpl<'a, M, W>,
|
||||||
next_separator: PreludeSeparator,
|
next_separator: PreludeSeparator,
|
||||||
field_separator: &'a [u8],
|
field_separator: &'a [u8],
|
||||||
hyperlink: HyperlinkSpan,
|
interp_status: hyperlink::InterpolatorStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A type of separator used in the prelude
|
/// A type of separator used in the prelude
|
||||||
@ -1660,45 +1645,45 @@ enum PreludeSeparator {
|
|||||||
|
|
||||||
impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
||||||
/// Creates a new prelude printer.
|
/// Creates a new prelude printer.
|
||||||
|
#[inline(always)]
|
||||||
fn new(std: &'a StandardImpl<'a, M, W>) -> PreludeWriter<'a, M, W> {
|
fn new(std: &'a StandardImpl<'a, M, W>) -> PreludeWriter<'a, M, W> {
|
||||||
Self {
|
PreludeWriter {
|
||||||
std,
|
std,
|
||||||
next_separator: PreludeSeparator::None,
|
next_separator: PreludeSeparator::None,
|
||||||
field_separator: std.separator_field(),
|
field_separator: std.separator_field(),
|
||||||
hyperlink: HyperlinkSpan::default(),
|
interp_status: hyperlink::InterpolatorStatus::inactive(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the prelude with a hyperlink when applicable.
|
/// Starts the prelude with a hyperlink when applicable.
|
||||||
///
|
///
|
||||||
/// If a heading was written, and the hyperlink pattern is invariant on
|
/// If a heading was written, and the hyperlink format is invariant on
|
||||||
/// the line number, then this doesn't hyperlink each line prelude, as it
|
/// 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
|
/// wouldn't point to the line anyway. The hyperlink on the heading should
|
||||||
/// be sufficient and less confusing.
|
/// be sufficient and less confusing.
|
||||||
|
#[inline(always)]
|
||||||
fn start(
|
fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
line_number: Option<u64>,
|
line_number: Option<u64>,
|
||||||
column: Option<u64>,
|
column: Option<u64>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
if let Some(path) = self.std.path() {
|
let Some(path) = self.std.path() else { return Ok(()) };
|
||||||
if self.config().hyperlink_pattern.is_line_dependent()
|
if self.config().hyperlink.format().is_line_dependent()
|
||||||
|| !self.config().heading
|
|| !self.config().heading
|
||||||
{
|
{
|
||||||
self.hyperlink = self.std.start_hyperlink_span(
|
self.interp_status =
|
||||||
path,
|
self.std.start_hyperlink(path, line_number, column)?;
|
||||||
line_number,
|
|
||||||
column,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ends the prelude and writes the remaining output.
|
/// Ends the prelude and writes the remaining output.
|
||||||
|
#[inline(always)]
|
||||||
fn end(&mut self) -> io::Result<()> {
|
fn end(&mut self) -> io::Result<()> {
|
||||||
if self.hyperlink.is_active() {
|
self.std.end_hyperlink(std::mem::replace(
|
||||||
self.hyperlink.end(&mut *self.std.wtr().borrow_mut())?;
|
&mut self.interp_status,
|
||||||
}
|
hyperlink::InterpolatorStatus::inactive(),
|
||||||
|
))?;
|
||||||
self.write_separator()
|
self.write_separator()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1706,8 +1691,15 @@ impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
|||||||
/// write that path to the underlying writer followed by the given field
|
/// 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
|
/// separator. (If a path terminator is set, then that is used instead of
|
||||||
/// the field separator.)
|
/// the field separator.)
|
||||||
|
#[inline(always)]
|
||||||
fn write_path(&mut self) -> io::Result<()> {
|
fn write_path(&mut self) -> io::Result<()> {
|
||||||
if let Some(path) = self.std.path() {
|
// The prelude doesn't handle headings, only what comes before a match
|
||||||
|
// on the same line. So if we are emitting paths in headings, we should
|
||||||
|
// not do it here on each line.
|
||||||
|
if self.config().heading {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Some(path) = self.std.path() else { return Ok(()) };
|
||||||
self.write_separator()?;
|
self.write_separator()?;
|
||||||
self.std.write_path(path)?;
|
self.std.write_path(path)?;
|
||||||
|
|
||||||
@ -1716,12 +1708,13 @@ impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
|||||||
} else {
|
} else {
|
||||||
PreludeSeparator::FieldSeparator
|
PreludeSeparator::FieldSeparator
|
||||||
};
|
};
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the line number field.
|
/// Writes the line number field if present.
|
||||||
fn write_line_number(&mut self, line_number: u64) -> io::Result<()> {
|
#[inline(always)]
|
||||||
|
fn write_line_number(&mut self, line: Option<u64>) -> io::Result<()> {
|
||||||
|
let Some(line_number) = line else { return Ok(()) };
|
||||||
self.write_separator()?;
|
self.write_separator()?;
|
||||||
let n = line_number.to_string();
|
let n = line_number.to_string();
|
||||||
self.std.write_spec(self.config().colors.line(), n.as_bytes())?;
|
self.std.write_spec(self.config().colors.line(), n.as_bytes())?;
|
||||||
@ -1729,8 +1722,13 @@ impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the column number field.
|
/// Writes the column number field if present and configured to do so.
|
||||||
fn write_column_number(&mut self, column_number: u64) -> io::Result<()> {
|
#[inline(always)]
|
||||||
|
fn write_column_number(&mut self, column: Option<u64>) -> io::Result<()> {
|
||||||
|
if !self.config().column {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Some(column_number) = column else { return Ok(()) };
|
||||||
self.write_separator()?;
|
self.write_separator()?;
|
||||||
let n = column_number.to_string();
|
let n = column_number.to_string();
|
||||||
self.std.write_spec(self.config().colors.column(), n.as_bytes())?;
|
self.std.write_spec(self.config().colors.column(), n.as_bytes())?;
|
||||||
@ -1738,8 +1736,12 @@ impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the byte offset field.
|
/// Writes the byte offset field if configured to do so.
|
||||||
|
#[inline(always)]
|
||||||
fn write_byte_offset(&mut self, offset: u64) -> io::Result<()> {
|
fn write_byte_offset(&mut self, offset: u64) -> io::Result<()> {
|
||||||
|
if !self.config().byte_offset {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
self.write_separator()?;
|
self.write_separator()?;
|
||||||
let n = offset.to_string();
|
let n = offset.to_string();
|
||||||
self.std.write_spec(self.config().colors.column(), n.as_bytes())?;
|
self.std.write_spec(self.config().colors.column(), n.as_bytes())?;
|
||||||
@ -1751,6 +1753,7 @@ impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
|||||||
///
|
///
|
||||||
/// This is called before writing the contents of a field, and at
|
/// This is called before writing the contents of a field, and at
|
||||||
/// the end of the prelude.
|
/// the end of the prelude.
|
||||||
|
#[inline(always)]
|
||||||
fn write_separator(&mut self) -> io::Result<()> {
|
fn write_separator(&mut self) -> io::Result<()> {
|
||||||
match self.next_separator {
|
match self.next_separator {
|
||||||
PreludeSeparator::None => {}
|
PreludeSeparator::None => {}
|
||||||
@ -1767,6 +1770,7 @@ impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn config(&self) -> &Config {
|
fn config(&self) -> &Config {
|
||||||
self.std.config()
|
self.std.config()
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ use {
|
|||||||
use crate::{
|
use crate::{
|
||||||
color::ColorSpecs,
|
color::ColorSpecs,
|
||||||
counter::CounterWriter,
|
counter::CounterWriter,
|
||||||
hyperlink::{HyperlinkPattern, HyperlinkSpan},
|
hyperlink::{self, HyperlinkConfig},
|
||||||
stats::Stats,
|
stats::Stats,
|
||||||
util::{find_iter_at_in_context, PrinterPath},
|
util::{find_iter_at_in_context, PrinterPath},
|
||||||
};
|
};
|
||||||
@ -29,7 +29,7 @@ use crate::{
|
|||||||
struct Config {
|
struct Config {
|
||||||
kind: SummaryKind,
|
kind: SummaryKind,
|
||||||
colors: ColorSpecs,
|
colors: ColorSpecs,
|
||||||
hyperlink_pattern: HyperlinkPattern,
|
hyperlink: HyperlinkConfig,
|
||||||
stats: bool,
|
stats: bool,
|
||||||
path: bool,
|
path: bool,
|
||||||
max_matches: Option<u64>,
|
max_matches: Option<u64>,
|
||||||
@ -44,7 +44,7 @@ impl Default for Config {
|
|||||||
Config {
|
Config {
|
||||||
kind: SummaryKind::Count,
|
kind: SummaryKind::Count,
|
||||||
colors: ColorSpecs::default(),
|
colors: ColorSpecs::default(),
|
||||||
hyperlink_pattern: HyperlinkPattern::default(),
|
hyperlink: HyperlinkConfig::default(),
|
||||||
stats: false,
|
stats: false,
|
||||||
path: true,
|
path: true,
|
||||||
max_matches: None,
|
max_matches: None,
|
||||||
@ -169,7 +169,6 @@ 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![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +215,7 @@ impl SummaryBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the hyperlink pattern to use for hyperlinks output by this printer.
|
/// Set the configuration to use for hyperlinks output by this printer.
|
||||||
///
|
///
|
||||||
/// Regardless of the hyperlink format provided here, whether hyperlinks
|
/// Regardless of the hyperlink format provided here, whether hyperlinks
|
||||||
/// are actually used or not is determined by the implementation of
|
/// are actually used or not is determined by the implementation of
|
||||||
@ -226,12 +225,12 @@ impl SummaryBuilder {
|
|||||||
///
|
///
|
||||||
/// This completely overrides any previous hyperlink format.
|
/// This completely overrides any previous hyperlink format.
|
||||||
///
|
///
|
||||||
/// The default pattern format results in not emitting any hyperlinks.
|
/// The default configuration results in not emitting any hyperlinks.
|
||||||
pub fn hyperlink_pattern(
|
pub fn hyperlink(
|
||||||
&mut self,
|
&mut self,
|
||||||
pattern: HyperlinkPattern,
|
config: HyperlinkConfig,
|
||||||
) -> &mut SummaryBuilder {
|
) -> &mut SummaryBuilder {
|
||||||
self.config.hyperlink_pattern = pattern;
|
self.config.hyperlink = config;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,7 +356,6 @@ 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> {
|
||||||
@ -400,6 +398,8 @@ impl<W: WriteColor> Summary<W> {
|
|||||||
&'s mut self,
|
&'s mut self,
|
||||||
matcher: M,
|
matcher: M,
|
||||||
) -> SummarySink<'static, 's, M, W> {
|
) -> SummarySink<'static, 's, M, W> {
|
||||||
|
let interpolator =
|
||||||
|
hyperlink::Interpolator::new(&self.config.hyperlink);
|
||||||
let stats = if self.config.stats || self.config.kind.requires_stats() {
|
let stats = if self.config.stats || self.config.kind.requires_stats() {
|
||||||
Some(Stats::new())
|
Some(Stats::new())
|
||||||
} else {
|
} else {
|
||||||
@ -408,6 +408,7 @@ impl<W: WriteColor> Summary<W> {
|
|||||||
SummarySink {
|
SummarySink {
|
||||||
matcher,
|
matcher,
|
||||||
summary: self,
|
summary: self,
|
||||||
|
interpolator,
|
||||||
path: None,
|
path: None,
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
match_count: 0,
|
match_count: 0,
|
||||||
@ -432,18 +433,19 @@ impl<W: WriteColor> Summary<W> {
|
|||||||
if !self.config.path && !self.config.kind.requires_path() {
|
if !self.config.path && !self.config.kind.requires_path() {
|
||||||
return self.sink(matcher);
|
return self.sink(matcher);
|
||||||
}
|
}
|
||||||
|
let interpolator =
|
||||||
|
hyperlink::Interpolator::new(&self.config.hyperlink);
|
||||||
let stats = if self.config.stats || self.config.kind.requires_stats() {
|
let stats = if self.config.stats || self.config.kind.requires_stats() {
|
||||||
Some(Stats::new())
|
Some(Stats::new())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let ppath = PrinterPath::with_separator(
|
let ppath = PrinterPath::new(path.as_ref())
|
||||||
path.as_ref(),
|
.with_separator(self.config.separator_path);
|
||||||
self.config.separator_path,
|
|
||||||
);
|
|
||||||
SummarySink {
|
SummarySink {
|
||||||
matcher,
|
matcher,
|
||||||
summary: self,
|
summary: self,
|
||||||
|
interpolator,
|
||||||
path: Some(ppath),
|
path: Some(ppath),
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
match_count: 0,
|
match_count: 0,
|
||||||
@ -490,6 +492,7 @@ impl<W> Summary<W> {
|
|||||||
pub struct SummarySink<'p, 's, M: Matcher, W> {
|
pub struct SummarySink<'p, 's, M: Matcher, W> {
|
||||||
matcher: M,
|
matcher: M,
|
||||||
summary: &'s mut Summary<W>,
|
summary: &'s mut Summary<W>,
|
||||||
|
interpolator: hyperlink::Interpolator,
|
||||||
path: Option<PrinterPath<'p>>,
|
path: Option<PrinterPath<'p>>,
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
match_count: u64,
|
match_count: u64,
|
||||||
@ -595,36 +598,34 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
|
|||||||
/// (color and hyperlink).
|
/// (color and hyperlink).
|
||||||
fn write_path(&mut self) -> io::Result<()> {
|
fn write_path(&mut self) -> io::Result<()> {
|
||||||
if self.path.is_some() {
|
if self.path.is_some() {
|
||||||
let mut hyperlink = self.start_hyperlink_span()?;
|
let status = self.start_hyperlink()?;
|
||||||
|
|
||||||
self.write_spec(
|
self.write_spec(
|
||||||
self.summary.config.colors.path(),
|
self.summary.config.colors.path(),
|
||||||
self.path.as_ref().unwrap().as_bytes(),
|
self.path.as_ref().unwrap().as_bytes(),
|
||||||
)?;
|
)?;
|
||||||
|
self.end_hyperlink(status)?;
|
||||||
if hyperlink.is_active() {
|
|
||||||
hyperlink.end(&mut *self.summary.wtr.borrow_mut())?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a hyperlink span when applicable.
|
/// Starts a hyperlink span when applicable.
|
||||||
fn start_hyperlink_span(&mut self) -> io::Result<HyperlinkSpan> {
|
fn start_hyperlink(
|
||||||
if let Some(ref path) = self.path {
|
&mut self,
|
||||||
let mut wtr = self.summary.wtr.borrow_mut();
|
) -> io::Result<hyperlink::InterpolatorStatus> {
|
||||||
if wtr.supports_hyperlinks() {
|
let Some(hyperpath) =
|
||||||
if let Some(spec) = path.create_hyperlink_spec(
|
self.path.as_ref().and_then(|p| p.as_hyperlink())
|
||||||
&self.summary.config.hyperlink_pattern,
|
else {
|
||||||
None,
|
return Ok(hyperlink::InterpolatorStatus::inactive());
|
||||||
None,
|
};
|
||||||
&mut self.summary.buf,
|
let values = hyperlink::Values::new(hyperpath);
|
||||||
) {
|
self.interpolator.begin(&values, &mut *self.summary.wtr.borrow_mut())
|
||||||
return Ok(HyperlinkSpan::start(&mut *wtr, &spec)?);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
fn end_hyperlink(
|
||||||
Ok(HyperlinkSpan::default())
|
&self,
|
||||||
|
status: hyperlink::InterpolatorStatus,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
self.interpolator.finish(status, &mut *self.summary.wtr.borrow_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the line terminator configured on the given searcher.
|
/// Write the line terminator configured on the given searcher.
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
use std::{borrow::Cow, fmt, io, path::Path, time};
|
use std::{borrow::Cow, cell::OnceCell, fmt, io, path::Path, time};
|
||||||
|
|
||||||
use {
|
use {
|
||||||
bstr::{ByteSlice, ByteVec},
|
bstr::ByteVec,
|
||||||
grep_matcher::{Captures, LineTerminator, Match, Matcher},
|
grep_matcher::{Captures, LineTerminator, Match, Matcher},
|
||||||
grep_searcher::{
|
grep_searcher::{
|
||||||
LineIter, Searcher, SinkContext, SinkContextKind, SinkError, SinkMatch,
|
LineIter, Searcher, SinkContext, SinkContextKind, SinkError, SinkMatch,
|
||||||
},
|
},
|
||||||
termcolor::HyperlinkSpec,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{hyperlink::HyperlinkPath, MAX_LOOK_AHEAD};
|
||||||
hyperlink::{HyperlinkPath, HyperlinkPattern, HyperlinkValues},
|
|
||||||
MAX_LOOK_AHEAD,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A type for handling replacements while amortizing allocation.
|
/// A type for handling replacements while amortizing allocation.
|
||||||
pub(crate) struct Replacer<M: Matcher> {
|
pub(crate) struct Replacer<M: Matcher> {
|
||||||
@ -268,11 +264,12 @@ impl<'a> Sunk<'a> {
|
|||||||
/// something else. This allows us to amortize work if we are printing the
|
/// something else. This allows us to amortize work if we are printing the
|
||||||
/// file path for every match.
|
/// file path for every match.
|
||||||
///
|
///
|
||||||
/// In the common case, no transformation is needed, which lets us avoid the
|
/// In the common case, no transformation is needed, which lets us avoid
|
||||||
/// allocation. Typically, only Windows requires a transform, since we can't
|
/// the allocation. Typically, only Windows requires a transform, since
|
||||||
/// access the raw bytes of a path directly and first need to lossily convert
|
/// it's fraught to access the raw bytes of a path directly and first need
|
||||||
/// to UTF-8. Windows is also typically where the path separator replacement
|
/// to lossily convert to UTF-8. Windows is also typically where the path
|
||||||
/// is used, e.g., in cygwin environments to use `/` instead of `\`.
|
/// separator replacement is used, e.g., in cygwin environments to use `/`
|
||||||
|
/// instead of `\`.
|
||||||
///
|
///
|
||||||
/// Users of this type are expected to construct it from a normal `Path`
|
/// Users of this type are expected to construct it from a normal `Path`
|
||||||
/// found in the standard library. It can then be written to any `io::Write`
|
/// found in the standard library. It can then be written to any `io::Write`
|
||||||
@ -281,54 +278,55 @@ impl<'a> Sunk<'a> {
|
|||||||
/// will not roundtrip correctly.
|
/// will not roundtrip correctly.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct PrinterPath<'a> {
|
pub(crate) struct PrinterPath<'a> {
|
||||||
|
// On Unix, we can re-materialize a `Path` from our `Cow<'a, [u8]>` with
|
||||||
|
// zero cost, so there's no point in storing it. At time of writing,
|
||||||
|
// OsStr::as_os_str_bytes (and its corresponding constructor) are not
|
||||||
|
// stable yet. Those would let us achieve the same end portably. (As long
|
||||||
|
// as we keep our UTF-8 requirement on Windows.)
|
||||||
|
#[cfg(not(unix))]
|
||||||
path: &'a Path,
|
path: &'a Path,
|
||||||
bytes: Cow<'a, [u8]>,
|
bytes: Cow<'a, [u8]>,
|
||||||
hyperlink_path: std::cell::OnceCell<Option<HyperlinkPath>>,
|
hyperlink: 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(crate) fn new(path: &'a Path) -> PrinterPath<'a> {
|
pub(crate) fn new(path: &'a Path) -> PrinterPath<'a> {
|
||||||
PrinterPath {
|
PrinterPath {
|
||||||
|
#[cfg(not(unix))]
|
||||||
path,
|
path,
|
||||||
|
// N.B. This is zero-cost on Unix and requires at least a UTF-8
|
||||||
|
// check on Windows. This doesn't allocate on Windows unless the
|
||||||
|
// path is invalid UTF-8 (which is exceptionally rare).
|
||||||
bytes: Vec::from_path_lossy(path),
|
bytes: Vec::from_path_lossy(path),
|
||||||
hyperlink_path: std::cell::OnceCell::new(),
|
hyperlink: OnceCell::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new printer path from the given path which can be efficiently
|
/// Set the separator on this path.
|
||||||
/// written to a writer without allocation.
|
|
||||||
///
|
///
|
||||||
/// If the given separator is present, then any separators in `path` are
|
/// When set, `PrinterPath::as_bytes` will return the path provided but
|
||||||
/// replaced with it.
|
/// with its separator replaced with the one given.
|
||||||
pub(crate) fn with_separator(
|
pub(crate) fn with_separator(
|
||||||
path: &'a Path,
|
mut self,
|
||||||
sep: Option<u8>,
|
sep: Option<u8>,
|
||||||
) -> PrinterPath<'a> {
|
) -> PrinterPath<'a> {
|
||||||
let mut ppath = PrinterPath::new(path);
|
|
||||||
if let Some(sep) = sep {
|
|
||||||
ppath.replace_separator(sep);
|
|
||||||
}
|
|
||||||
ppath
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the path separator in this path with the given separator
|
/// Replace the path separator in this path with the given separator
|
||||||
/// and do it in place. On Windows, both `/` and `\` are treated as
|
/// and do it in place. On Windows, both `/` and `\` are treated as
|
||||||
/// path separators that are both replaced by `new_sep`. In all other
|
/// path separators that are both replaced by `new_sep`. In all other
|
||||||
/// 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(bytes: &[u8], sep: u8) -> Vec<u8> {
|
||||||
let transformed_path: Vec<u8> = self
|
let mut bytes = bytes.to_vec();
|
||||||
.as_bytes()
|
for b in bytes.iter_mut() {
|
||||||
.bytes()
|
if *b == b'/' || (cfg!(windows) && *b == b'\\') {
|
||||||
.map(|b| {
|
*b = sep;
|
||||||
if b == b'/' || (cfg!(windows) && b == b'\\') {
|
|
||||||
new_sep
|
|
||||||
} else {
|
|
||||||
b
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.collect();
|
bytes
|
||||||
self.bytes = Cow::Owned(transformed_path);
|
}
|
||||||
|
let Some(sep) = sep else { return self };
|
||||||
|
self.bytes = Cow::Owned(replace_separator(self.as_bytes(), sep));
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the raw bytes for this path.
|
/// Return the raw bytes for this path.
|
||||||
@ -336,32 +334,30 @@ impl<'a> PrinterPath<'a> {
|
|||||||
&self.bytes
|
&self.bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a hyperlink for this path and the given line and column, using
|
/// Return this path as a hyperlink.
|
||||||
/// the specified pattern. Uses the given buffer to store the hyperlink.
|
///
|
||||||
pub(crate) fn create_hyperlink_spec<'b>(
|
/// Note that a hyperlink may not be able to be created from a path.
|
||||||
&self,
|
/// Namely, computing the hyperlink may require touching the file system
|
||||||
pattern: &HyperlinkPattern,
|
/// (e.g., for path canonicalization) and that can fail. This failure is
|
||||||
line_number: Option<u64>,
|
/// silent but is logged.
|
||||||
column: Option<u64>,
|
pub(crate) fn as_hyperlink(&self) -> Option<&HyperlinkPath> {
|
||||||
buffer: &'b mut Vec<u8>,
|
self.hyperlink
|
||||||
) -> Option<HyperlinkSpec<'b>> {
|
.get_or_init(|| HyperlinkPath::from_path(self.as_path()))
|
||||||
if pattern.is_empty() {
|
.as_ref()
|
||||||
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.
|
/// Return this path as an actual `Path` type.
|
||||||
///
|
fn as_path(&self) -> &Path {
|
||||||
/// This is what the {file} placeholder will be substituted with.
|
#[cfg(unix)]
|
||||||
fn hyperlink_path(&self) -> Option<&HyperlinkPath> {
|
fn imp<'p>(p: &'p PrinterPath<'_>) -> &'p Path {
|
||||||
self.hyperlink_path
|
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
|
||||||
.get_or_init(|| HyperlinkPath::from_path(self.path))
|
Path::new(OsStr::from_bytes(p.as_bytes()))
|
||||||
.as_ref()
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn imp<'p>(p: &'p PrinterPath<'_>) -> &'p Path {
|
||||||
|
p.path
|
||||||
|
}
|
||||||
|
imp(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user