Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make browser links out of HTML file paths #12889

Merged
merged 1 commit into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ serde_json = "1.0.108"
sha1 = "0.10.6"
sha2 = "0.10.8"
shell-escape = "0.1.5"
supports-hyperlinks = "2.1.0"
snapbox = { version = "0.4.14", features = ["diff", "path"] }
syn = { version = "2.0.38", features = ["extra-traits", "full"] }
tar = { version = "0.4.40", default-features = false }
Expand Down Expand Up @@ -173,6 +174,7 @@ serde_ignored.workspace = true
serde_json = { workspace = true, features = ["raw_value"] }
sha1.workspace = true
shell-escape.workspace = true
supports-hyperlinks.workspace = true
syn.workspace = true
tar.workspace = true
tempfile.workspace = true
Expand Down
23 changes: 13 additions & 10 deletions src/cargo/core/compiler/timings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,18 +339,21 @@ impl<'cfg> Timings<'cfg> {
include_str!("timings.js")
)?;
drop(f);
let msg = format!(
"report saved to {}",
std::env::current_dir()
.unwrap_or_default()
.join(&filename)
.display()
);

let unstamped_filename = timings_path.join("cargo-timing.html");
paths::link_or_copy(&filename, &unstamped_filename)?;
self.config
.shell()
.status_with_color("Timing", msg, &style::NOTE)?;

let mut shell = self.config.shell();
let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
let link = shell.err_file_hyperlink(&timing_path);
let msg = format!(
"report saved to {}{}{}",
link.open(),
timing_path.display(),
link.close()
);
shell.status_with_color("Timing", msg, &style::NOTE)?;

Ok(())
}

Expand Down
109 changes: 109 additions & 0 deletions src/cargo/core/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use anstream::AutoStream;
use anstyle::Style;

use crate::util::errors::CargoResult;
use crate::util::hostname;
use crate::util::style::*;

pub enum TtyWidth {
Expand Down Expand Up @@ -57,6 +58,7 @@ pub struct Shell {
/// Flag that indicates the current line needs to be cleared before
/// printing. Used when a progress bar is currently displayed.
needs_clear: bool,
hostname: Option<String>,
}

impl fmt::Debug for Shell {
Expand Down Expand Up @@ -85,6 +87,7 @@ enum ShellOut {
stderr: AutoStream<std::io::Stderr>,
stderr_tty: bool,
color_choice: ColorChoice,
hyperlinks: bool,
},
}

Expand All @@ -111,10 +114,12 @@ impl Shell {
stdout: AutoStream::new(std::io::stdout(), stdout_choice),
stderr: AutoStream::new(std::io::stderr(), stderr_choice),
color_choice: auto_clr,
hyperlinks: supports_hyperlinks(),
stderr_tty: std::io::stderr().is_terminal(),
},
verbosity: Verbosity::Verbose,
needs_clear: false,
hostname: None,
}
}

Expand All @@ -124,6 +129,7 @@ impl Shell {
output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write
verbosity: Verbosity::Verbose,
needs_clear: false,
hostname: None,
}
}

Expand Down Expand Up @@ -314,6 +320,16 @@ impl Shell {
Ok(())
}

pub fn set_hyperlinks(&mut self, yes: bool) -> CargoResult<()> {
if let ShellOut::Stream {
ref mut hyperlinks, ..
} = self.output
{
*hyperlinks = yes;
}
Ok(())
}

/// Gets the current color choice.
///
/// If we are not using a color stream, this will always return `Never`, even if the color
Expand All @@ -340,6 +356,61 @@ impl Shell {
}
}

pub fn out_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
let supports_hyperlinks = match &self.output {
ShellOut::Write(_) => false,
ShellOut::Stream {
stdout, hyperlinks, ..
} => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
};
Hyperlink {
url: supports_hyperlinks.then_some(url),
}
}

