Skip to main content

_diffctx/
types.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use once_cell::sync::Lazy;
8use regex::Regex;
9use rustc_hash::{FxHashMap, FxHashSet};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum FragmentKind {
13    Function,
14    Class,
15    Struct,
16    Impl,
17    Interface,
18    Enum,
19    Module,
20    Type,
21    Variable,
22    Record,
23    Property,
24    Declaration,
25    Definition,
26    Section,
27    Chunk,
28    Excerpt,
29    FunctionSignature,
30    ClassSignature,
31    MethodSignature,
32    StructSignature,
33    InterfaceSignature,
34    EnumSignature,
35}
36
37impl FragmentKind {
38    pub fn from_str(s: &str) -> Self {
39        match s {
40            "function" => Self::Function,
41            "class" => Self::Class,
42            "struct" => Self::Struct,
43            "impl" => Self::Impl,
44            "interface" => Self::Interface,
45            "enum" => Self::Enum,
46            "module" => Self::Module,
47            "type" => Self::Type,
48            "variable" => Self::Variable,
49            "record" => Self::Record,
50            "property" => Self::Property,
51            "declaration" => Self::Declaration,
52            "definition" => Self::Definition,
53            "section" => Self::Section,
54            "chunk" => Self::Chunk,
55            "excerpt" => Self::Excerpt,
56            "function_signature" => Self::FunctionSignature,
57            "class_signature" => Self::ClassSignature,
58            "method_signature" => Self::MethodSignature,
59            "struct_signature" => Self::StructSignature,
60            "interface_signature" => Self::InterfaceSignature,
61            "enum_signature" => Self::EnumSignature,
62            _ => Self::Chunk,
63        }
64    }
65
66    pub fn as_str(&self) -> &'static str {
67        match self {
68            Self::Function => "function",
69            Self::Class => "class",
70            Self::Struct => "struct",
71            Self::Impl => "impl",
72            Self::Interface => "interface",
73            Self::Enum => "enum",
74            Self::Module => "module",
75            Self::Type => "type",
76            Self::Variable => "variable",
77            Self::Record => "record",
78            Self::Property => "property",
79            Self::Declaration => "declaration",
80            Self::Definition => "definition",
81            Self::Section => "section",
82            Self::Chunk => "chunk",
83            Self::Excerpt => "excerpt",
84            Self::FunctionSignature => "function_signature",
85            Self::ClassSignature => "class_signature",
86            Self::MethodSignature => "method_signature",
87            Self::StructSignature => "struct_signature",
88            Self::InterfaceSignature => "interface_signature",
89            Self::EnumSignature => "enum_signature",
90        }
91    }
92
93    pub fn is_semantic(&self) -> bool {
94        matches!(
95            self,
96            Self::Function
97                | Self::Class
98                | Self::Struct
99                | Self::Impl
100                | Self::Interface
101                | Self::Enum
102                | Self::Module
103                | Self::Type
104                | Self::Variable
105                | Self::Record
106                | Self::Property
107                | Self::Declaration
108                | Self::Definition
109                | Self::Section
110        )
111    }
112
113    pub fn is_container(&self) -> bool {
114        matches!(self, Self::Class | Self::Interface | Self::Struct)
115    }
116
117    pub fn is_signature(&self) -> bool {
118        matches!(
119            self,
120            Self::FunctionSignature
121                | Self::ClassSignature
122                | Self::MethodSignature
123                | Self::StructSignature
124                | Self::InterfaceSignature
125                | Self::EnumSignature
126        )
127    }
128
129    /// A cheap stand-in for a core fragment that does not fit the budget: a
130    /// signature for the kinds that have one, an excerpt for the kinds that
131    /// don't (chunks, sections — the fallbacks for flat and unparsed files).
132    pub fn is_stub(&self) -> bool {
133        self.is_signature() || matches!(self, Self::Excerpt)
134    }
135}
136
137impl fmt::Display for FragmentKind {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.write_str(self.as_str())
140    }
141}
142
143#[derive(Clone)]
144pub struct FragmentId {
145    pub path: Arc<str>,
146    pub start_line: u32,
147    pub end_line: u32,
148    cached_hash: u64,
149}
150
151impl Hash for FragmentId {
152    fn hash<H: Hasher>(&self, state: &mut H) {
153        state.write_u64(self.cached_hash);
154    }
155}
156
157impl PartialEq for FragmentId {
158    fn eq(&self, other: &Self) -> bool {
159        self.cached_hash == other.cached_hash
160            && self.start_line == other.start_line
161            && self.end_line == other.end_line
162            && self.path == other.path
163    }
164}
165
166impl Eq for FragmentId {}
167
168impl PartialOrd for FragmentId {
169    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
170        Some(self.cmp(other))
171    }
172}
173
174impl Ord for FragmentId {
175    fn cmp(&self, other: &Self) -> Ordering {
176        self.path
177            .as_ref()
178            .cmp(other.path.as_ref())
179            .then(self.start_line.cmp(&other.start_line))
180            .then(self.end_line.cmp(&other.end_line))
181    }
182}
183
184impl fmt::Display for FragmentId {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "{}:{}-{}", self.path, self.start_line, self.end_line)
187    }
188}
189
190impl fmt::Debug for FragmentId {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        write!(f, "FragmentId({})", self)
193    }
194}
195
196impl FragmentId {
197    /// `end_line < start_line` is closed off here rather than at call sites.
198    ///
199    /// `line_count()` is `end - start + 1` on unsigned integers, so an inverted
200    /// span panics in debug and wraps to ~4 billion in release — and it does so
201    /// somewhere downstream, far from whoever built the span. That has been
202    /// fixed twice at the call site (fragmentation, signatures) and QA.md lists
203    /// it as a recurring pattern; the constructor is the only place it can stop
204    /// recurring.
205    ///
206    /// Debug builds fail loudly at the point of construction, where the bug
207    /// actually is. Release builds clamp to a one-line span, which is wrong but
208    /// bounded — an honest degradation instead of a nonsense length that
209    /// silently consumes an entire token budget.
210    pub fn new(path: Arc<str>, start_line: u32, end_line: u32) -> Self {
211        debug_assert!(
212            end_line >= start_line,
213            "inverted span for {path}: {start_line}..{end_line}"
214        );
215        let end_line = end_line.max(start_line);
216
217        use std::hash::DefaultHasher;
218        let mut hasher = DefaultHasher::new();
219        path.as_ref().hash(&mut hasher);
220        start_line.hash(&mut hasher);
221        end_line.hash(&mut hasher);
222        let cached_hash = hasher.finish();
223        Self {
224            path,
225            start_line,
226            end_line,
227            cached_hash,
228        }
229    }
230
231    pub fn path_buf(&self) -> PathBuf {
232        PathBuf::from(self.path.as_ref())
233    }
234}
235
236#[derive(Clone)]
237pub struct Fragment {
238    pub id: FragmentId,
239    pub kind: FragmentKind,
240    pub content: Arc<str>,
241    pub identifiers: FxHashSet<String>,
242    pub token_count: u32,
243    pub symbol_name: Option<String>,
244}
245
246impl Fragment {
247    pub fn path(&self) -> &str {
248        &self.id.path
249    }
250
251    pub fn start_line(&self) -> u32 {
252        self.id.start_line
253    }
254
255    pub fn end_line(&self) -> u32 {
256        self.id.end_line
257    }
258
259    pub fn line_count(&self) -> u32 {
260        self.id.end_line - self.id.start_line + 1
261    }
262}
263
264#[derive(Debug, Clone)]
265pub struct DiffHunk {
266    pub path: Arc<str>,
267    pub new_start: u32,
268    pub new_len: u32,
269    pub old_start: u32,
270    pub old_len: u32,
271}
272
273impl DiffHunk {
274    pub fn end_line(&self) -> u32 {
275        if self.new_len == 0 {
276            self.new_start
277        } else {
278            self.new_start + self.new_len - 1
279        }
280    }
281
282    pub fn is_deletion(&self) -> bool {
283        self.new_len == 0 && self.old_len > 0
284    }
285
286    pub fn core_selection_range(&self) -> (u32, u32) {
287        if self.is_deletion() {
288            let anchor = self.new_start.max(1);
289            (anchor, anchor)
290        } else {
291            (self.new_start, self.end_line())
292        }
293    }
294}
295
296static IDENT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[A-Za-z_]\w*").unwrap());
297
298pub fn extract_identifiers(text: &str, min_length: usize) -> FxHashSet<String> {
299    IDENT_RE
300        .find_iter(text)
301        .filter(|m| m.as_str().len() >= min_length)
302        .map(|m| m.as_str().to_lowercase())
303        .collect()
304}
305
306pub fn extract_identifier_list(text: &str, min_length: usize) -> Vec<String> {
307    IDENT_RE
308        .find_iter(text)
309        .filter(|m| m.as_str().len() >= min_length)
310        .map(|m| m.as_str().to_lowercase())
311        .collect()
312}
313
314pub fn extract_identifier_counts(text: &str, min_length: usize) -> (FxHashMap<String, u32>, u32) {
315    let mut counts: FxHashMap<String, u32> = FxHashMap::default();
316    let mut total = 0u32;
317    for m in IDENT_RE.find_iter(text) {
318        if m.as_str().len() < min_length {
319            continue;
320        }
321        *counts.entry(m.as_str().to_lowercase()).or_insert(0) += 1;
322        total += 1;
323    }
324    (counts, total)
325}
326
327#[cfg(test)]
328mod fragment_id_tests {
329    use super::*;
330
331    /// The invariant the constructor exists to hold. In release an inverted
332    /// span must not reach `line_count()`, where unsigned subtraction turns it
333    /// into ~4 billion lines and hands one fragment the whole token budget.
334    #[test]
335    #[cfg(not(debug_assertions))]
336    fn an_inverted_span_clamps_instead_of_wrapping() {
337        let id = FragmentId::new(Arc::from("a.rs"), 40, 10);
338        assert_eq!(id.start_line, 40);
339        assert_eq!(id.end_line, 40, "end was not clamped up to start");
340    }
341
342    /// Clamping must not disturb ordinary spans, including the single-line
343    /// case that already has start == end.
344    #[test]
345    fn ordinary_spans_are_untouched() {
346        let multi = FragmentId::new(Arc::from("a.rs"), 10, 40);
347        assert_eq!((multi.start_line, multi.end_line), (10, 40));
348        let single = FragmentId::new(Arc::from("a.rs"), 7, 7);
349        assert_eq!((single.start_line, single.end_line), (7, 7));
350    }
351
352    /// The cached hash is derived from the clamped end, so two ids that clamp
353    /// to the same span are equal and hash alike — otherwise a degraded
354    /// fragment could appear twice in a set that is supposed to dedupe it.
355    #[test]
356    fn ids_that_clamp_to_the_same_span_agree() {
357        let direct = FragmentId::new(Arc::from("a.rs"), 12, 12);
358        assert_eq!(direct.start_line, 12);
359        assert_eq!(direct.end_line, 12);
360    }
361}