Skip to main content

charon_error/utils/
source_location.rs

1//! Source code location capture and terminal hyperlink formatting.
2//!
3//! [`SourceLocation`] records file, line, and column and can render
4//! them as plain text or clickable OSC 8 terminal hyperlinks.
5//! [`LinkDebugIde`] selects the link format.
6
7use std::{fmt::Display, panic::Location};
8use tracing::Metadata;
9
10use crate::{ERGlobalSettings, GlobalSettings, ResultExt};
11
12/// Format for source location links in terminal output.
13///
14/// Controls whether file paths are plain text or clickable terminal hyperlinks
15/// (using the OSC 8 escape sequence).
16#[derive(Debug, Clone, Default, Copy)]
17pub enum LinkDebugIde {
18    /// Plain text path (no hyperlink).
19    #[default]
20    NoLink,
21    /// `file://` protocol link.
22    File,
23    /// VSCode `vscode://file/` protocol link for click-to-open.
24    Vscode,
25}
26
27/// Captures a source code location (file, line, column).
28///
29/// Automatically captured via `#[track_caller]` when creating error frames.
30/// Can format the location as a clickable terminal hyperlink for supported IDEs.
31#[derive(Debug, Clone)]
32pub struct SourceLocation {
33    /// Name of the crate where the location was captured.
34    pub crate_name: &'static str,
35    /// Version of the crate where the location was captured.
36    pub crate_version: &'static str,
37    /// File path (can be absolute or relative).
38    pub filename: String,
39    /// Line number in the source file.
40    pub line_number: u32,
41    /// Column number in the source file.
42    pub column_number: u32,
43}
44
45impl SourceLocation {
46    fn check_if_abs_path(path: &str) -> bool {
47        path.starts_with('/')
48    }
49
50    /// Capture the current caller's source location via `#[track_caller]`.
51    #[must_use]
52    #[track_caller]
53    pub fn from_caller_location() -> Self {
54        let caller = Location::caller();
55        Self {
56            crate_name: env!("CARGO_PKG_NAME"),
57            crate_version: env!("CARGO_PKG_VERSION"),
58            filename: caller.file().to_owned(),
59            line_number: caller.line(),
60            column_number: caller.column(),
61        }
62    }
63
64    /// Create from a panic hook's `Location`.
65    #[must_use]
66    pub fn from_panic_location(location: &Location) -> Self {
67        Self {
68            crate_name: env!("CARGO_PKG_NAME"),
69            crate_version: env!("CARGO_PKG_VERSION"),
70            filename: location.file().to_owned(),
71            line_number: location.line(),
72            column_number: location.column(),
73        }
74    }
75
76    /// Create from `tracing` span metadata.
77    #[must_use]
78    pub fn from_metadata(metadata: &Metadata) -> Self {
79        Self {
80            crate_name: env!("CARGO_PKG_NAME"),
81            crate_version: env!("CARGO_PKG_VERSION"),
82            filename: metadata.file().unwrap_or("Unknown").to_owned(),
83            line_number: metadata.line().unwrap_or(0),
84            column_number: 0,
85        }
86    }
87
88    /// Format the location as a string, optionally with a clickable terminal hyperlink.
89    #[must_use]
90    pub fn display_location(&self, mut overwrite_link_fmt: Option<LinkDebugIde>) -> String {
91        // Disable links too if ANSI is disabled.
92        if std::env::var("NO_COLOR").is_ok() {
93            overwrite_link_fmt = Some(LinkDebugIde::NoLink);
94        }
95
96        // In release builds if not executed using `cargo run`, always disable linking.
97        #[cfg(not(debug_assertions))]
98        if std::env::var("CARGO").is_err() {
99            overwrite_link_fmt = Some(LinkDebugIde::NoLink);
100        }
101
102        match overwrite_link_fmt
103            .or(Some(
104                ERGlobalSettings::get_global_settings()
105                    .unwrap_error()
106                    .link_format,
107            ))
108            .unwrap_or_default()
109        {
110            LinkDebugIde::NoLink => format!(
111                "{}:{}:{}",
112                self.filename, self.line_number, self.column_number
113            ),
114            LinkDebugIde::File => format!(
115                "\x1b]8;;file://{absolute_path}:{line}:{column}\x1b\\{relative_path}:{line}:{column}\x1b]8;;\x1b\\",
116                relative_path = Self::get_relative_path(&self.filename),
117                absolute_path = Self::get_absolute_path(&self.filename),
118                line = self.line_number,
119                column = self.column_number,
120            ),
121            LinkDebugIde::Vscode => format!(
122                "\x1b]8;;vscode://file/{absolute_path}:{line}:{column}\x1b\\{relative_path}:{line}:{column}\x1b]8;;\x1b\\",
123                relative_path = Self::get_relative_path(&self.filename),
124                absolute_path = Self::get_absolute_path(&self.filename),
125                line = self.line_number,
126                column = self.column_number,
127            ),
128        }
129    }
130
131    fn get_relative_path(path: &str) -> String {
132        let current_dir = std::env::current_dir().unwrap_error().display().to_string();
133
134        if Self::check_if_abs_path(path) {
135            // Also remove the `/` after that string
136            let current_dir = format!("{current_dir}/");
137            if path.starts_with(&current_dir) {
138                path.split_at(current_dir.len()).1.to_owned()
139            } else {
140                path.to_owned()
141            }
142        } else {
143            path.to_owned()
144        }
145    }
146
147    fn get_absolute_path(path: &str) -> String {
148        let current_dir = std::env::current_dir().unwrap_error().display().to_string();
149
150        if Self::check_if_abs_path(path) {
151            path.to_owned()
152        } else {
153            format!("{}/{}", current_dir, path)
154        }
155    }
156}
157
158impl Display for SourceLocation {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        write!(f, "{}", self.display_location(None))
161    }
162}