mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-11-23 21:54:45 +02:00
printer: deduplicate hyperlink alias names
This exports a new `HyperlinkAlias` type in the `grep-printer` crate. This includes a "display priority" with each alias and a function for getting all supported aliases from the crate. This should hopefully make it possible for downstream users of this crate to include a list of supported aliases in the documentation. Closes #3103
This commit is contained in:
committed by
Andrew Gallant
parent
fdfda9ae73
commit
66aa4a63bb
@@ -17,7 +17,7 @@ ripgrep. For example, `-E`, `--encoding` and `--no-encoding` all manipulate the
|
|||||||
same encoding state in ripgrep.
|
same encoding state in ripgrep.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::LazyLock};
|
||||||
|
|
||||||
use {anyhow::Context as AnyhowContext, bstr::ByteVec};
|
use {anyhow::Context as AnyhowContext, bstr::ByteVec};
|
||||||
|
|
||||||
@@ -2897,6 +2897,9 @@ impl Flag for HyperlinkFormat {
|
|||||||
r"Set the format of hyperlinks."
|
r"Set the format of hyperlinks."
|
||||||
}
|
}
|
||||||
fn doc_long(&self) -> &'static str {
|
fn doc_long(&self) -> &'static str {
|
||||||
|
static DOC: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
let mut doc = String::new();
|
||||||
|
doc.push_str(
|
||||||
r#"
|
r#"
|
||||||
Set the format of hyperlinks to use when printing results. Hyperlinks make
|
Set the format of hyperlinks to use when printing results. Hyperlinks make
|
||||||
certain elements of ripgrep's output, such as file paths, clickable. This
|
certain elements of ripgrep's output, such as file paths, clickable. This
|
||||||
@@ -2905,10 +2908,23 @@ example, the format \fBfile://{host}{path}\fP will emit an RFC 8089 hyperlink.
|
|||||||
To see the format that ripgrep is using, pass the \flag{debug} flag.
|
To see the format that ripgrep is using, pass the \flag{debug} flag.
|
||||||
.sp
|
.sp
|
||||||
Alternatively, a format string may correspond to one of the following aliases:
|
Alternatively, a format string may correspond to one of the following aliases:
|
||||||
\fBdefault\fP, \fBnone\fP, \fBfile\fP, \fBgrep+\fP, \fBkitty\fP, \fBmacvim\fP,
|
"#,
|
||||||
\fBtextmate\fP, \fBvscode\fP, \fBvscode-insiders\fP, \fBvscodium\fP. The
|
);
|
||||||
alias will be replaced with a format string that is intended to work for the
|
|
||||||
corresponding application.
|
let mut aliases = grep::printer::hyperlink_aliases();
|
||||||
|
aliases.sort_by_key(|alias| {
|
||||||
|
alias.display_priority().unwrap_or(i16::MAX)
|
||||||
|
});
|
||||||
|
for (i, alias) in aliases.iter().enumerate() {
|
||||||
|
doc.push_str(r"\fB");
|
||||||
|
doc.push_str(alias.name());
|
||||||
|
doc.push_str(r"\fP");
|
||||||
|
doc.push_str(if i < aliases.len() - 1 { ", " } else { "." });
|
||||||
|
}
|
||||||
|
doc.push_str(
|
||||||
|
r#"
|
||||||
|
The alias will be replaced with a format string that is intended to work for
|
||||||
|
the corresponding application.
|
||||||
.sp
|
.sp
|
||||||
The following variables are available in the format string:
|
The following variables are available in the format string:
|
||||||
.sp
|
.sp
|
||||||
@@ -2985,7 +3001,11 @@ in the output. To make the path appear, and thus also a hyperlink, use the
|
|||||||
.sp
|
.sp
|
||||||
For more information on hyperlinks in terminal emulators, see:
|
For more information on hyperlinks in terminal emulators, see:
|
||||||
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||||
"#
|
"#,
|
||||||
|
);
|
||||||
|
doc
|
||||||
|
});
|
||||||
|
&DOC
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> {
|
fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> {
|
||||||
|
|||||||
44
crates/printer/src/hyperlink/aliases.rs
Normal file
44
crates/printer/src/hyperlink/aliases.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use crate::hyperlink::HyperlinkAlias;
|
||||||
|
|
||||||
|
/// Aliases to well-known hyperlink schemes.
|
||||||
|
///
|
||||||
|
/// These need to be sorted by name.
|
||||||
|
pub(super) const HYPERLINK_PATTERN_ALIASES: &[HyperlinkAlias] = &[
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
prioritized_alias(0, "default", "file://{host}{path}"),
|
||||||
|
#[cfg(windows)]
|
||||||
|
prioritized_alias(0, "default", "file://{path}"),
|
||||||
|
alias("file", "file://{host}{path}"),
|
||||||
|
// https://github.com/misaki-web/grepp
|
||||||
|
alias("grep+", "grep+://{path}:{line}"),
|
||||||
|
alias("kitty", "file://{host}{path}#{line}"),
|
||||||
|
// https://macvim.org/docs/gui_mac.txt.html#mvim%3A%2F%2F
|
||||||
|
alias(
|
||||||
|
"macvim",
|
||||||
|
"mvim://open?url=file://{path}&line={line}&column={column}",
|
||||||
|
),
|
||||||
|
prioritized_alias(1, "none", ""),
|
||||||
|
// https://macromates.com/blog/2007/the-textmate-url-scheme/
|
||||||
|
alias(
|
||||||
|
"textmate",
|
||||||
|
"txmt://open?url=file://{path}&line={line}&column={column}",
|
||||||
|
),
|
||||||
|
// https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls
|
||||||
|
alias("vscode", "vscode://file{path}:{line}:{column}"),
|
||||||
|
alias("vscode-insiders", "vscode-insiders://file{path}:{line}:{column}"),
|
||||||
|
alias("vscodium", "vscodium://file{path}:{line}:{column}"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Creates a [`HyperlinkAlias`].
|
||||||
|
const fn alias(name: &'static str, format: &'static str) -> HyperlinkAlias {
|
||||||
|
HyperlinkAlias { name, format, display_priority: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`HyperlinkAlias`] with a display priority.
|
||||||
|
const fn prioritized_alias(
|
||||||
|
priority: i16,
|
||||||
|
name: &'static str,
|
||||||
|
format: &'static str,
|
||||||
|
) -> HyperlinkAlias {
|
||||||
|
HyperlinkAlias { name, format, display_priority: Some(priority) }
|
||||||
|
}
|
||||||
@@ -5,7 +5,11 @@ use {
|
|||||||
termcolor::{HyperlinkSpec, WriteColor},
|
termcolor::{HyperlinkSpec, WriteColor},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{hyperlink_aliases, util::DecimalFormatter};
|
use crate::util::DecimalFormatter;
|
||||||
|
|
||||||
|
use self::aliases::HYPERLINK_PATTERN_ALIASES;
|
||||||
|
|
||||||
|
mod aliases;
|
||||||
|
|
||||||
/// Hyperlink configuration.
|
/// Hyperlink configuration.
|
||||||
///
|
///
|
||||||
@@ -107,8 +111,8 @@ impl std::str::FromStr for HyperlinkFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut builder = FormatBuilder::new();
|
let mut builder = FormatBuilder::new();
|
||||||
let input = match hyperlink_aliases::find(s) {
|
let input = match HyperlinkAlias::find(s) {
|
||||||
Some(format) => format,
|
Some(alias) => alias.format(),
|
||||||
None => s,
|
None => s,
|
||||||
};
|
};
|
||||||
let mut name = String::new();
|
let mut name = String::new();
|
||||||
@@ -179,6 +183,57 @@ impl std::fmt::Display for HyperlinkFormat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An alias for a hyperlink format.
|
||||||
|
///
|
||||||
|
/// Hyperlink aliases are built-in formats, therefore they hold static values.
|
||||||
|
/// Some of their features are usable in const blocks.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct HyperlinkAlias {
|
||||||
|
name: &'static str,
|
||||||
|
format: &'static str,
|
||||||
|
display_priority: Option<i16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HyperlinkAlias {
|
||||||
|
/// Returns the name of the alias.
|
||||||
|
pub const fn name(&self) -> &str {
|
||||||
|
self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the display priority of this alias.
|
||||||
|
///
|
||||||
|
/// If no priority is set, then `None` is returned.
|
||||||
|
///
|
||||||
|
/// The display priority is meant to reflect some special status associated
|
||||||
|
/// with an alias. For example, the `default` and `none` aliases have a
|
||||||
|
/// display priority. This is meant to encourage listing them first in
|
||||||
|
/// documentation.
|
||||||
|
///
|
||||||
|
/// A lower display priority implies the alias should be shown before
|
||||||
|
/// aliases with a higher (or absent) display priority.
|
||||||
|
///
|
||||||
|
/// Callers cannot rely on any specific display priority value to remain
|
||||||
|
/// stable across semver compatible releases of this crate.
|
||||||
|
pub const fn display_priority(&self) -> Option<i16> {
|
||||||
|
self.display_priority
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the format string of the alias.
|
||||||
|
const fn format(&self) -> &'static str {
|
||||||
|
self.format
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks for the hyperlink alias defined by the given name.
|
||||||
|
///
|
||||||
|
/// If one does not exist, `None` is returned.
|
||||||
|
fn find(name: &str) -> Option<&HyperlinkAlias> {
|
||||||
|
HYPERLINK_PATTERN_ALIASES
|
||||||
|
.binary_search_by_key(&name, |alias| alias.name())
|
||||||
|
.map(|i| &HYPERLINK_PATTERN_ALIASES[i])
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A static environment for hyperlink interpolation.
|
/// A static environment for hyperlink interpolation.
|
||||||
///
|
///
|
||||||
/// This environment permits setting the values of variables used in hyperlink
|
/// This environment permits setting the values of variables used in hyperlink
|
||||||
@@ -255,15 +310,18 @@ impl std::fmt::Display for HyperlinkFormatError {
|
|||||||
|
|
||||||
match self.kind {
|
match self.kind {
|
||||||
NoVariables => {
|
NoVariables => {
|
||||||
let aliases = hyperlink_aliases::iter()
|
let mut aliases = hyperlink_aliases();
|
||||||
.map(|(name, _)| name)
|
aliases.sort_by_key(|alias| {
|
||||||
.collect::<Vec<&str>>()
|
alias.display_priority().unwrap_or(i16::MAX)
|
||||||
.join(", ");
|
});
|
||||||
|
let names: Vec<&str> =
|
||||||
|
aliases.iter().map(|alias| alias.name()).collect();
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"at least a {{path}} variable is required in a \
|
"at least a {{path}} variable is required in a \
|
||||||
hyperlink format, or otherwise use a valid alias: {}",
|
hyperlink format, or otherwise use a valid alias: \
|
||||||
aliases,
|
{aliases}",
|
||||||
|
aliases = names.join(", "),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
NoPathVariable => {
|
NoPathVariable => {
|
||||||
@@ -863,6 +921,26 @@ impl HyperlinkPath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the set of hyperlink aliases supported by this crate.
|
||||||
|
///
|
||||||
|
/// Aliases are supported by the `FromStr` trait implementation of a
|
||||||
|
/// [`HyperlinkFormat`]. That is, if an alias is seen, then it is automatically
|
||||||
|
/// replaced with the corresponding format. For example, the `vscode` alias
|
||||||
|
/// maps to `vscode://file{path}:{line}:{column}`.
|
||||||
|
///
|
||||||
|
/// This is exposed to allow callers to include hyperlink aliases in
|
||||||
|
/// documentation in a way that is guaranteed to match what is actually
|
||||||
|
/// supported.
|
||||||
|
///
|
||||||
|
/// The list returned is guaranteed to be sorted lexicographically
|
||||||
|
/// by the alias name. Callers may want to re-sort the list using
|
||||||
|
/// [`HyperlinkAlias::display_priority`] via a stable sort when showing the
|
||||||
|
/// list to users. This will cause special aliases like `none` and `default` to
|
||||||
|
/// appear first.
|
||||||
|
pub fn hyperlink_aliases() -> Vec<HyperlinkAlias> {
|
||||||
|
HYPERLINK_PATTERN_ALIASES.iter().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -1036,4 +1114,46 @@ mod tests {
|
|||||||
"//server/dir/file.txt"
|
"//server/dir/file.txt"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aliases_are_sorted() {
|
||||||
|
let aliases = hyperlink_aliases();
|
||||||
|
let mut prev =
|
||||||
|
aliases.first().expect("aliases should be non-empty").name();
|
||||||
|
for alias in aliases.iter().skip(1) {
|
||||||
|
let name = alias.name();
|
||||||
|
assert!(
|
||||||
|
name > prev,
|
||||||
|
"'{prev}' should come before '{name}' in \
|
||||||
|
HYPERLINK_PATTERN_ALIASES",
|
||||||
|
);
|
||||||
|
prev = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alias_names_are_reasonable() {
|
||||||
|
for alias in hyperlink_aliases() {
|
||||||
|
// 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!(alias.name().chars().all(|c| c.is_alphanumeric()
|
||||||
|
|| c == '+'
|
||||||
|
|| c == '-'
|
||||||
|
|| c == '.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aliases_are_valid_formats() {
|
||||||
|
for alias in hyperlink_aliases() {
|
||||||
|
let (name, format) = (alias.name(), alias.format());
|
||||||
|
assert!(
|
||||||
|
format.parse::<HyperlinkFormat>().is_ok(),
|
||||||
|
"invalid hyperlink alias '{name}': {format}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/// Aliases to well-known hyperlink schemes.
|
|
||||||
///
|
|
||||||
/// These need to be sorted by name.
|
|
||||||
const HYPERLINK_PATTERN_ALIASES: &[(&str, &str)] = &[
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
("default", "file://{host}{path}"),
|
|
||||||
#[cfg(windows)]
|
|
||||||
("default", "file://{path}"),
|
|
||||||
("file", "file://{host}{path}"),
|
|
||||||
// https://github.com/misaki-web/grepp
|
|
||||||
("grep+", "grep+://{path}:{line}"),
|
|
||||||
("kitty", "file://{host}{path}#{line}"),
|
|
||||||
// https://macvim.org/docs/gui_mac.txt.html#mvim%3A%2F%2F
|
|
||||||
("macvim", "mvim://open?url=file://{path}&line={line}&column={column}"),
|
|
||||||
("none", ""),
|
|
||||||
// https://macromates.com/blog/2007/the-textmate-url-scheme/
|
|
||||||
("textmate", "txmt://open?url=file://{path}&line={line}&column={column}"),
|
|
||||||
// https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls
|
|
||||||
("vscode", "vscode://file{path}:{line}:{column}"),
|
|
||||||
("vscode-insiders", "vscode-insiders://file{path}:{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}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,8 +63,8 @@ assert_eq!(output, expected);
|
|||||||
pub use crate::{
|
pub use crate::{
|
||||||
color::{default_color_specs, ColorError, ColorSpecs, UserColorSpec},
|
color::{default_color_specs, ColorError, ColorSpecs, UserColorSpec},
|
||||||
hyperlink::{
|
hyperlink::{
|
||||||
HyperlinkConfig, HyperlinkEnvironment, HyperlinkFormat,
|
hyperlink_aliases, HyperlinkAlias, HyperlinkConfig,
|
||||||
HyperlinkFormatError,
|
HyperlinkEnvironment, HyperlinkFormat, HyperlinkFormatError,
|
||||||
},
|
},
|
||||||
path::{PathPrinter, PathPrinterBuilder},
|
path::{PathPrinter, PathPrinterBuilder},
|
||||||
standard::{Standard, StandardBuilder, StandardSink},
|
standard::{Standard, StandardBuilder, StandardSink},
|
||||||
@@ -92,7 +92,6 @@ mod macros;
|
|||||||
mod color;
|
mod color;
|
||||||
mod counter;
|
mod counter;
|
||||||
mod hyperlink;
|
mod hyperlink;
|
||||||
mod hyperlink_aliases;
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
mod json;
|
mod json;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
|||||||
Reference in New Issue
Block a user