pub fn err_hyperlink<D: fmt::Display>(&self, url: D) -> Hyperlink<D> {
let supports_hyperlinks = match &self.output {
ShellOut::Write(_) => false,
ShellOut::Stream {
stderr, hyperlinks, ..
} => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks,
};
if supports_hyperlinks {
Hyperlink { url: Some(url) }
} else {
Hyperlink { url: None }
}
}

pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
let url = self.file_hyperlink(path);
url.map(|u| self.out_hyperlink(u)).unwrap_or_default()
}

pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink<url::Url> {
epage marked this conversation as resolved.
Show resolved Hide resolved
let url = self.file_hyperlink(path);
url.map(|u| self.err_hyperlink(u)).unwrap_or_default()
}

fn file_hyperlink(&mut self, path: &std::path::Path) -> Option<url::Url> {
let mut url = url::Url::from_file_path(path).ok()?;
// Do a best-effort of setting the host in the URL to avoid issues with opening a link
// scoped to the computer you've SSHed into
let hostname = if cfg!(windows) {
// Not supported correctly on windows
None
} else {
if let Some(hostname) = self.hostname.as_deref() {
Some(hostname)
} else {
self.hostname = hostname().ok().and_then(|h| h.into_string().ok());
self.hostname.as_deref()
}
};
let _ = url.set_host(hostname);
Some(url)
}

