cadi_core/rehydration/
view.rs

1//! Virtual View representation
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// A virtual view - atoms assembled into a coherent context
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct VirtualView {
9    /// The assembled source code
10    pub source: String,
11
12    /// Atoms included in this view (in order)
13    pub atoms: Vec<String>,
14
15    /// Which atoms were added automatically (Ghost Imports)
16    pub ghost_atoms: Vec<String>,
17
18    /// Total token estimate
19    pub token_estimate: usize,
20
21    /// Primary language of the view
22    pub language: String,
23
24    /// Map of symbol name -> line number in the view
25    pub symbol_locations: HashMap<String, usize>,
26
27    /// Fragments that make up this view
28    pub fragments: Vec<ViewFragment>,
29
30    /// Was the view truncated due to token limits?
31    pub truncated: bool,
32
33    /// Explanation of how the view was constructed
34    pub explanation: String,
35}
36
37impl VirtualView {
38    /// Create a new empty view
39    pub fn new(language: impl Into<String>) -> Self {
40        Self {
41            source: String::new(),
42            atoms: Vec::new(),
43            ghost_atoms: Vec::new(),
44            token_estimate: 0,
45            language: language.into(),
46            symbol_locations: HashMap::new(),
47            fragments: Vec::new(),
48            truncated: false,
49            explanation: String::new(),
50        }
51    }
52
53    /// Get the number of lines in the view
54    pub fn line_count(&self) -> usize {
55        self.source.lines().count()
56    }
57
58    /// Get the symbols defined in this view
59    pub fn defined_symbols(&self) -> Vec<&String> {
60        self.symbol_locations.keys().collect()
61    }
62
63    /// Find the line number where a symbol is defined
64    pub fn find_symbol(&self, name: &str) -> Option<usize> {
65        self.symbol_locations.get(name).copied()
66    }
67
68    /// Get a snippet around a symbol
69    pub fn snippet_for_symbol(&self, name: &str, context_lines: usize) -> Option<String> {
70        let line = self.find_symbol(name)?;
71        let lines: Vec<&str> = self.source.lines().collect();
72        
73        let start = line.saturating_sub(context_lines + 1);
74        let end = (line + context_lines).min(lines.len());
75        
76        Some(lines[start..end].join("\n"))
77    }
78
79    /// Check if this view contains a specific atom
80    pub fn contains_atom(&self, chunk_id: &str) -> bool {
81        self.atoms.contains(&chunk_id.to_string())
82    }
83
84    /// Was this atom added automatically (ghost import)?
85    pub fn is_ghost(&self, chunk_id: &str) -> bool {
86        self.ghost_atoms.contains(&chunk_id.to_string())
87    }
88}
89
90/// A fragment in a virtual view
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ViewFragment {
93    /// The chunk ID this fragment came from
94    pub chunk_id: String,
95
96    /// Alias if available
97    pub alias: Option<String>,
98
99    /// Start line in the assembled view
100    pub start_line: usize,
101
102    /// End line in the assembled view
103    pub end_line: usize,
104
105    /// Token count for this fragment
106    pub token_count: usize,
107
108    /// Why this fragment was included
109    pub inclusion_reason: InclusionReason,
110
111    /// Symbols defined in this fragment
112    pub defines: Vec<String>,
113}
114
115/// Why a fragment was included in a view
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum InclusionReason {
119    /// Explicitly requested
120    Requested,
121    /// Imported by a requested atom
122    Imported,
123    /// Type referenced by a requested atom
124    TypeDependency,
125    /// Required for context (Ghost Import)
126    GhostImport,
127}
128
129impl ViewFragment {
130    /// Create a new fragment
131    pub fn new(chunk_id: impl Into<String>, reason: InclusionReason) -> Self {
132        Self {
133            chunk_id: chunk_id.into(),
134            alias: None,
135            start_line: 0,
136            end_line: 0,
137            token_count: 0,
138            inclusion_reason: reason,
139            defines: Vec::new(),
140        }
141    }
142
143    /// With alias
144    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
145        self.alias = Some(alias.into());
146        self
147    }
148
149    /// With line range
150    pub fn with_lines(mut self, start: usize, end: usize) -> Self {
151        self.start_line = start;
152        self.end_line = end;
153        self
154    }
155
156    /// With token count
157    pub fn with_tokens(mut self, tokens: usize) -> Self {
158        self.token_count = tokens;
159        self
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_view_creation() {
169        let mut view = VirtualView::new("rust");
170        view.source = "fn hello() {}\n\nfn world() {}".to_string();
171        view.atoms = vec!["chunk:a".to_string(), "chunk:b".to_string()];
172        view.symbol_locations.insert("hello".to_string(), 1);
173        view.symbol_locations.insert("world".to_string(), 3);
174
175        assert_eq!(view.line_count(), 3);
176        assert_eq!(view.find_symbol("hello"), Some(1));
177        assert!(view.contains_atom("chunk:a"));
178    }
179
180    #[test]
181    fn test_snippet_extraction() {
182        let mut view = VirtualView::new("rust");
183        view.source = "line 1\nline 2\nfn target() {}\nline 4\nline 5".to_string();
184        view.symbol_locations.insert("target".to_string(), 3);
185
186        let snippet = view.snippet_for_symbol("target", 1).unwrap();
187        assert!(snippet.contains("line 2"));
188        assert!(snippet.contains("fn target"));
189        assert!(snippet.contains("line 4"));
190    }
191}