1use std::path::PathBuf;
2
3#[derive(Debug, thiserror::Error)]
4#[non_exhaustive]
5pub enum ConfigError {
6 #[error("no home directory available — XDG resolution failed (no $HOME on this platform)")]
7 NoHomeDir,
8
9 #[error("io error reading {path}: {source}")]
10 Io {
11 path: PathBuf,
12 #[source]
13 source: std::io::Error,
14 },
15
16 #[error("io error writing {path}: {source}")]
17 Write {
18 path: PathBuf,
19 #[source]
20 source: std::io::Error,
21 },
22
23 #[error("parse error in {path} at line {line}, column {col}: {message}\n | {snippet}")]
24 Parse {
25 path: PathBuf,
26 line: usize,
27 col: usize,
28 message: String,
29 snippet: String,
30 },
31
32 #[error("invalid config in {path}: {message}")]
36 Invalid { path: PathBuf, message: String },
37}
38
39pub fn locate(src: &str, byte_offset: usize) -> (usize, usize, String) {
46 let mut clamped = byte_offset.min(src.len());
47 while !src.is_char_boundary(clamped) {
50 clamped -= 1;
51 }
52 let mut line_start = 0usize;
53 let mut line_no = 1usize;
54 for (i, ch) in src.char_indices() {
55 if i >= clamped {
56 break;
57 }
58 if ch == '\n' {
59 line_no += 1;
60 line_start = i + ch.len_utf8();
61 }
62 }
63 let line_end = src[line_start..]
64 .find('\n')
65 .map_or(src.len(), |n| line_start + n);
66 let line_text = src[line_start..line_end].to_string();
67 let col = src[line_start..clamped].chars().count() + 1;
68 (line_no, col, line_text)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::locate;
74
75 #[test]
76 fn locate_first_line() {
77 let (l, c, txt) = locate("hello\nworld\n", 0);
78 assert_eq!((l, c), (1, 1));
79 assert_eq!(txt, "hello");
80 }
81
82 #[test]
83 fn locate_second_line_mid() {
84 let (l, c, txt) = locate("hello\nworld\n", 8);
85 assert_eq!((l, c), (2, 3));
86 assert_eq!(txt, "world");
87 }
88
89 #[test]
90 fn locate_clamps_past_end() {
91 let (l, c, _) = locate("a", 999);
92 assert_eq!((l, c), (1, 2));
93 }
94
95 #[test]
96 fn locate_multibyte_before_offset_counts_chars_not_bytes() {
97 let src = "é = x";
99 let x_offset = src.find('x').unwrap();
100 let (l, c, txt) = locate(src, x_offset);
101 assert_eq!((l, c), (1, 5));
102 assert_eq!(txt, "é = x");
103 }
104
105 #[test]
106 fn locate_mid_char_offset_snaps_to_boundary() {
107 let (l, c, txt) = locate("é = 1", 1);
110 assert_eq!((l, c), (1, 1));
111 assert_eq!(txt, "é = 1");
112 }
113
114 #[test]
115 fn locate_unicode_offset() {
116 let src = "α = 1\nβ = 2";
117 let beta_offset = src.find('β').unwrap();
118 let (l, c, txt) = locate(src, beta_offset);
119 assert_eq!(l, 2);
120 assert_eq!(c, 1);
121 assert_eq!(txt, "β = 2");
122 }
123}