/// Prints a message to stderr and translates ANSI escape code into console colors.
pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> {
if self.needs_clear {
Expand Down Expand Up @@ -439,6 +510,44 @@ fn supports_color(choice: anstream::ColorChoice) -> bool {
}
}

fn supports_hyperlinks() -> bool {
#[allow(clippy::disallowed_methods)] // We are reading the state of the system, not config
if std::env::var_os("TERM_PROGRAM").as_deref() == Some(std::ffi::OsStr::new("iTerm.app")) {
// Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2
return false;
}

supports_hyperlinks::supports_hyperlinks()
}

pub struct Hyperlink<D: fmt::Display> {
url: Option<D>,
}

impl<D: fmt::Display> Default for Hyperlink<D> {
fn default() -> Self {
Self { url: None }
}
}

impl<D: fmt::Display> Hyperlink<D> {
pub fn open(&self) -> impl fmt::Display {
if let Some(url) = self.url.as_ref() {
format!("\x1B]8;;{url}\x1B\\")
} else {
String::new()
}
}

pub fn close(&self) -> impl fmt::Display {
if self.url.is_some() {
"\x1B]8;;\x1B\\"
} else {
""
}
}
}

#[cfg(unix)]
mod imp {
use super::{Shell, TtyWidth};
Expand Down
12 changes: 10 additions & 2 deletions src/cargo/ops/cargo_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> {
cfg.map(|path_args| (path_args.path.resolve_program(ws.config()), path_args.args))
};
let mut shell = ws.config().shell();
shell.status("Opening", path.display())?;
let link = shell.err_file_hyperlink(&path);
shell.status(
"Opening",
format!("{}{}{}", link.open(), path.display(), link.close()),
)?;
open_docs(&path, &mut shell, config_browser, ws.config())?;
}
} else {
Expand All @@ -47,7 +51,11 @@ pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> {
.join("index.html");
if path.exists() {
let mut shell = ws.config().shell();
shell.status("Generated", path.display())?;
let link = shell.err_file_hyperlink(&path);
shell.status(
"Generated",
epage marked this conversation as resolved.
Show resolved Hide resolved
format!("{}{}{}", link.open(), path.display(), link.close()),
)?;
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/cargo/util/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,9 @@ impl Config {

self.shell().set_verbosity(verbosity);
self.shell().set_color_choice(color)?;
if let Some(hyperlinks) = term.hyperlinks {
self.shell().set_hyperlinks(hyperlinks)?;
}
self.progress_config = term.progress.unwrap_or_default();
self.extra_verbose = extra_verbose;
self.frozen = frozen;
Expand Down Expand Up @@ -2560,6 +2563,7 @@ struct TermConfig {
verbose: Option<bool>,
quiet: Option<bool>,
color: Option<String>,
hyperlinks: Option<bool>,
#[serde(default)]
#[serde(deserialize_with = "progress_or_string")]
progress: Option<ProgressConfig>,
Expand Down
77 changes: 77 additions & 0 deletions src/cargo/util/hostname.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copied from https://github.com/BurntSushi/ripgrep/blob/7099e174acbcbd940f57e4ab4913fee4040c826e/crates/cli/src/hostname.rs

use std::{ffi::OsString, io};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from ripgrep because the hostname and gethostname packages had what looked like less robust implementations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we note this in code comment, so that people can update this copy when needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/// Returns the hostname of the current system.
///
/// It is unusual, although technically possible, for this routine to return
/// an error. It is difficult to list out the error conditions, but one such
/// possibility is platform support.
///
/// # Platform specific behavior
///
/// On Unix, this returns the result of the `gethostname` function from the
/// `libc` linked into the program.
pub fn hostname() -> io::Result<OsString> {
#[cfg(unix)]
{
gethostname()
}
#[cfg(not(unix))]
{
Err(io::Error::new(
io::ErrorKind::Other,
"hostname could not be found on unsupported platform",
))
}
}

#[cfg(unix)]
fn gethostname() -> io::Result<OsString> {
use std::os::unix::ffi::OsStringExt;

// SAFETY: There don't appear to be any safety requirements for calling
// sysconf.
let limit = unsafe { libc::sysconf(libc::_SC_HOST_NAME_MAX) };
if limit == -1 {
// It is in theory possible for sysconf to return -1 for a limit but
// *not* set errno, in which case, io::Error::last_os_error is
// indeterminate. But untangling that is super annoying because std
// doesn't expose any unix-specific APIs for inspecting the errno. (We
// could do it ourselves, but it just doesn't seem worth doing?)
return Err(io::Error::last_os_error());
}
let Ok(maxlen) = usize::try_from(limit) else {
let msg = format!("host name max limit ({}) overflowed usize", limit);
return Err(io::Error::new(io::ErrorKind::Other, msg));
};
// maxlen here includes the NUL terminator.
let mut buf = vec![0; maxlen];
// SAFETY: The pointer we give is valid as it is derived directly from a
// Vec. Similarly, `maxlen` is the length of our Vec, and is thus valid
// to write to.
let rc = unsafe { libc::gethostname(buf.as_mut_ptr().cast::<libc::c_char>(), maxlen) };
if rc == -1 {
return Err(io::Error::last_os_error());
}
// POSIX says that if the hostname is bigger than `maxlen`, then it may
// write a truncate name back that is not necessarily NUL terminated (wtf,
// lol). So if we can't find a NUL terminator, then just give up.
let Some(zeropos) = buf.iter().position(|&b| b == 0) else {
let msg = "could not find NUL terminator in hostname";
return Err(io::Error::new(io::ErrorKind::Other, msg));
};
buf.truncate(zeropos);
buf.shrink_to_fit();
Ok(OsString::from_vec(buf))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn print_hostname() {
println!("{:?}", hostname());
}
}
2 changes: 2 additions & 0 deletions src/cargo/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub use self::flock::{FileLock, Filesystem};
pub use self::graph::Graph;
pub use self::hasher::StableHasher;
pub use self::hex::{hash_u64, short_hash, to_hex};
pub use self::hostname::hostname;
pub use self::into_url::IntoUrl;
pub use self::into_url_with_base::IntoUrlWithBase;
pub(crate) use self::io::LimitErrorReader;
Expand Down Expand Up @@ -45,6 +46,7 @@ mod flock;
pub mod graph;
mod hasher;
pub mod hex;
mod hostname;
pub mod important_paths;
pub mod interning;
pub mod into_url;
Expand Down
Loading