1
0
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:
Lucas Trzesniewski
2025-07-16 21:53:43 +02:00
committed by Andrew Gallant
parent fdfda9ae73
commit 66aa4a63bb
5 changed files with 202 additions and 104 deletions

View File

@@ -17,7 +17,7 @@ ripgrep. For example, `-E`, `--encoding` and `--no-encoding` all manipulate the
same encoding state in ripgrep.
*/
use std::path::PathBuf;
use std::{path::PathBuf, sync::LazyLock};
use {anyhow::Context as AnyhowContext, bstr::ByteVec};
@@ -2897,6 +2897,9 @@ impl Flag for HyperlinkFormat {
r"Set the format of hyperlinks."
}
fn doc_long(&self) -> &'static str {
static DOC: LazyLock<String> = LazyLock::new(|| {
let mut doc = String::new();
doc.push_str(
r#"
Set the format of hyperlinks to use when printing results. Hyperlinks make
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.
.sp
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
The following variables are available in the format string:
.sp
@@ -2985,7 +3001,11 @@ in the output. To make the path appear, and thus also a hyperlink, use the
.sp
For more information on hyperlinks in terminal emulators, see:
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
"#
"#,
);
doc
});
&DOC
}
fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> {

View 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) }
}

View File

@@ -5,7 +5,11 @@ use {
termcolor::{HyperlinkSpec, WriteColor},
};
use crate::{hyperlink_aliases, util::DecimalFormatter};
use crate::util::DecimalFormatter;
use self::aliases::HYPERLINK_PATTERN_ALIASES;
mod aliases;
/// Hyperlink configuration.
///
@@ -107,8 +111,8 @@ impl std::str::FromStr for HyperlinkFormat {
}
let mut builder = FormatBuilder::new();
let input = match hyperlink_aliases::find(s) {
Some(format) => format,
let input = match HyperlinkAlias::find(s) {
Some(alias) => alias.format(),
None => s,
};
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.
///
/// This environment permits setting the values of variables used in hyperlink
@@ -255,15 +310,18 @@ impl std::fmt::Display for HyperlinkFormatError {
match self.kind {
NoVariables => {
let aliases = hyperlink_aliases::iter()
.map(|(name, _)| name)
.collect::<Vec<&str>>()
.join(", ");
let mut aliases = hyperlink_aliases();
aliases.sort_by_key(|alias| {
alias.display_priority().unwrap_or(i16::MAX)
});
let names: Vec<&str> =
aliases.iter().map(|alias| alias.name()).collect();
write!(
f,
"at least a {{path}} variable is required in a \
hyperlink format, or otherwise use a valid alias: {}",
aliases,
hyperlink format, or otherwise use a valid alias: \
{aliases}",
aliases = names.join(", "),
)
}
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)]
mod tests {
use std::str::FromStr;
@@ -1036,4 +1114,46 @@ mod tests {
"//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}",
);
}
}
}

View File

@@ -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}",
);
}
}
}

View File

@@ -63,8 +63,8 @@ assert_eq!(output, expected);
pub use crate::{
color::{default_color_specs, ColorError, ColorSpecs, UserColorSpec},
hyperlink::{
HyperlinkConfig, HyperlinkEnvironment, HyperlinkFormat,
HyperlinkFormatError,
hyperlink_aliases, HyperlinkAlias, HyperlinkConfig,
HyperlinkEnvironment, HyperlinkFormat, HyperlinkFormatError,
},
path::{PathPrinter, PathPrinterBuilder},
standard::{Standard, StandardBuilder, StandardSink},
@@ -92,7 +92,6 @@ mod macros;
mod color;
mod counter;
mod hyperlink;
mod hyperlink_aliases;
#[cfg(feature = "serde")]
mod json;
#[cfg(feature = "serde")]