Skip to main content

teksilo_preview/
source_loc.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Captured source location for a registered widget catalog entry.
5//!
6//! Populated by the `register_widget_catalog!` macro at expansion time
7//! via `file!()` / `line!()`. The previewer's `--file=PATH` resolution
8//! matches against the captured `file` by suffix to handle platform
9//! path canonicalisation.
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct SourceLoc {
13    pub file: &'static str,
14    pub line: u32,
15}
16
17impl SourceLoc {
18    pub const fn new(file: &'static str, line: u32) -> Self {
19        Self { file, line }
20    }
21
22    /// Match this source location against a path supplied on the command
23    /// line. The match is a suffix match — the user typically supplies a
24    /// workspace-relative path while `file!()` returns a path relative to
25    /// the crate the macro expanded in. Matching by suffix accommodates
26    /// both without requiring path canonicalisation.
27    pub fn matches_path(&self, target: &str) -> bool {
28        // Normalise separators so the comparison is consistent across OSes.
29        fn norm(s: &str) -> String {
30            s.replace('\\', "/")
31        }
32        // A suffix match, but only on a `/` component boundary — so a short
33        // query like "button.rs" does NOT match ".../radio_button.rs".
34        fn suffix_on_boundary(hay: &str, needle: &str) -> bool {
35            hay == needle || hay.strip_suffix(needle).is_some_and(|p| p.ends_with('/'))
36        }
37        let a = norm(self.file);
38        let b = norm(target);
39        suffix_on_boundary(&a, &b) || suffix_on_boundary(&b, &a)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn matches_path_handles_suffix_matches() {
49        let loc = SourceLoc::new("crates/teksilo-widgets/src/button.rs", 42);
50        assert!(loc.matches_path("crates/teksilo-widgets/src/button.rs"));
51        assert!(loc.matches_path("button.rs"));
52        assert!(loc.matches_path("src/button.rs"));
53        assert!(!loc.matches_path("crates/teksilo-widgets/src/checkbox.rs"));
54    }
55
56    #[test]
57    fn suffix_match_respects_component_boundaries() {
58        // "button.rs" must not match a sibling whose name merely ends in it.
59        let radio = SourceLoc::new("crates/teksilo-widgets/src/radio_button.rs", 1);
60        assert!(!radio.matches_path("button.rs"));
61        assert!(radio.matches_path("radio_button.rs"));
62        assert!(radio.matches_path("src/radio_button.rs"));
63    }
64
65    #[test]
66    fn matches_path_normalises_separators() {
67        let loc = SourceLoc::new("crates\\teksilo-widgets\\src\\button.rs", 1);
68        assert!(loc.matches_path("button.rs"));
69        assert!(loc.matches_path("src/button.rs"));
70    }
71}