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:
committed by
Andrew Gallant
parent
afb7f6ebd5
commit
458ca71e96
@@ -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<()> {
|
||||
|
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: u16,
|
||||
name: &'static str,
|
||||
format: &'static str,
|
||||
) -> HyperlinkAlias {
|
||||
HyperlinkAlias { name, format, display_priority: Some(priority) }
|
||||
}
|
@@ -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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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::{
|
||||
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")]
|
||||
|
Reference in New Issue
Block a user