1
0
mirror of https://github.com/BurntSushi/ripgrep.git synced 2025-09-16 08:26:28 +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 afb7f6ebd5
commit 458ca71e96
5 changed files with 199 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,7 +2897,10 @@ impl Flag for HyperlinkFormat {
r"Set the format of hyperlinks."
}
fn doc_long(&self) -> &'static str {
r#"
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
generally only works in terminal emulators that support OSC-8 hyperlinks. For
@@ -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(u16::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: u16,
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,54 @@ 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<u16>,
}
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.
pub const fn display_priority(&self) -> Option<u16> {
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 +307,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(u16::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 +918,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 +1111,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")]