mirror of
				https://github.com/BurntSushi/ripgrep.git
				synced 2025-10-30 23:17:47 +02:00 
			
		
		
		
	printer: add hyperlinks
This commit represents the initial work to get hyperlinks working and was submitted as part of PR #2483. Subsequent commits largely retain the functionality and structure of the hyperlink support added here, but rejigger some things around.
This commit is contained in:
		
				
					committed by
					
						 Andrew Gallant
						Andrew Gallant
					
				
			
			
				
	
			
			
			
						parent
						
							86ef683308
						
					
				
				
					commit
					1a50324013
				
			
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -7,6 +7,7 @@ target | ||||
| /termcolor/Cargo.lock | ||||
| /wincolor/Cargo.lock | ||||
| /deployment | ||||
| /.idea | ||||
|  | ||||
| # Snapcraft files | ||||
| stage | ||||
|   | ||||
							
								
								
									
										69
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										69
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -136,6 +136,16 @@ version = "1.0.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| 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]] | ||||
| name = "glob" | ||||
| version = "0.3.1" | ||||
| @@ -208,9 +218,11 @@ version = "0.1.7" | ||||
| dependencies = [ | ||||
|  "base64", | ||||
|  "bstr", | ||||
|  "gethostname", | ||||
|  "grep-matcher", | ||||
|  "grep-regex", | ||||
|  "grep-searcher", | ||||
|  "lazy_static", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "termcolor", | ||||
| @@ -612,3 +624,60 @@ name = "winapi-x86_64-pc-windows-gnu" | ||||
| version = "0.4.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| 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]' | ||||
|     '--field-context-separator[set string to delimit fields in context lines]' | ||||
|     '--field-match-separator[set string to delimit fields in matching lines]' | ||||
|     '--hyperlink-format=[specify pattern for hyperlinks]:pattern' | ||||
|     '--trace[show more verbose debug messages]' | ||||
|     '--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size (bytes)' | ||||
|     "(1 stats)--files[show each file that would be searched (but don't search)]" | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| use std::io; | ||||
|  | ||||
| use termcolor; | ||||
| use termcolor::HyperlinkSpec; | ||||
|  | ||||
| use crate::is_tty_stdout; | ||||
|  | ||||
| @@ -101,6 +102,16 @@ impl termcolor::WriteColor for StandardStream { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn supports_hyperlinks(&self) -> bool { | ||||
|         use self::StandardStreamKind::*; | ||||
|  | ||||
|         match self.0 { | ||||
|             LineBuffered(ref w) => w.supports_hyperlinks(), | ||||
|             BlockBuffered(ref w) => w.supports_hyperlinks(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn set_color(&mut self, spec: &termcolor::ColorSpec) -> io::Result<()> { | ||||
|         use self::StandardStreamKind::*; | ||||
| @@ -111,6 +122,16 @@ impl termcolor::WriteColor for StandardStream { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> { | ||||
|         use self::StandardStreamKind::*; | ||||
|  | ||||
|         match self.0 { | ||||
|             LineBuffered(ref mut w) => w.set_hyperlink(link), | ||||
|             BlockBuffered(ref mut w) => w.set_hyperlink(link), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn reset(&mut self) -> io::Result<()> { | ||||
|         use self::StandardStreamKind::*; | ||||
|   | ||||
| @@ -580,6 +580,7 @@ pub fn all_args_and_flags() -> Vec<RGArg> { | ||||
|     flag_glob_case_insensitive(&mut args); | ||||
|     flag_heading(&mut args); | ||||
|     flag_hidden(&mut args); | ||||
|     flag_hyperlink_format(&mut args); | ||||
|     flag_iglob(&mut args); | ||||
|     flag_ignore_case(&mut args); | ||||
|     flag_ignore_file(&mut args); | ||||
| @@ -1494,6 +1495,26 @@ This flag can be disabled with --no-hidden. | ||||
|     args.push(arg); | ||||
| } | ||||
|  | ||||
| fn flag_hyperlink_format(args: &mut Vec<RGArg>) { | ||||
|     const SHORT: &str = "Set the format of hyperlinks to match results."; | ||||
|     const LONG: &str = long!( | ||||
|         "\ | ||||
| Set the format of hyperlinks to match results. This defines a pattern which | ||||
| can contain the following placeholders: {file}, {line}, {column}, and {host}. | ||||
| An empty pattern or 'none' disables hyperlinks. | ||||
|  | ||||
| The {file} placeholder is required, and will be replaced with the absolute | ||||
| 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}' | ||||
| " | ||||
|     ); | ||||
|     let arg = | ||||
|         RGArg::flag("hyperlink-format", "FORMAT").help(SHORT).long_help(LONG); | ||||
|     args.push(arg); | ||||
| } | ||||
|  | ||||
| fn flag_iglob(args: &mut Vec<RGArg>) { | ||||
|     const SHORT: &str = "Include or exclude files case insensitively."; | ||||
|     const LONG: &str = long!( | ||||
|   | ||||
| @@ -5,6 +5,7 @@ use std::fs; | ||||
| use std::io::{self, Write}; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::process; | ||||
| use std::str::FromStr; | ||||
| use std::sync::Arc; | ||||
| use std::time::SystemTime; | ||||
|  | ||||
| @@ -17,8 +18,8 @@ use grep::pcre2::{ | ||||
|     RegexMatcherBuilder as PCRE2RegexMatcherBuilder, | ||||
| }; | ||||
| use grep::printer::{ | ||||
|     default_color_specs, ColorSpecs, JSONBuilder, Standard, StandardBuilder, | ||||
|     Stats, Summary, SummaryBuilder, SummaryKind, JSON, | ||||
|     default_color_specs, ColorSpecs, HyperlinkPattern, JSONBuilder, Standard, | ||||
|     StandardBuilder, Stats, Summary, SummaryBuilder, SummaryKind, JSON, | ||||
| }; | ||||
| use grep::regex::{ | ||||
|     RegexMatcher as RustRegexMatcher, | ||||
| @@ -235,6 +236,7 @@ impl Args { | ||||
|         let mut builder = PathPrinterBuilder::new(); | ||||
|         builder | ||||
|             .color_specs(self.matches().color_specs()?) | ||||
|             .hyperlink_pattern(self.matches().hyperlink_pattern()?) | ||||
|             .separator(self.matches().path_separator()?) | ||||
|             .terminator(self.matches().path_terminator().unwrap_or(b'\n')); | ||||
|         Ok(builder.build(wtr)) | ||||
| @@ -772,6 +774,7 @@ impl ArgMatches { | ||||
|         let mut builder = StandardBuilder::new(); | ||||
|         builder | ||||
|             .color_specs(self.color_specs()?) | ||||
|             .hyperlink_pattern(self.hyperlink_pattern()?) | ||||
|             .stats(self.stats()) | ||||
|             .heading(self.heading()) | ||||
|             .path(self.with_filename(paths)) | ||||
| @@ -811,6 +814,7 @@ impl ArgMatches { | ||||
|         builder | ||||
|             .kind(self.summary_kind().expect("summary format")) | ||||
|             .color_specs(self.color_specs()?) | ||||
|             .hyperlink_pattern(self.hyperlink_pattern()?) | ||||
|             .stats(self.stats()) | ||||
|             .path(self.with_filename(paths)) | ||||
|             .max_matches(self.max_count()?) | ||||
| @@ -1118,6 +1122,17 @@ impl ArgMatches { | ||||
|         self.is_present("hidden") || self.unrestricted_count() >= 2 | ||||
|     } | ||||
|  | ||||
|     /// Returns the hyperlink pattern to use. A default pattern suitable | ||||
|     /// for the current system is used if the value is not set. | ||||
|     /// | ||||
|     /// If an invalid pattern is provided, then an error is returned. | ||||
|     fn hyperlink_pattern(&self) -> Result<HyperlinkPattern> { | ||||
|         Ok(match self.value_of_lossy("hyperlink-format") { | ||||
|             Some(pattern) => HyperlinkPattern::from_str(&pattern)?, | ||||
|             None => HyperlinkPattern::default_file_scheme(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// Returns true if ignore files should be processed case insensitively. | ||||
|     fn ignore_file_case_insensitive(&self) -> bool { | ||||
|         self.is_present("ignore-file-case-insensitive") | ||||
|   | ||||
| @@ -1,13 +1,16 @@ | ||||
| use std::io; | ||||
| use std::path::Path; | ||||
|  | ||||
| use grep::printer::{ColorSpecs, PrinterPath}; | ||||
| use grep::printer::{ | ||||
|     ColorSpecs, HyperlinkPattern, HyperlinkSpan, PrinterPath, | ||||
| }; | ||||
| use termcolor::WriteColor; | ||||
|  | ||||
| /// A configuration for describing how paths should be written. | ||||
| #[derive(Clone, Debug)] | ||||
| struct Config { | ||||
|     colors: ColorSpecs, | ||||
|     hyperlink_pattern: HyperlinkPattern, | ||||
|     separator: Option<u8>, | ||||
|     terminator: u8, | ||||
| } | ||||
| @@ -16,6 +19,7 @@ impl Default for Config { | ||||
|     fn default() -> Config { | ||||
|         Config { | ||||
|             colors: ColorSpecs::default(), | ||||
|             hyperlink_pattern: HyperlinkPattern::default(), | ||||
|             separator: None, | ||||
|             terminator: b'\n', | ||||
|         } | ||||
| @@ -37,7 +41,7 @@ impl PathPrinterBuilder { | ||||
|     /// Create a new path printer with the current configuration that writes | ||||
|     /// paths to the given writer. | ||||
|     pub fn build<W: WriteColor>(&self, wtr: W) -> PathPrinter<W> { | ||||
|         PathPrinter { config: self.config.clone(), wtr } | ||||
|         PathPrinter { config: self.config.clone(), wtr, buf: vec![] } | ||||
|     } | ||||
|  | ||||
|     /// Set the color specification for this printer. | ||||
| @@ -52,6 +56,17 @@ impl PathPrinterBuilder { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the hyperlink pattern to use for hyperlinks output by this printer. | ||||
|     /// | ||||
|     /// Colors need to be enabled for hyperlinks to be output. | ||||
|     pub fn hyperlink_pattern( | ||||
|         &mut self, | ||||
|         pattern: HyperlinkPattern, | ||||
|     ) -> &mut PathPrinterBuilder { | ||||
|         self.config.hyperlink_pattern = pattern; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// A path separator. | ||||
|     /// | ||||
|     /// When provided, the path's default separator will be replaced with | ||||
| @@ -80,6 +95,7 @@ impl PathPrinterBuilder { | ||||
| pub struct PathPrinter<W> { | ||||
|     config: Config, | ||||
|     wtr: W, | ||||
|     buf: Vec<u8>, | ||||
| } | ||||
|  | ||||
| impl<W: WriteColor> PathPrinter<W> { | ||||
| @@ -89,10 +105,30 @@ impl<W: WriteColor> PathPrinter<W> { | ||||
|         if !self.wtr.supports_color() { | ||||
|             self.wtr.write_all(ppath.as_bytes())?; | ||||
|         } else { | ||||
|             let mut hyperlink = self.start_hyperlink_span(&ppath)?; | ||||
|             self.wtr.set_color(self.config.colors.path())?; | ||||
|             self.wtr.write_all(ppath.as_bytes())?; | ||||
|             self.wtr.reset()?; | ||||
|             hyperlink.end(&mut self.wtr)?; | ||||
|         } | ||||
|         self.wtr.write_all(&[self.config.terminator]) | ||||
|     } | ||||
|  | ||||
|     /// Starts a hyperlink span when applicable. | ||||
|     fn start_hyperlink_span( | ||||
|         &mut self, | ||||
|         path: &PrinterPath, | ||||
|     ) -> io::Result<HyperlinkSpan> { | ||||
|         if self.wtr.supports_hyperlinks() { | ||||
|             if let Some(spec) = path.create_hyperlink_spec( | ||||
|                 &self.config.hyperlink_pattern, | ||||
|                 None, | ||||
|                 None, | ||||
|                 &mut self.buf, | ||||
|             ) { | ||||
|                 return Ok(HyperlinkSpan::start(&mut self.wtr, &spec)?); | ||||
|             } | ||||
|         } | ||||
|         Ok(HyperlinkSpan::default()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,8 +21,10 @@ serde1 = ["base64", "serde", "serde_json"] | ||||
| [dependencies] | ||||
| base64 = { version = "0.20.0", optional = true } | ||||
| bstr = "1.6.0" | ||||
| gethostname = "0.4.3" | ||||
| grep-matcher = { version = "0.1.6", path = "../matcher" } | ||||
| grep-searcher = { version = "0.1.11", path = "../searcher" } | ||||
| lazy_static = "1.1.0" | ||||
| termcolor = "1.0.4" | ||||
| serde = { version = "1.0.77", optional = true, features = ["derive"] } | ||||
| serde_json = { version = "1.0.27", optional = true } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use std::io::{self, Write}; | ||||
|  | ||||
| use termcolor::{ColorSpec, WriteColor}; | ||||
| use termcolor::{ColorSpec, HyperlinkSpec, WriteColor}; | ||||
|  | ||||
| /// A writer that counts the number of bytes that have been successfully | ||||
| /// written. | ||||
| @@ -76,10 +76,18 @@ impl<W: WriteColor> WriteColor for CounterWriter<W> { | ||||
|         self.wtr.supports_color() | ||||
|     } | ||||
|  | ||||
|     fn supports_hyperlinks(&self) -> bool { | ||||
|         self.wtr.supports_hyperlinks() | ||||
|     } | ||||
|  | ||||
|     fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { | ||||
|         self.wtr.set_color(spec) | ||||
|     } | ||||
|  | ||||
|     fn set_hyperlink(&mut self, link: &HyperlinkSpec) -> io::Result<()> { | ||||
|         self.wtr.set_hyperlink(link) | ||||
|     } | ||||
|  | ||||
|     fn reset(&mut self) -> io::Result<()> { | ||||
|         self.wtr.reset() | ||||
|     } | ||||
|   | ||||
							
								
								
									
										664
									
								
								crates/printer/src/hyperlink.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										664
									
								
								crates/printer/src/hyperlink.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,664 @@ | ||||
| use crate::hyperlink_aliases::HYPERLINK_PATTERN_ALIASES; | ||||
| use bstr::ByteSlice; | ||||
| use std::error::Error; | ||||
| use std::fmt::Display; | ||||
| use std::io; | ||||
| use std::io::Write; | ||||
| use std::path::Path; | ||||
| use std::str::FromStr; | ||||
| use termcolor::{HyperlinkSpec, WriteColor}; | ||||
|  | ||||
| /// A builder for `HyperlinkPattern`. | ||||
| /// | ||||
| /// Once a `HyperlinkPattern` is built, it is immutable. | ||||
| #[derive(Debug)] | ||||
| pub struct HyperlinkPatternBuilder { | ||||
|     parts: Vec<Part>, | ||||
| } | ||||
|  | ||||
| /// A hyperlink pattern with placeholders. | ||||
| /// | ||||
| /// This can be created with `HyperlinkPatternBuilder` or from a string | ||||
| /// using `HyperlinkPattern::from_str`. | ||||
| #[derive(Clone, Debug, Default, Eq, PartialEq)] | ||||
| pub struct HyperlinkPattern { | ||||
|     parts: Vec<Part>, | ||||
|     is_line_dependent: bool, | ||||
| } | ||||
|  | ||||
| /// A hyperlink pattern part. | ||||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||||
| enum Part { | ||||
|     /// Static text. Can include invariant values such as the hostname. | ||||
|     Text(Vec<u8>), | ||||
|     /// Placeholder for the file path. | ||||
|     File, | ||||
|     /// Placeholder for the line number. | ||||
|     Line, | ||||
|     /// Placeholder for the column number. | ||||
|     Column, | ||||
| } | ||||
|  | ||||
| /// An error that can occur when parsing a hyperlink pattern. | ||||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||||
| pub enum HyperlinkPatternError { | ||||
|     /// This occurs when the pattern syntax is not valid. | ||||
|     InvalidSyntax, | ||||
|     /// This occurs when the {file} placeholder is missing. | ||||
|     NoFilePlaceholder, | ||||
|     /// This occurs when the {line} placeholder is missing, | ||||
|     /// while the {column} placeholder is present. | ||||
|     NoLinePlaceholder, | ||||
|     /// This occurs when an unknown placeholder is used. | ||||
|     InvalidPlaceholder(String), | ||||
|     /// The pattern doesn't start with a valid scheme. | ||||
|     InvalidScheme, | ||||
| } | ||||
|  | ||||
| /// The values to replace the pattern placeholders with. | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct HyperlinkValues<'a> { | ||||
|     file: &'a HyperlinkPath, | ||||
|     line: u64, | ||||
|     column: u64, | ||||
| } | ||||
|  | ||||
| /// Represents the {file} part of a hyperlink. | ||||
| /// | ||||
| /// This is the value to use as-is in the hyperlink, converted from an OS file path. | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct HyperlinkPath(Vec<u8>); | ||||
|  | ||||
| impl HyperlinkPatternBuilder { | ||||
|     /// Creates a new hyperlink pattern builder. | ||||
|     pub fn new() -> Self { | ||||
|         Self { parts: vec![] } | ||||
|     } | ||||
|  | ||||
|     /// Appends static text. | ||||
|     pub fn append_text(&mut self, text: &[u8]) -> &mut Self { | ||||
|         if let Some(Part::Text(contents)) = self.parts.last_mut() { | ||||
|             contents.extend_from_slice(text); | ||||
|         } else if !text.is_empty() { | ||||
|             self.parts.push(Part::Text(text.to_vec())); | ||||
|         } | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Appends the hostname. | ||||
|     /// | ||||
|     /// On WSL, appends `wsl$/{distro}` instead. | ||||
|     pub fn append_hostname(&mut self) -> &mut Self { | ||||
|         self.append_text(Self::get_hostname().as_bytes()) | ||||
|     } | ||||
|  | ||||
|     /// Returns the hostname to use in the pattern. | ||||
|     /// | ||||
|     /// On WSL, returns `wsl$/{distro}`. | ||||
|     fn get_hostname() -> String { | ||||
|         if cfg!(unix) { | ||||
|             if let Ok(mut wsl_distro) = std::env::var("WSL_DISTRO_NAME") { | ||||
|                 wsl_distro.insert_str(0, "wsl$/"); | ||||
|                 return wsl_distro; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         gethostname::gethostname().to_string_lossy().to_string() | ||||
|     } | ||||
|  | ||||
|     /// Appends a placeholder for the file path. | ||||
|     pub fn append_file(&mut self) -> &mut Self { | ||||
|         self.parts.push(Part::File); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Appends a placeholder for the line number. | ||||
|     pub fn append_line(&mut self) -> &mut Self { | ||||
|         self.parts.push(Part::Line); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Appends a placeholder for the column number. | ||||
|     pub fn append_column(&mut self) -> &mut Self { | ||||
|         self.parts.push(Part::Column); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Builds the pattern. | ||||
|     pub fn build(&self) -> Result<HyperlinkPattern, HyperlinkPatternError> { | ||||
|         self.validate()?; | ||||
|  | ||||
|         Ok(HyperlinkPattern { | ||||
|             parts: self.parts.clone(), | ||||
|             is_line_dependent: self.parts.contains(&Part::Line), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// Validate that the pattern is well-formed. | ||||
|     fn validate(&self) -> Result<(), HyperlinkPatternError> { | ||||
|         if self.parts.is_empty() { | ||||
|             return Ok(()); | ||||
|         } | ||||
|  | ||||
|         if !self.parts.contains(&Part::File) { | ||||
|             return Err(HyperlinkPatternError::NoFilePlaceholder); | ||||
|         } | ||||
|  | ||||
|         if self.parts.contains(&Part::Column) | ||||
|             && !self.parts.contains(&Part::Line) | ||||
|         { | ||||
|             return Err(HyperlinkPatternError::NoLinePlaceholder); | ||||
|         } | ||||
|  | ||||
|         self.validate_scheme() | ||||
|     } | ||||
|  | ||||
|     /// Validate that the pattern starts with a valid scheme. | ||||
|     /// | ||||
|     /// A valid scheme starts with an alphabetic character, continues with | ||||
|     /// a sequence of alphanumeric characters, periods, hyphens or plus signs, | ||||
|     /// and ends with a colon. | ||||
|     fn validate_scheme(&self) -> Result<(), HyperlinkPatternError> { | ||||
|         if let Some(Part::Text(value)) = self.parts.first() { | ||||
|             if let Some(colon_index) = value.find_byte(b':') { | ||||
|                 if value[0].is_ascii_alphabetic() | ||||
|                     && value.iter().take(colon_index).all(|c| { | ||||
|                         c.is_ascii_alphanumeric() | ||||
|                             || matches!(c, b'.' | b'-' | b'+') | ||||
|                     }) | ||||
|                 { | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Err(HyperlinkPatternError::InvalidScheme) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl HyperlinkPattern { | ||||
|     /// Creates an empty hyperlink pattern. | ||||
|     pub fn empty() -> Self { | ||||
|         HyperlinkPattern::default() | ||||
|     } | ||||
|  | ||||
|     /// Creates a default pattern suitable for Unix. | ||||
|     /// | ||||
|     /// The returned pattern is `file://{host}/{file}` | ||||
|     #[cfg(unix)] | ||||
|     pub fn default_file_scheme() -> Self { | ||||
|         HyperlinkPatternBuilder::new() | ||||
|             .append_text(b"file://") | ||||
|             .append_hostname() | ||||
|             .append_text(b"/") | ||||
|             .append_file() | ||||
|             .build() | ||||
|             .unwrap() | ||||
|     } | ||||
|  | ||||
|     /// Creates a default pattern suitable for Windows. | ||||
|     /// | ||||
|     /// The returned pattern is `file:///{file}` | ||||
|     #[cfg(windows)] | ||||
|     pub fn default_file_scheme() -> Self { | ||||
|         HyperlinkPatternBuilder::new() | ||||
|             .append_text(b"file:///") | ||||
|             .append_file() | ||||
|             .build() | ||||
|             .unwrap() | ||||
|     } | ||||
|  | ||||
|     /// Returns true if this pattern is empty. | ||||
|     pub fn is_empty(&self) -> bool { | ||||
|         self.parts.is_empty() | ||||
|     } | ||||
|  | ||||
|     /// Returns true if the pattern can produce line-dependent hyperlinks. | ||||
|     pub fn is_line_dependent(&self) -> bool { | ||||
|         self.is_line_dependent | ||||
|     } | ||||
|  | ||||
|     /// Renders this pattern with the given values to the given output. | ||||
|     pub fn render( | ||||
|         &self, | ||||
|         values: &HyperlinkValues, | ||||
|         output: &mut impl Write, | ||||
|     ) -> io::Result<()> { | ||||
|         for part in &self.parts { | ||||
|             part.render(values, output)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl FromStr for HyperlinkPattern { | ||||
|     type Err = HyperlinkPatternError; | ||||
|  | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         let mut builder = HyperlinkPatternBuilder::new(); | ||||
|         let mut input = s.as_bytes(); | ||||
|  | ||||
|         if let Ok(index) = HYPERLINK_PATTERN_ALIASES | ||||
|             .binary_search_by_key(&input, |&(name, _)| name.as_bytes()) | ||||
|         { | ||||
|             input = HYPERLINK_PATTERN_ALIASES[index].1.as_bytes(); | ||||
|         } | ||||
|  | ||||
|         while !input.is_empty() { | ||||
|             if input[0] == b'{' { | ||||
|                 // Placeholder | ||||
|                 let end = input | ||||
|                     .find_byte(b'}') | ||||
|                     .ok_or(HyperlinkPatternError::InvalidSyntax)?; | ||||
|  | ||||
|                 match &input[1..end] { | ||||
|                     b"file" => builder.append_file(), | ||||
|                     b"line" => builder.append_line(), | ||||
|                     b"column" => builder.append_column(), | ||||
|                     b"host" => builder.append_hostname(), | ||||
|                     other => { | ||||
|                         return Err(HyperlinkPatternError::InvalidPlaceholder( | ||||
|                             String::from_utf8_lossy(other).to_string(), | ||||
|                         )) | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 input = &input[(end + 1)..]; | ||||
|             } else { | ||||
|                 // Static text | ||||
|                 let end = input.find_byte(b'{').unwrap_or(input.len()); | ||||
|                 builder.append_text(&input[..end]); | ||||
|                 input = &input[end..]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         builder.build() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToString for HyperlinkPattern { | ||||
|     fn to_string(&self) -> String { | ||||
|         self.parts.iter().map(|p| p.to_string()).collect() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Part { | ||||
|     fn render( | ||||
|         &self, | ||||
|         values: &HyperlinkValues, | ||||
|         output: &mut impl Write, | ||||
|     ) -> io::Result<()> { | ||||
|         match self { | ||||
|             Part::Text(text) => output.write_all(text), | ||||
|             Part::File => output.write_all(&values.file.0), | ||||
|             Part::Line => write!(output, "{}", values.line), | ||||
|             Part::Column => write!(output, "{}", values.column), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToString for Part { | ||||
|     fn to_string(&self) -> String { | ||||
|         match self { | ||||
|             Part::Text(text) => String::from_utf8_lossy(text).to_string(), | ||||
|             Part::File => "{file}".to_string(), | ||||
|             Part::Line => "{line}".to_string(), | ||||
|             Part::Column => "{column}".to_string(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for HyperlinkPatternError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             HyperlinkPatternError::InvalidSyntax => { | ||||
|                 write!(f, "invalid hyperlink pattern syntax") | ||||
|             } | ||||
|             HyperlinkPatternError::NoFilePlaceholder => { | ||||
|                 write!(f, "the {{file}} placeholder is required in hyperlink patterns") | ||||
|             } | ||||
|             HyperlinkPatternError::NoLinePlaceholder => { | ||||
|                 write!(f, "the hyperlink pattern contains a {{column}} placeholder, \ | ||||
|                     but no {{line}} placeholder is present") | ||||
|             } | ||||
|             HyperlinkPatternError::InvalidPlaceholder(name) => { | ||||
|                 write!( | ||||
|                     f, | ||||
|                     "invalid hyperlink pattern placeholder: '{}', choose from: \ | ||||
|                         file, line, column, host", | ||||
|                     name | ||||
|                 ) | ||||
|             } | ||||
|             HyperlinkPatternError::InvalidScheme => { | ||||
|                 write!( | ||||
|                     f, | ||||
|                     "the hyperlink pattern must start with a valid URL scheme" | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Error for HyperlinkPatternError {} | ||||
|  | ||||
| impl<'a> HyperlinkValues<'a> { | ||||
|     /// Creates a new set of hyperlink values. | ||||
|     pub fn new( | ||||
|         file: &'a HyperlinkPath, | ||||
|         line: Option<u64>, | ||||
|         column: Option<u64>, | ||||
|     ) -> Self { | ||||
|         HyperlinkValues { | ||||
|             file, | ||||
|             line: line.unwrap_or(1), | ||||
|             column: column.unwrap_or(1), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl HyperlinkPath { | ||||
|     /// Returns a hyperlink path from an OS path. | ||||
|     #[cfg(unix)] | ||||
|     pub fn from_path(path: &Path) -> Option<Self> { | ||||
|         // On Unix, this function returns the absolute file path without the leading slash, | ||||
|         // as it makes for more natural hyperlink patterns, for instance: | ||||
|         //   file://{host}/{file}   instead of   file://{host}{file} | ||||
|         //   vscode://file/{file}   instead of   vscode://file{file} | ||||
|         // It also allows for patterns to be multi-platform. | ||||
|  | ||||
|         let path = path.canonicalize().ok()?; | ||||
|         let path = path.to_str()?.as_bytes(); | ||||
|         let path = if path.starts_with(b"/") { &path[1..] } else { path }; | ||||
|         Some(Self::encode(path)) | ||||
|     } | ||||
|  | ||||
|     /// Returns a hyperlink path from an OS path. | ||||
|     #[cfg(windows)] | ||||
|     pub fn from_path(path: &Path) -> Option<Self> { | ||||
|         // On Windows, Path::canonicalize returns the result of | ||||
|         // GetFinalPathNameByHandleW with VOLUME_NAME_DOS, | ||||
|         // which produces paths such as the following: | ||||
|         //   \\?\C:\dir\file.txt           (local path) | ||||
|         //   \\?\UNC\server\dir\file.txt   (network share) | ||||
|         // | ||||
|         // The \\?\ prefix comes from VOLUME_NAME_DOS and is constant. | ||||
|         // It is followed either by the drive letter, or by UNC\ | ||||
|         // (universal naming convention), which denotes a network share. | ||||
|         // | ||||
|         // Given that the default URL pattern on Windows is file:///{file} | ||||
|         // we need to return the following from this function: | ||||
|         //   C:/dir/file.txt        (local path) | ||||
|         //   /server/dir/file.txt   (network share) | ||||
|         // | ||||
|         // Which produces the following links: | ||||
|         //   file:///C:/dir/file.txt        (local path) | ||||
|         //   file:////server/dir/file.txt   (network share) | ||||
|         // | ||||
|         // This substitutes the {file} placeholder with the expected value | ||||
|         // for the most common DOS paths, but on the other hand, | ||||
|         // network paths start with a single slash, which may be unexpected. | ||||
|         // It produces correct URLs though. | ||||
|         // | ||||
|         // Note that the following URL syntax is also valid for network shares: | ||||
|         //   file://server/dir/file.txt | ||||
|         // It is also more consistent with the Unix case, but in order to | ||||
|         // use it, the pattern would have to be file://{file} and | ||||
|         // the {file} placeholder would have to be replaced with | ||||
|         //   /C:/dir/file.txt | ||||
|         // for local files, which is not ideal, and it is certainly unexpected. | ||||
|         // | ||||
|         // Also note that the file://C:/dir/file.txt syntax is not correct, | ||||
|         // even though it often works in practice. | ||||
|         // | ||||
|         // In the end, this choice was confirmed by VSCode, whose pattern | ||||
|         // is vscode://file/{file}:{line}:{column} and which correctly understands | ||||
|         // the following URL format for network drives: | ||||
|         //   vscode://file//server/dir/file.txt:1:1 | ||||
|         // It doesn't parse any other number of slashes in "file//server" as a network path. | ||||
|  | ||||
|         const WIN32_NAMESPACE_PREFIX: &[u8] = br"\\?\"; | ||||
|         const UNC_PREFIX: &[u8] = br"UNC\"; | ||||
|  | ||||
|         let path = path.canonicalize().ok()?; | ||||
|         let mut path = path.to_str()?.as_bytes(); | ||||
|  | ||||
|         if path.starts_with(WIN32_NAMESPACE_PREFIX) { | ||||
|             path = &path[WIN32_NAMESPACE_PREFIX.len()..]; | ||||
|  | ||||
|             if path.starts_with(UNC_PREFIX) { | ||||
|                 path = &path[(UNC_PREFIX.len() - 1)..]; | ||||
|             } | ||||
|         } else { | ||||
|             return None; | ||||
|         } | ||||
|  | ||||
|         Some(Self::encode(path)) | ||||
|     } | ||||
|  | ||||
|     /// Percent-encodes a path. | ||||
|     /// | ||||
|     /// The alphanumeric ASCII characters and "-", ".", "_", "~" are unreserved | ||||
|     /// as per section 2.3 of RFC 3986 (Uniform Resource Identifier (URI): Generic Syntax), | ||||
|     /// and are not encoded. The other ASCII characters except "/" and ":" are percent-encoded, | ||||
|     /// and "\" is replaced by "/" on Windows. | ||||
|     /// | ||||
|     /// Section 4 of RFC 8089 (The "file" URI Scheme) does not mandate precise encoding | ||||
|     /// requirements for non-ASCII characters, and this implementation leaves them unencoded. | ||||
|     /// On Windows, the UrlCreateFromPathW function does not encode non-ASCII characters. | ||||
|     /// Doing so with UTF-8 encoded paths creates invalid file:// URLs on that platform. | ||||
|     fn encode(input: &[u8]) -> HyperlinkPath { | ||||
|         let mut result = Vec::with_capacity(input.len()); | ||||
|  | ||||
|         for &c in input { | ||||
|             match c { | ||||
|                 b'0'..=b'9' | ||||
|                 | b'A'..=b'Z' | ||||
|                 | b'a'..=b'z' | ||||
|                 | b'/' | ||||
|                 | b':' | ||||
|                 | b'-' | ||||
|                 | b'.' | ||||
|                 | b'_' | ||||
|                 | b'~' | ||||
|                 | 128.. => { | ||||
|                     result.push(c); | ||||
|                 } | ||||
|                 #[cfg(windows)] | ||||
|                 b'\\' => { | ||||
|                     result.push(b'/'); | ||||
|                 } | ||||
|                 _ => { | ||||
|                     const HEX: &[u8] = b"0123456789ABCDEF"; | ||||
|                     result.push(b'%'); | ||||
|                     result.push(HEX[(c >> 4) as usize]); | ||||
|                     result.push(HEX[(c & 0xF) as usize]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Self(result) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for HyperlinkPath { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}", | ||||
|             std::str::from_utf8(&self.0).unwrap_or("invalid utf-8") | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A simple abstraction over a hyperlink span written to the terminal. | ||||
| /// This helps tracking whether a hyperlink has been started, and should be ended. | ||||
| #[derive(Debug, Default)] | ||||
| pub struct HyperlinkSpan { | ||||
|     active: bool, | ||||
| } | ||||
|  | ||||
| impl HyperlinkSpan { | ||||
|     /// Starts a hyperlink and returns a span which tracks whether it is still in effect. | ||||
|     pub fn start( | ||||
|         wtr: &mut impl WriteColor, | ||||
|         hyperlink: &HyperlinkSpec, | ||||
|     ) -> io::Result<Self> { | ||||
|         if wtr.supports_hyperlinks() && hyperlink.uri().is_some() { | ||||
|             wtr.set_hyperlink(hyperlink)?; | ||||
|             Ok(HyperlinkSpan { active: true }) | ||||
|         } else { | ||||
|             Ok(HyperlinkSpan { active: false }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Ends the hyperlink span if it is active. | ||||
|     pub fn end(&mut self, wtr: &mut impl WriteColor) -> io::Result<()> { | ||||
|         if self.is_active() { | ||||
|             wtr.set_hyperlink(&HyperlinkSpec::close())?; | ||||
|             self.active = false; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Returns true if there is currently an active hyperlink. | ||||
|     pub fn is_active(&self) -> bool { | ||||
|         self.active | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|  | ||||
|     #[test] | ||||
|     fn build_pattern() { | ||||
|         let pattern = HyperlinkPatternBuilder::new() | ||||
|             .append_text(b"foo://") | ||||
|             .append_text(b"bar-") | ||||
|             .append_text(b"baz") | ||||
|             .append_file() | ||||
|             .build() | ||||
|             .unwrap(); | ||||
|  | ||||
|         assert_eq!(pattern.to_string(), "foo://bar-baz{file}"); | ||||
|         assert_eq!(pattern.parts[0], Part::Text(b"foo://bar-baz".to_vec())); | ||||
|         assert!(!pattern.is_empty()); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn build_empty_pattern() { | ||||
|         let pattern = HyperlinkPatternBuilder::new().build().unwrap(); | ||||
|  | ||||
|         assert!(pattern.is_empty()); | ||||
|         assert_eq!(pattern, HyperlinkPattern::empty()); | ||||
|         assert_eq!(pattern, HyperlinkPattern::default()); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn handle_alias() { | ||||
|         assert!(HyperlinkPattern::from_str("file").is_ok()); | ||||
|         assert!(HyperlinkPattern::from_str("none").is_ok()); | ||||
|         assert!(HyperlinkPattern::from_str("none").unwrap().is_empty()); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn parse_pattern() { | ||||
|         let pattern = HyperlinkPattern::from_str( | ||||
|             "foo://{host}/bar/{file}:{line}:{column}", | ||||
|         ) | ||||
|         .unwrap(); | ||||
|  | ||||
|         assert_eq!( | ||||
|             pattern.to_string(), | ||||
|             "foo://{host}/bar/{file}:{line}:{column}" | ||||
|                 .replace("{host}", &HyperlinkPatternBuilder::get_hostname()) | ||||
|         ); | ||||
|         assert_eq!(pattern.parts.len(), 6); | ||||
|         assert!(pattern.parts.contains(&Part::File)); | ||||
|         assert!(pattern.parts.contains(&Part::Line)); | ||||
|         assert!(pattern.parts.contains(&Part::Column)); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn parse_valid() { | ||||
|         assert!(HyperlinkPattern::from_str("").unwrap().is_empty()); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("foo://{file}").unwrap().to_string(), | ||||
|             "foo://{file}" | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("foo://{file}/bar") | ||||
|                 .unwrap() | ||||
|                 .to_string(), | ||||
|             "foo://{file}/bar" | ||||
|         ); | ||||
|  | ||||
|         HyperlinkPattern::from_str("f://{file}").unwrap(); | ||||
|         HyperlinkPattern::from_str("f:{file}").unwrap(); | ||||
|         HyperlinkPattern::from_str("f-+.:{file}").unwrap(); | ||||
|         HyperlinkPattern::from_str("f42:{file}").unwrap(); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn parse_invalid() { | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("foo://bar").unwrap_err(), | ||||
|             HyperlinkPatternError::NoFilePlaceholder | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("foo://{bar}").unwrap_err(), | ||||
|             HyperlinkPatternError::InvalidPlaceholder("bar".to_string()) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("foo://{file").unwrap_err(), | ||||
|             HyperlinkPatternError::InvalidSyntax | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("foo://{file}:{column}").unwrap_err(), | ||||
|             HyperlinkPatternError::NoLinePlaceholder | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("{file}").unwrap_err(), | ||||
|             HyperlinkPatternError::InvalidScheme | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str(":{file}").unwrap_err(), | ||||
|             HyperlinkPatternError::InvalidScheme | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             HyperlinkPattern::from_str("f*:{file}").unwrap_err(), | ||||
|             HyperlinkPatternError::InvalidScheme | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn aliases_are_valid() { | ||||
|         for (name, definition) in HYPERLINK_PATTERN_ALIASES { | ||||
|             assert!( | ||||
|                 HyperlinkPattern::from_str(definition).is_ok(), | ||||
|                 "invalid hyperlink alias: {}", | ||||
|                 name | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn aliases_are_sorted() { | ||||
|         let mut names = HYPERLINK_PATTERN_ALIASES.iter().map(|(name, _)| name); | ||||
|  | ||||
|         let Some(mut previous_name) = names.next() else { | ||||
|             return; | ||||
|         }; | ||||
|  | ||||
|         for name in names { | ||||
|             assert!( | ||||
|                 name > previous_name, | ||||
|                 r#""{}" should be sorted before "{}" in `HYPERLINK_PATTERN_ALIASES`"#, | ||||
|                 name, | ||||
|                 previous_name | ||||
|             ); | ||||
|  | ||||
|             previous_name = name; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								crates/printer/src/hyperlink_aliases.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								crates/printer/src/hyperlink_aliases.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| /// Aliases to well-known hyperlink schemes. | ||||
| /// | ||||
| /// These need to be sorted by name. | ||||
| pub const HYPERLINK_PATTERN_ALIASES: &[(&str, &str)] = &[ | ||||
|     #[cfg(unix)] | ||||
|     ("file", "file://{host}/{file}"), | ||||
|     #[cfg(windows)] | ||||
|     ("file", "file:///{file}"), | ||||
|     // https://github.com/misaki-web/grepp | ||||
|     ("grep+", "grep+:///{file}:{line}"), | ||||
|     ("kitty", "file://{host}/{file}#{line}"), | ||||
|     // https://macvim.org/docs/gui_mac.txt.html#mvim%3A%2F%2F | ||||
|     ("macvim", "mvim://open?url=file:///{file}&line={line}&column={column}"), | ||||
|     ("none", ""), | ||||
|     // https://github.com/inopinatus/sublime_url | ||||
|     ("subl", "subl://open?url=file:///{file}&line={line}&column={column}"), | ||||
|     // https://macromates.com/blog/2007/the-textmate-url-scheme/ | ||||
|     ("textmate", "txmt://open?url=file:///{file}&line={line}&column={column}"), | ||||
|     // https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls | ||||
|     ("vscode", "vscode://file/{file}:{line}:{column}"), | ||||
|     ("vscode-insiders", "vscode-insiders://file/{file}:{line}:{column}"), | ||||
|     ("vscodium", "vscodium://file/{file}:{line}:{column}"), | ||||
| ]; | ||||
| @@ -67,6 +67,10 @@ fn example() -> Result<(), Box<Error>> { | ||||
| pub use crate::color::{ | ||||
|     default_color_specs, ColorError, ColorSpecs, UserColorSpec, | ||||
| }; | ||||
| pub use crate::hyperlink::{ | ||||
|     HyperlinkPath, HyperlinkPattern, HyperlinkPatternError, HyperlinkSpan, | ||||
|     HyperlinkValues, | ||||
| }; | ||||
| #[cfg(feature = "serde1")] | ||||
| pub use crate::json::{JSONBuilder, JSONSink, JSON}; | ||||
| pub use crate::standard::{Standard, StandardBuilder, StandardSink}; | ||||
| @@ -90,6 +94,8 @@ mod macros; | ||||
|  | ||||
| mod color; | ||||
| mod counter; | ||||
| mod hyperlink; | ||||
| mod hyperlink_aliases; | ||||
| #[cfg(feature = "serde1")] | ||||
| mod json; | ||||
| #[cfg(feature = "serde1")] | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor}; | ||||
|  | ||||
| use crate::color::ColorSpecs; | ||||
| use crate::counter::CounterWriter; | ||||
| use crate::hyperlink::{HyperlinkPattern, HyperlinkSpan}; | ||||
| use crate::stats::Stats; | ||||
| use crate::util::{ | ||||
|     find_iter_at_in_context, trim_ascii_prefix, trim_line_terminator, | ||||
| @@ -29,6 +30,7 @@ use crate::util::{ | ||||
| #[derive(Debug, Clone)] | ||||
| struct Config { | ||||
|     colors: ColorSpecs, | ||||
|     hyperlink_pattern: HyperlinkPattern, | ||||
|     stats: bool, | ||||
|     heading: bool, | ||||
|     path: bool, | ||||
| @@ -54,6 +56,7 @@ impl Default for Config { | ||||
|     fn default() -> Config { | ||||
|         Config { | ||||
|             colors: ColorSpecs::default(), | ||||
|             hyperlink_pattern: HyperlinkPattern::default(), | ||||
|             stats: false, | ||||
|             heading: false, | ||||
|             path: true, | ||||
| @@ -122,6 +125,7 @@ impl StandardBuilder { | ||||
|         Standard { | ||||
|             config: self.config.clone(), | ||||
|             wtr: RefCell::new(CounterWriter::new(wtr)), | ||||
|             buf: RefCell::new(vec![]), | ||||
|             matches: vec![], | ||||
|         } | ||||
|     } | ||||
| @@ -160,6 +164,17 @@ impl StandardBuilder { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the hyperlink pattern to use for hyperlinks output by this printer. | ||||
|     /// | ||||
|     /// Colors need to be enabled for hyperlinks to be output. | ||||
|     pub fn hyperlink_pattern( | ||||
|         &mut self, | ||||
|         pattern: HyperlinkPattern, | ||||
|     ) -> &mut StandardBuilder { | ||||
|         self.config.hyperlink_pattern = pattern; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Enable the gathering of various aggregate statistics. | ||||
|     /// | ||||
|     /// When this is enabled (it's disabled by default), statistics will be | ||||
| @@ -467,6 +482,7 @@ impl StandardBuilder { | ||||
| pub struct Standard<W> { | ||||
|     config: Config, | ||||
|     wtr: RefCell<CounterWriter<W>>, | ||||
|     buf: RefCell<Vec<u8>>, | ||||
|     matches: Vec<Match>, | ||||
| } | ||||
|  | ||||
| @@ -1209,23 +1225,25 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|         line_number: Option<u64>, | ||||
|         column: Option<u64>, | ||||
|     ) -> io::Result<()> { | ||||
|         let sep = self.separator_field(); | ||||
|         let mut prelude = PreludeWriter::new(self); | ||||
|         prelude.start(line_number, column)?; | ||||
|  | ||||
|         if !self.config().heading { | ||||
|             self.write_path_field(sep)?; | ||||
|             prelude.write_path()?; | ||||
|         } | ||||
|         if let Some(n) = line_number { | ||||
|             self.write_line_number(n, sep)?; | ||||
|             prelude.write_line_number(n)?; | ||||
|         } | ||||
|         if let Some(n) = column { | ||||
|             if self.config().column { | ||||
|                 self.write_column_number(n, sep)?; | ||||
|                 prelude.write_column_number(n)?; | ||||
|             } | ||||
|         } | ||||
|         if self.config().byte_offset { | ||||
|             self.write_byte_offset(absolute_byte_offset, sep)?; | ||||
|             prelude.write_byte_offset(absolute_byte_offset)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|  | ||||
|         prelude.end() | ||||
|     } | ||||
|  | ||||
|     #[inline(always)] | ||||
| @@ -1386,7 +1404,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|     /// terminator.) | ||||
|     fn write_path_line(&self) -> io::Result<()> { | ||||
|         if let Some(path) = self.path() { | ||||
|             self.write_spec(self.config().colors.path(), path.as_bytes())?; | ||||
|             self.write_path_hyperlink(path)?; | ||||
|             if let Some(term) = self.config().path_terminator { | ||||
|                 self.write(&[term])?; | ||||
|             } else { | ||||
| @@ -1396,22 +1414,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// If this printer has a file path associated with it, then this will | ||||
|     /// 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 | ||||
|     /// the field separator.) | ||||
|     fn write_path_field(&self, field_separator: &[u8]) -> io::Result<()> { | ||||
|         if let Some(path) = self.path() { | ||||
|             self.write_spec(self.config().colors.path(), path.as_bytes())?; | ||||
|             if let Some(term) = self.config().path_terminator { | ||||
|                 self.write(&[term])?; | ||||
|             } else { | ||||
|                 self.write(field_separator)?; | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_search_prelude(&self) -> io::Result<()> { | ||||
|         let this_search_written = self.wtr().borrow().count() > 0; | ||||
|         if this_search_written { | ||||
| @@ -1438,7 +1440,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|         let bin = self.searcher.binary_detection(); | ||||
|         if let Some(byte) = bin.quit_byte() { | ||||
|             if let Some(path) = self.path() { | ||||
|                 self.write_spec(self.config().colors.path(), path.as_bytes())?; | ||||
|                 self.write_path_hyperlink(path)?; | ||||
|                 self.write(b": ")?; | ||||
|             } | ||||
|             let remainder = format!( | ||||
| @@ -1450,7 +1452,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|             self.write(remainder.as_bytes())?; | ||||
|         } else if let Some(byte) = bin.convert_byte() { | ||||
|             if let Some(path) = self.path() { | ||||
|                 self.write_spec(self.config().colors.path(), path.as_bytes())?; | ||||
|                 self.write_path_hyperlink(path)?; | ||||
|                 self.write(b": ")?; | ||||
|             } | ||||
|             let remainder = format!( | ||||
| @@ -1471,39 +1473,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_line_number( | ||||
|         &self, | ||||
|         line_number: u64, | ||||
|         field_separator: &[u8], | ||||
|     ) -> io::Result<()> { | ||||
|         let n = line_number.to_string(); | ||||
|         self.write_spec(self.config().colors.line(), n.as_bytes())?; | ||||
|         self.write(field_separator)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_column_number( | ||||
|         &self, | ||||
|         column_number: u64, | ||||
|         field_separator: &[u8], | ||||
|     ) -> io::Result<()> { | ||||
|         let n = column_number.to_string(); | ||||
|         self.write_spec(self.config().colors.column(), n.as_bytes())?; | ||||
|         self.write(field_separator)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_byte_offset( | ||||
|         &self, | ||||
|         offset: u64, | ||||
|         field_separator: &[u8], | ||||
|     ) -> io::Result<()> { | ||||
|         let n = offset.to_string(); | ||||
|         self.write_spec(self.config().colors.column(), n.as_bytes())?; | ||||
|         self.write(field_separator)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_line_term(&self) -> io::Result<()> { | ||||
|         self.write(self.searcher.line_terminator().as_bytes()) | ||||
|     } | ||||
| @@ -1516,6 +1485,40 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_path(&self, path: &PrinterPath) -> io::Result<()> { | ||||
|         let mut wtr = self.wtr().borrow_mut(); | ||||
|         wtr.set_color(self.config().colors.path())?; | ||||
|         wtr.write_all(path.as_bytes())?; | ||||
|         wtr.reset() | ||||
|     } | ||||
|  | ||||
|     fn write_path_hyperlink(&self, path: &PrinterPath) -> io::Result<()> { | ||||
|         let mut hyperlink = self.start_hyperlink_span(path, None, None)?; | ||||
|         self.write_path(path)?; | ||||
|         hyperlink.end(&mut *self.wtr().borrow_mut()) | ||||
|     } | ||||
|  | ||||
|     fn start_hyperlink_span( | ||||
|         &self, | ||||
|         path: &PrinterPath, | ||||
|         line_number: Option<u64>, | ||||
|         column: Option<u64>, | ||||
|     ) -> io::Result<HyperlinkSpan> { | ||||
|         let mut wtr = self.wtr().borrow_mut(); | ||||
|         if wtr.supports_hyperlinks() { | ||||
|             let mut buf = self.buf().borrow_mut(); | ||||
|             if let Some(spec) = path.create_hyperlink_spec( | ||||
|                 &self.config().hyperlink_pattern, | ||||
|                 line_number, | ||||
|                 column, | ||||
|                 &mut buf, | ||||
|             ) { | ||||
|                 return HyperlinkSpan::start(&mut *wtr, &spec); | ||||
|             } | ||||
|         } | ||||
|         Ok(HyperlinkSpan::default()) | ||||
|     } | ||||
|  | ||||
|     fn start_color_match(&self) -> io::Result<()> { | ||||
|         if self.in_color_match.get() { | ||||
|             return Ok(()); | ||||
| @@ -1569,6 +1572,12 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|         &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. | ||||
|     fn path(&self) -> Option<&'a PrinterPath<'a>> { | ||||
|         self.sink.path.as_ref() | ||||
| @@ -1615,6 +1624,139 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A writer for the prelude (the beginning part of a matching line). | ||||
| /// | ||||
| /// This encapsulates the state needed to print the prelude. | ||||
| struct PreludeWriter<'a, M: Matcher, W> { | ||||
|     std: &'a StandardImpl<'a, M, W>, | ||||
|     next_separator: PreludeSeparator, | ||||
|     field_separator: &'a [u8], | ||||
|     hyperlink: HyperlinkSpan, | ||||
| } | ||||
|  | ||||
| /// A type of separator used in the prelude | ||||
| enum PreludeSeparator { | ||||
|     /// No separator. | ||||
|     None, | ||||
|     /// The field separator, either for a matching or contextual line. | ||||
|     FieldSeparator, | ||||
|     /// The path terminator. | ||||
|     PathTerminator, | ||||
| } | ||||
|  | ||||
| impl<'a, M: Matcher, W: WriteColor> PreludeWriter<'a, M, W> { | ||||
|     /// Creates a new prelude printer. | ||||
|     fn new(std: &'a StandardImpl<'a, M, W>) -> PreludeWriter<'a, M, W> { | ||||
|         Self { | ||||
|             std, | ||||
|             next_separator: PreludeSeparator::None, | ||||
|             field_separator: std.separator_field(), | ||||
|             hyperlink: HyperlinkSpan::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Starts the prelude with a hyperlink when applicable. | ||||
|     /// | ||||
|     /// If a heading was written, and the hyperlink pattern is invariant on 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 be sufficient and less confusing. | ||||
|     fn start( | ||||
|         &mut self, | ||||
|         line_number: Option<u64>, | ||||
|         column: Option<u64>, | ||||
|     ) -> io::Result<()> { | ||||
|         if let Some(path) = self.std.path() { | ||||
|             if self.config().hyperlink_pattern.is_line_dependent() | ||||
|                 || !self.config().heading | ||||
|             { | ||||
|                 self.hyperlink = self.std.start_hyperlink_span( | ||||
|                     path, | ||||
|                     line_number, | ||||
|                     column, | ||||
|                 )?; | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Ends the prelude and writes the remaining output. | ||||
|     fn end(&mut self) -> io::Result<()> { | ||||
|         if self.hyperlink.is_active() { | ||||
|             self.hyperlink.end(&mut *self.std.wtr().borrow_mut())?; | ||||
|         } | ||||
|         self.write_separator() | ||||
|     } | ||||
|  | ||||
|     /// If this printer has a file path associated with it, then this will | ||||
|     /// 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 | ||||
|     /// the field separator.) | ||||
|     fn write_path(&mut self) -> io::Result<()> { | ||||
|         if let Some(path) = self.std.path() { | ||||
|             self.write_separator()?; | ||||
|             self.std.write_path(path)?; | ||||
|  | ||||
|             self.next_separator = if self.config().path_terminator.is_some() { | ||||
|                 PreludeSeparator::PathTerminator | ||||
|             } else { | ||||
|                 PreludeSeparator::FieldSeparator | ||||
|             }; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Writes the line number field. | ||||
|     fn write_line_number(&mut self, line_number: u64) -> io::Result<()> { | ||||
|         self.write_separator()?; | ||||
|         let n = line_number.to_string(); | ||||
|         self.std.write_spec(self.config().colors.line(), n.as_bytes())?; | ||||
|         self.next_separator = PreludeSeparator::FieldSeparator; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Writes the column number field. | ||||
|     fn write_column_number(&mut self, column_number: u64) -> io::Result<()> { | ||||
|         self.write_separator()?; | ||||
|         let n = column_number.to_string(); | ||||
|         self.std.write_spec(self.config().colors.column(), n.as_bytes())?; | ||||
|         self.next_separator = PreludeSeparator::FieldSeparator; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Writes the byte offset field. | ||||
|     fn write_byte_offset(&mut self, offset: u64) -> io::Result<()> { | ||||
|         self.write_separator()?; | ||||
|         let n = offset.to_string(); | ||||
|         self.std.write_spec(self.config().colors.column(), n.as_bytes())?; | ||||
|         self.next_separator = PreludeSeparator::FieldSeparator; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Writes the separator defined by the preceding field. | ||||
|     /// | ||||
|     /// This is called before writing the contents of a field, and at | ||||
|     /// the end of the prelude. | ||||
|     fn write_separator(&mut self) -> io::Result<()> { | ||||
|         match self.next_separator { | ||||
|             PreludeSeparator::None => {} | ||||
|             PreludeSeparator::FieldSeparator => { | ||||
|                 self.std.write(self.field_separator)?; | ||||
|             } | ||||
|             PreludeSeparator::PathTerminator => { | ||||
|                 if let Some(term) = self.config().path_terminator { | ||||
|                     self.std.write(&[term])?; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         self.next_separator = PreludeSeparator::None; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn config(&self) -> &Config { | ||||
|         self.std.config() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use grep_matcher::LineTerminator; | ||||
|   | ||||
| @@ -10,6 +10,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor}; | ||||
|  | ||||
| use crate::color::ColorSpecs; | ||||
| use crate::counter::CounterWriter; | ||||
| use crate::hyperlink::{HyperlinkPattern, HyperlinkSpan}; | ||||
| use crate::stats::Stats; | ||||
| use crate::util::{find_iter_at_in_context, PrinterPath}; | ||||
|  | ||||
| @@ -22,6 +23,7 @@ use crate::util::{find_iter_at_in_context, PrinterPath}; | ||||
| struct Config { | ||||
|     kind: SummaryKind, | ||||
|     colors: ColorSpecs, | ||||
|     hyperlink_pattern: HyperlinkPattern, | ||||
|     stats: bool, | ||||
|     path: bool, | ||||
|     max_matches: Option<u64>, | ||||
| @@ -36,6 +38,7 @@ impl Default for Config { | ||||
|         Config { | ||||
|             kind: SummaryKind::Count, | ||||
|             colors: ColorSpecs::default(), | ||||
|             hyperlink_pattern: HyperlinkPattern::default(), | ||||
|             stats: false, | ||||
|             path: true, | ||||
|             max_matches: None, | ||||
| @@ -160,6 +163,7 @@ impl SummaryBuilder { | ||||
|         Summary { | ||||
|             config: self.config.clone(), | ||||
|             wtr: RefCell::new(CounterWriter::new(wtr)), | ||||
|             buf: vec![], | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -206,6 +210,17 @@ impl SummaryBuilder { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the hyperlink pattern to use for hyperlinks output by this printer. | ||||
|     /// | ||||
|     /// Colors need to be enabled for hyperlinks to be output. | ||||
|     pub fn hyperlink_pattern( | ||||
|         &mut self, | ||||
|         pattern: HyperlinkPattern, | ||||
|     ) -> &mut SummaryBuilder { | ||||
|         self.config.hyperlink_pattern = pattern; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Enable the gathering of various aggregate statistics. | ||||
|     /// | ||||
|     /// When this is enabled (it's disabled by default), statistics will be | ||||
| @@ -328,6 +343,7 @@ impl SummaryBuilder { | ||||
| pub struct Summary<W> { | ||||
|     config: Config, | ||||
|     wtr: RefCell<CounterWriter<W>>, | ||||
|     buf: Vec<u8>, | ||||
| } | ||||
|  | ||||
| impl<W: WriteColor> Summary<W> { | ||||
| @@ -532,12 +548,9 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> { | ||||
|     /// write that path to the underlying writer followed by a line terminator. | ||||
|     /// (If a path terminator is set, then that is used instead of the line | ||||
|     /// terminator.) | ||||
|     fn write_path_line(&self, searcher: &Searcher) -> io::Result<()> { | ||||
|         if let Some(ref path) = self.path { | ||||
|             self.write_spec( | ||||
|                 self.summary.config.colors.path(), | ||||
|                 path.as_bytes(), | ||||
|             )?; | ||||
|     fn write_path_line(&mut self, searcher: &Searcher) -> io::Result<()> { | ||||
|         if self.path.is_some() { | ||||
|             self.write_path()?; | ||||
|             if let Some(term) = self.summary.config.path_terminator { | ||||
|                 self.write(&[term])?; | ||||
|             } else { | ||||
| @@ -551,12 +564,9 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> { | ||||
|     /// write that path to the underlying writer followed by the field | ||||
|     /// separator. (If a path terminator is set, then that is used instead of | ||||
|     /// the field separator.) | ||||
|     fn write_path_field(&self) -> io::Result<()> { | ||||
|         if let Some(ref path) = self.path { | ||||
|             self.write_spec( | ||||
|                 self.summary.config.colors.path(), | ||||
|                 path.as_bytes(), | ||||
|             )?; | ||||
|     fn write_path_field(&mut self) -> io::Result<()> { | ||||
|         if self.path.is_some() { | ||||
|             self.write_path()?; | ||||
|             if let Some(term) = self.summary.config.path_terminator { | ||||
|                 self.write(&[term])?; | ||||
|             } else { | ||||
| @@ -566,6 +576,43 @@ impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// If this printer has a file path associated with it, then this will | ||||
|     /// write that path to the underlying writer in the appropriate style | ||||
|     /// (color and hyperlink). | ||||
|     fn write_path(&mut self) -> io::Result<()> { | ||||
|         if self.path.is_some() { | ||||
|             let mut hyperlink = self.start_hyperlink_span()?; | ||||
|  | ||||
|             self.write_spec( | ||||
|                 self.summary.config.colors.path(), | ||||
|                 self.path.as_ref().unwrap().as_bytes(), | ||||
|             )?; | ||||
|  | ||||
|             if hyperlink.is_active() { | ||||
|                 hyperlink.end(&mut *self.summary.wtr.borrow_mut())?; | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Starts a hyperlink span when applicable. | ||||
|     fn start_hyperlink_span(&mut self) -> io::Result<HyperlinkSpan> { | ||||
|         if let Some(ref path) = self.path { | ||||
|             let mut wtr = self.summary.wtr.borrow_mut(); | ||||
|             if wtr.supports_hyperlinks() { | ||||
|                 if let Some(spec) = path.create_hyperlink_spec( | ||||
|                     &self.summary.config.hyperlink_pattern, | ||||
|                     None, | ||||
|                     None, | ||||
|                     &mut self.summary.buf, | ||||
|                 ) { | ||||
|                     return Ok(HyperlinkSpan::start(&mut *wtr, &spec)?); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Ok(HyperlinkSpan::default()) | ||||
|     } | ||||
|  | ||||
|     /// Write the line terminator configured on the given searcher. | ||||
|     fn write_line_term(&self, searcher: &Searcher) -> io::Result<()> { | ||||
|         self.write(searcher.line_terminator().as_bytes()) | ||||
| @@ -704,11 +751,11 @@ impl<'p, 's, M: Matcher, W: WriteColor> Sink for SummarySink<'p, 's, M, W> { | ||||
|             } | ||||
|             SummaryKind::CountMatches => { | ||||
|                 if show_count { | ||||
|                     self.write_path_field()?; | ||||
|                     let stats = self | ||||
|                         .stats | ||||
|                         .as_ref() | ||||
|                         .expect("CountMatches should enable stats tracking"); | ||||
|                     self.write_path_field()?; | ||||
|                     self.write(stats.matches().to_string().as_bytes())?; | ||||
|                     self.write_line_term(searcher)?; | ||||
|                 } | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| use std::borrow::Cow; | ||||
| use std::fmt; | ||||
| use std::io; | ||||
| use std::cell::OnceCell; | ||||
| use std::path::Path; | ||||
| use std::time; | ||||
| use std::{fmt, io}; | ||||
|  | ||||
| use bstr::{ByteSlice, ByteVec}; | ||||
| use grep_matcher::{Captures, LineTerminator, Match, Matcher}; | ||||
| @@ -11,7 +11,9 @@ use grep_searcher::{ | ||||
| }; | ||||
| #[cfg(feature = "serde1")] | ||||
| use serde::{Serialize, Serializer}; | ||||
| use termcolor::HyperlinkSpec; | ||||
|  | ||||
| use crate::hyperlink::{HyperlinkPath, HyperlinkPattern, HyperlinkValues}; | ||||
| use crate::MAX_LOOK_AHEAD; | ||||
|  | ||||
| /// A type for handling replacements while amortizing allocation. | ||||
| @@ -276,12 +278,20 @@ impl<'a> Sunk<'a> { | ||||
| /// portability with a small cost: on Windows, paths that are not valid UTF-16 | ||||
| /// will not roundtrip correctly. | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct PrinterPath<'a>(Cow<'a, [u8]>); | ||||
| pub struct PrinterPath<'a> { | ||||
|     path: &'a Path, | ||||
|     bytes: Cow<'a, [u8]>, | ||||
|     hyperlink_path: OnceCell<Option<HyperlinkPath>>, | ||||
| } | ||||
|  | ||||
| impl<'a> PrinterPath<'a> { | ||||
|     /// Create a new path suitable for printing. | ||||
|     pub fn new(path: &'a Path) -> PrinterPath<'a> { | ||||
|         PrinterPath(Vec::from_path_lossy(path)) | ||||
|         PrinterPath { | ||||
|             path, | ||||
|             bytes: Vec::from_path_lossy(path), | ||||
|             hyperlink_path: OnceCell::new(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Create a new printer path from the given path which can be efficiently | ||||
| @@ -303,7 +313,7 @@ impl<'a> PrinterPath<'a> { | ||||
|     /// environments, only `/` is treated as a path separator. | ||||
|     fn replace_separator(&mut self, new_sep: u8) { | ||||
|         let transformed_path: Vec<u8> = self | ||||
|             .0 | ||||
|             .as_bytes() | ||||
|             .bytes() | ||||
|             .map(|b| { | ||||
|                 if b == b'/' || (cfg!(windows) && b == b'\\') { | ||||
| @@ -313,12 +323,40 @@ impl<'a> PrinterPath<'a> { | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|         self.0 = Cow::Owned(transformed_path); | ||||
|         self.bytes = Cow::Owned(transformed_path); | ||||
|     } | ||||
|  | ||||
|     /// Return the raw bytes for this path. | ||||
|     pub fn as_bytes(&self) -> &[u8] { | ||||
|         &self.0 | ||||
|         &self.bytes | ||||
|     } | ||||
|  | ||||
|     /// Creates a hyperlink for this path and the given line and column, using the specified | ||||
|     /// pattern. Uses the given buffer to store the hyperlink. | ||||
|     pub fn create_hyperlink_spec<'b>( | ||||
|         &self, | ||||
|         pattern: &HyperlinkPattern, | ||||
|         line_number: Option<u64>, | ||||
|         column: Option<u64>, | ||||
|         buffer: &'b mut Vec<u8>, | ||||
|     ) -> Option<HyperlinkSpec<'b>> { | ||||
|         if pattern.is_empty() { | ||||
|             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. | ||||
|     /// | ||||
|     /// This is what the {file} placeholder will be substituted with. | ||||
|     fn hyperlink_path(&self) -> Option<&HyperlinkPath> { | ||||
|         self.hyperlink_path | ||||
|             .get_or_init(|| HyperlinkPath::from_path(self.path)) | ||||
|             .as_ref() | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -380,6 +380,7 @@ rgtest!(r428_color_context_path, |dir: Dir, mut cmd: TestCommand| { | ||||
|         "-N", | ||||
|         "--colors=match:none", | ||||
|         "--color=always", | ||||
|         "--hyperlink-format=", | ||||
|         "foo", | ||||
|     ]); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user