Skip to main content

swls_core/util/
mod.rs

1use crate::{
2    lsp_types::{Location, Position, Range},
3    text::{LineIndex, PositionEncoding},
4    Label,
5};
6
7/// Encoding used for all LSP positions emitted/consumed by these helpers.
8///
9/// The LSP default is UTF-16; until we negotiate `positionEncoding` at
10/// `initialize`, everything goes through this single constant so the choice is
11/// explicit and changeable in one place.
12const ENCODING: PositionEncoding = PositionEncoding::Utf16;
13
14pub mod fs;
15/// Commonly used RDF prefixes
16pub mod ns;
17pub mod token;
18pub mod triple;
19
20pub use rdf_parsers::{spanned, Spanned};
21
22// /// Maps http:// and https:// urls to virtual:// urls
23// /// This enables the editor to show them
24// pub fn make_virtual_url(url: &str, prefix: &str) -> Option<Url> {
25//     if !url.starts_with("http") {
26//         return None;
27//     }
28//
29//     let url = format!("virtual://prefix/{}.ttl", prefix);
30//
31//     crate::lsp_types::Url::parse(&url).ok()
32// }
33
34pub fn range_to_range(range: &std::ops::Range<usize>, rope: &LineIndex) -> Option<Range> {
35    let start = offset_to_position(range.start, rope)?;
36    let end = offset_to_position(range.end, rope)?;
37    Range::new(start, end).into()
38}
39
40pub fn lsp_range_to_range(
41    range: &crate::lsp_types::Range,
42    rope: &LineIndex,
43) -> Option<std::ops::Range<usize>> {
44    let start = position_to_offset(range.start, rope)?;
45    let end = position_to_offset(range.end, rope)?;
46    Some(start..end)
47}
48
49pub fn offset_to_position(offset: usize, rope: &LineIndex) -> Option<Position> {
50    rope.byte_to_position(offset, ENCODING)
51}
52pub fn position_to_offset(position: Position, rope: &LineIndex) -> Option<usize> {
53    rope.position_to_byte(position, ENCODING)
54}
55pub fn offsets_to_range(start: usize, end: usize, rope: &LineIndex) -> Option<Range> {
56    let start = offset_to_position(start, rope)?;
57    let end = offset_to_position(end, rope)?;
58    Some(Range { start, end })
59}
60
61pub fn token_to_location(
62    token: &std::ops::Range<usize>,
63    label: &Label,
64    rope: &LineIndex,
65) -> Option<Location> {
66    let range = range_to_range(token, rope)?;
67    Some(Location {
68        range,
69        uri: label.0.clone(),
70    })
71}
72
73// ── IRI resolution ────────────────────────────────────────────────────────────
74
75/// Resolve `relative` against `base` using RFC 3986 path resolution, without
76/// any IRI validation.  This intentionally accepts non-standard / "invalid"
77/// IRIs so that tokens in the source document can be round-tripped back to the
78/// triple store without percent-encoding transformations.
79pub fn resolve_iri(base: &str, relative: &str) -> String {
80    // Already absolute (has a scheme colon before any slash or query char).
81    if looks_absolute(relative) {
82        return relative.to_string();
83    }
84    // Protocol-relative  //authority/path
85    if relative.starts_with("//") {
86        let scheme = base.split("://").next().unwrap_or("http");
87        return format!("{}:{}", scheme, relative);
88    }
89    // Root-relative  /path
90    if relative.starts_with('/') {
91        let authority_end = base
92            .find("://")
93            .and_then(|i| base[i + 3..].find('/').map(|j| i + 3 + j))
94            .unwrap_or(base.len());
95        return format!("{}{}", &base[..authority_end], relative);
96    }
97    // Fragment-only or empty  #frag  or  ""
98    if relative.is_empty() || relative.starts_with('#') {
99        let base_no_frag = base.split('#').next().unwrap_or(base);
100        return format!("{}{}", base_no_frag, relative);
101    }
102    // Relative path — merge with the base's "directory"
103    let base_dir = match base.rfind('/') {
104        Some(i) => &base[..=i],
105        None => "",
106    };
107    let merged = format!("{}{}", base_dir, relative);
108    normalize_path_segments(&merged)
109}
110
111/// Heuristic: does `s` carry its own scheme?  We look for `<scheme>:` where
112/// scheme starts with a letter and contains only [A-Za-z0-9+\-.].
113fn looks_absolute(s: &str) -> bool {
114    let bytes = s.as_bytes();
115    if bytes.is_empty() || !bytes[0].is_ascii_alphabetic() {
116        return false;
117    }
118    for (i, &b) in bytes.iter().enumerate() {
119        if b == b':' {
120            return i > 0;
121        }
122        if !matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'-' | b'.') {
123            return false;
124        }
125    }
126    false
127}
128
129/// Remove `.` and `..` segments from a merged path string, preserving any
130/// authority prefix (`scheme://host`).
131fn normalize_path_segments(path: &str) -> String {
132    // Split off a leading scheme://authority so we don't mangle it.
133    let (prefix, rest) = if let Some(i) = path.find("://") {
134        let after = &path[i + 3..];
135        let slash = after.find('/').map(|j| i + 3 + j).unwrap_or(path.len());
136        (&path[..slash], &path[slash..])
137    } else {
138        ("", path)
139    };
140
141    let mut stack: Vec<&str> = Vec::new();
142    for seg in rest.split('/') {
143        match seg {
144            "." => {}
145            ".." => {
146                stack.pop();
147            }
148            s => stack.push(s),
149        }
150    }
151    // If the original path ended with /. or /.. keep a trailing slash.
152    let trailing = if rest.ends_with("/..") || rest.ends_with("/.") {
153        "/"
154    } else {
155        ""
156    };
157    format!("{}/{}{}", prefix, stack.join("/"), trailing)
158}