1use std::collections::HashMap;
9use std::sync::Arc;
10
11use arc_swap::ArcSwap;
12use dashmap::{DashMap, DashSet};
13use dk_core::{CallEdge, Symbol, SymbolId};
14use serde::{Deserialize, Serialize};
15use uuid::Uuid;
16
17pub struct SessionGraph {
25 base_symbols: Option<Arc<ArcSwap<HashMap<SymbolId, Symbol>>>>,
27
28 pub(crate) modified_symbols: DashMap<SymbolId, Symbol>,
30
31 added_symbols: DashMap<SymbolId, Symbol>,
33
34 pub(crate) removed_symbols: DashSet<SymbolId>,
36
37 removed_symbol_names: DashMap<SymbolId, String>,
40
41 pub(crate) added_edges: DashMap<Uuid, CallEdge>,
43
44 pub(crate) removed_edges: DashSet<Uuid>,
46}
47
48#[derive(Serialize, Deserialize)]
56struct SessionGraphSnapshot {
57 modified_symbols: Vec<(SymbolId, Symbol)>,
58 added_symbols: Vec<(SymbolId, Symbol)>,
59 removed_symbols: Vec<SymbolId>,
60 #[serde(default)]
63 removed_symbol_names: Vec<(SymbolId, String)>,
64 added_edges: Vec<(Uuid, CallEdge)>,
65 removed_edges: Vec<Uuid>,
66}
67
68impl SessionGraph {
69 pub fn fork_from(base: Arc<ArcSwap<HashMap<SymbolId, Symbol>>>) -> Self {
71 Self {
72 base_symbols: Some(base),
73 modified_symbols: DashMap::new(),
74 added_symbols: DashMap::new(),
75 removed_symbols: DashSet::new(),
76 removed_symbol_names: DashMap::new(),
77 added_edges: DashMap::new(),
78 removed_edges: DashSet::new(),
79 }
80 }
81
82 pub fn empty() -> Self {
84 Self {
85 base_symbols: None,
86 modified_symbols: DashMap::new(),
87 added_symbols: DashMap::new(),
88 removed_symbols: DashSet::new(),
89 removed_symbol_names: DashMap::new(),
90 added_edges: DashMap::new(),
91 removed_edges: DashSet::new(),
92 }
93 }
94
95 pub fn get_symbol(&self, id: SymbolId) -> Option<Symbol> {
103 if self.removed_symbols.contains(&id) {
105 return None;
106 }
107
108 if let Some(sym) = self.modified_symbols.get(&id) {
110 return Some(sym.value().clone());
111 }
112
113 if let Some(sym) = self.added_symbols.get(&id) {
115 return Some(sym.value().clone());
116 }
117
118 if let Some(base) = &self.base_symbols {
120 let snapshot = base.load();
121 return snapshot.get(&id).cloned();
122 }
123
124 None
125 }
126
127 pub fn add_symbol(&self, symbol: Symbol) {
129 self.added_symbols.insert(symbol.id, symbol);
130 }
131
132 pub fn modify_symbol(&self, symbol: Symbol) {
134 let id = symbol.id;
135
136 if self.added_symbols.contains_key(&id) {
138 self.added_symbols.insert(id, symbol);
139 } else {
140 self.modified_symbols.insert(id, symbol);
141 }
142 }
143
144 pub fn remove_symbol(&self, id: SymbolId) {
146 if self.added_symbols.remove(&id).is_some() {
148 return;
149 }
150
151 if let Some((_, sym)) = self.modified_symbols.remove(&id) {
153 self.removed_symbol_names
154 .insert(id, sym.qualified_name.clone());
155 } else if let Some(base) = &self.base_symbols {
156 let snapshot = base.load();
158 if let Some(sym) = snapshot.get(&id) {
159 self.removed_symbol_names
160 .insert(id, sym.qualified_name.clone());
161 }
162 }
163
164 self.removed_symbols.insert(id);
166 }
167
168 pub fn add_edge(&self, edge: CallEdge) {
170 self.added_edges.insert(edge.id, edge);
171 }
172
173 pub fn remove_edge(&self, edge_id: Uuid) {
175 if self.added_edges.remove(&edge_id).is_some() {
177 return;
178 }
179 self.removed_edges.insert(edge_id);
180 }
181
182 pub fn get_edge(&self, edge_id: Uuid) -> Option<CallEdge> {
187 if self.removed_edges.contains(&edge_id) {
188 return None;
189 }
190 self.added_edges.get(&edge_id).map(|e| e.value().clone())
191 }
192
193 pub fn is_edge_removed(&self, edge_id: Uuid) -> bool {
196 self.removed_edges.contains(&edge_id)
197 }
198
199 pub fn changed_symbol_names(&self) -> Vec<String> {
204 let mut names = Vec::new();
205
206 for entry in self.added_symbols.iter() {
207 names.push(entry.value().qualified_name.clone());
208 }
209
210 for entry in self.modified_symbols.iter() {
211 names.push(entry.value().qualified_name.clone());
212 }
213
214 for id in self.removed_symbols.iter() {
217 let found = self
218 .base_symbols
219 .as_ref()
220 .and_then(|base| base.load().get(id.key()).map(|s| s.qualified_name.clone()));
221 if let Some(name) = found {
222 names.push(name);
223 } else if let Some(name) = self.removed_symbol_names.get(id.key()) {
224 names.push(name.value().clone());
225 }
226 }
227
228 names
229 }
230
231 pub fn remove_session_symbols_for_file(&self, file_path: &str) {
241 let target = std::path::Path::new(file_path);
242
243 let added_ids: Vec<SymbolId> = self
244 .added_symbols
245 .iter()
246 .filter(|e| e.value().file_path == target)
247 .map(|e| *e.key())
248 .collect();
249
250 let modified_ids: Vec<SymbolId> = self
251 .modified_symbols
252 .iter()
253 .filter(|e| e.value().file_path == target)
254 .map(|e| *e.key())
255 .collect();
256
257 for id in added_ids {
258 self.added_symbols.remove(&id);
259 }
260 for id in modified_ids {
261 self.remove_symbol(id);
263 }
264 }
265
266 pub fn update_from_parse(
275 &self,
276 _file_path: &str,
277 new_symbols: Vec<Symbol>,
278 base_symbols_for_file: &[Symbol],
279 ) {
280 let base_by_name: HashMap<&str, &Symbol> = base_symbols_for_file
282 .iter()
283 .map(|s| (s.qualified_name.as_str(), s))
284 .collect();
285
286 let new_by_name: HashMap<&str, &Symbol> = new_symbols
287 .iter()
288 .map(|s| (s.qualified_name.as_str(), s))
289 .collect();
290
291 for sym in &new_symbols {
294 if let Some(base_sym) = base_by_name.get(sym.qualified_name.as_str()) {
295 if sym.span != base_sym.span
297 || sym.signature != base_sym.signature
298 || sym.kind != base_sym.kind
299 || sym.visibility != base_sym.visibility
300 {
301 self.modify_symbol(sym.clone());
302 }
303 } else {
304 self.add_symbol(sym.clone());
305 }
306 }
307
308 for base_sym in base_symbols_for_file {
310 if !new_by_name.contains_key(base_sym.qualified_name.as_str()) {
311 self.remove_symbol(base_sym.id);
312 }
313 }
314 }
315
316 pub fn changed_symbols_for_file(&self, file_path: &str) -> Vec<String> {
319 let target = std::path::Path::new(file_path);
320 let mut names = Vec::new();
321
322 for entry in self.added_symbols.iter() {
323 if entry.value().file_path == target {
324 names.push(entry.value().name.clone());
325 }
326 }
327
328 for entry in self.modified_symbols.iter() {
329 if entry.value().file_path == target {
330 names.push(entry.value().name.clone());
331 }
332 }
333
334 names
335 }
336
337 pub fn change_count(&self) -> usize {
339 self.added_symbols.len() + self.modified_symbols.len() + self.removed_symbols.len()
340 }
341
342 pub fn to_msgpack(&self) -> anyhow::Result<Vec<u8>> {
350 let snapshot = SessionGraphSnapshot {
351 modified_symbols: self
352 .modified_symbols
353 .iter()
354 .map(|e| (*e.key(), e.value().clone()))
355 .collect(),
356 added_symbols: self
357 .added_symbols
358 .iter()
359 .map(|e| (*e.key(), e.value().clone()))
360 .collect(),
361 removed_symbols: self.removed_symbols.iter().map(|r| *r).collect(),
362 removed_symbol_names: self
363 .removed_symbol_names
364 .iter()
365 .map(|e| (*e.key(), e.value().clone()))
366 .collect(),
367 added_edges: self
368 .added_edges
369 .iter()
370 .map(|e| (*e.key(), e.value().clone()))
371 .collect(),
372 removed_edges: self.removed_edges.iter().map(|r| *r).collect(),
373 };
374
375 Ok(rmp_serde::to_vec_named(&snapshot)?)
376 }
377
378 pub fn from_msgpack(bytes: &[u8]) -> anyhow::Result<Self> {
385 let snapshot: SessionGraphSnapshot = rmp_serde::from_slice(bytes)?;
386
387 let modified_symbols = DashMap::new();
388 for (id, sym) in snapshot.modified_symbols {
389 modified_symbols.insert(id, sym);
390 }
391
392 let added_symbols = DashMap::new();
393 for (id, sym) in snapshot.added_symbols {
394 added_symbols.insert(id, sym);
395 }
396
397 let removed_symbols = DashSet::new();
398 for id in snapshot.removed_symbols {
399 removed_symbols.insert(id);
400 }
401
402 let removed_symbol_names = DashMap::new();
403 for (id, name) in snapshot.removed_symbol_names {
404 removed_symbol_names.insert(id, name);
405 }
406
407 let added_edges = DashMap::new();
408 for (id, edge) in snapshot.added_edges {
409 added_edges.insert(id, edge);
410 }
411
412 let removed_edges = DashSet::new();
413 for id in snapshot.removed_edges {
414 removed_edges.insert(id);
415 }
416
417 Ok(Self {
418 base_symbols: None,
419 modified_symbols,
420 added_symbols,
421 removed_symbols,
422 removed_symbol_names,
423 added_edges,
424 removed_edges,
425 })
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use dk_core::{Span, SymbolKind, Visibility};
433 use std::path::PathBuf;
434
435 fn make_symbol(name: &str) -> Symbol {
436 Symbol {
437 id: Uuid::new_v4(),
438 name: name.to_string(),
439 qualified_name: name.to_string(),
440 kind: SymbolKind::Function,
441 visibility: Visibility::Public,
442 file_path: PathBuf::from("test.rs"),
443 span: Span {
444 start_byte: 0,
445 end_byte: 10,
446 },
447 signature: None,
448 doc_comment: None,
449 parent: None,
450 last_modified_by: None,
451 last_modified_intent: None,
452 }
453 }
454
455 #[test]
456 fn empty_graph_returns_none() {
457 let g = SessionGraph::empty();
458 assert!(g.get_symbol(Uuid::new_v4()).is_none());
459 }
460
461 #[test]
462 fn add_and_get_symbol() {
463 let g = SessionGraph::empty();
464 let sym = make_symbol("foo");
465 let id = sym.id;
466 g.add_symbol(sym);
467 assert!(g.get_symbol(id).is_some());
468 assert_eq!(g.get_symbol(id).unwrap().name, "foo");
469 }
470
471 #[test]
472 fn remove_added_symbol() {
473 let g = SessionGraph::empty();
474 let sym = make_symbol("bar");
475 let id = sym.id;
476 g.add_symbol(sym);
477 g.remove_symbol(id);
478 assert!(g.get_symbol(id).is_none());
479 }
480
481 #[test]
482 fn modify_added_symbol_updates_in_place() {
483 let g = SessionGraph::empty();
484 let mut sym = make_symbol("baz");
485 let id = sym.id;
486 g.add_symbol(sym.clone());
487
488 sym.name = "baz_v2".to_string();
489 g.modify_symbol(sym);
490
491 let got = g.get_symbol(id).unwrap();
492 assert_eq!(got.name, "baz_v2");
493 }
494
495 #[test]
496 fn fork_from_base_lookup() {
497 let mut base = HashMap::new();
498 let sym = make_symbol("base_fn");
499 let id = sym.id;
500 base.insert(id, sym);
501
502 let shared = Arc::new(ArcSwap::from_pointee(base));
503 let g = SessionGraph::fork_from(shared);
504
505 assert!(g.get_symbol(id).is_some());
506 assert_eq!(g.get_symbol(id).unwrap().name, "base_fn");
507 }
508
509 #[test]
510 fn remove_base_symbol_hides_it() {
511 let mut base = HashMap::new();
512 let sym = make_symbol("base_fn");
513 let id = sym.id;
514 base.insert(id, sym);
515
516 let shared = Arc::new(ArcSwap::from_pointee(base));
517 let g = SessionGraph::fork_from(shared);
518
519 g.remove_symbol(id);
520 assert!(g.get_symbol(id).is_none());
521 }
522
523 #[test]
524 fn changed_symbol_names_collects_all() {
525 let mut base = HashMap::new();
526 let sym = make_symbol("removed_fn");
527 let removed_id = sym.id;
528 base.insert(removed_id, sym);
529
530 let shared = Arc::new(ArcSwap::from_pointee(base));
531 let g = SessionGraph::fork_from(shared);
532
533 let added = make_symbol("added_fn");
534 g.add_symbol(added);
535
536 let mut modified = make_symbol("modified_fn");
537 modified.id = Uuid::new_v4();
538 let mid = modified.id;
539 g.modified_symbols.insert(mid, modified);
541
542 g.remove_symbol(removed_id);
543
544 let names = g.changed_symbol_names();
545 assert!(names.contains(&"added_fn".to_string()));
546 assert!(names.contains(&"modified_fn".to_string()));
547 assert!(names.contains(&"removed_fn".to_string()));
548 }
549
550 #[test]
551 fn change_count() {
552 let g = SessionGraph::empty();
553 assert_eq!(g.change_count(), 0);
554
555 g.add_symbol(make_symbol("a"));
556 assert_eq!(g.change_count(), 1);
557 }
558
559 #[test]
560 fn changed_symbols_for_file_filters_by_path() {
561 let g = SessionGraph::empty();
562
563 let mut sym1 = make_symbol("create_task");
564 sym1.file_path = PathBuf::from("src/tasks.rs");
565 g.add_symbol(sym1);
566
567 let mut sym2 = make_symbol("delete_task");
568 sym2.file_path = PathBuf::from("src/tasks.rs");
569 g.add_symbol(sym2);
570
571 let mut sym3 = make_symbol("run_server");
572 sym3.file_path = PathBuf::from("src/main.rs");
573 g.add_symbol(sym3);
574
575 let task_syms = g.changed_symbols_for_file("src/tasks.rs");
576 assert_eq!(task_syms.len(), 2);
577 assert!(task_syms.contains(&"create_task".to_string()));
578 assert!(task_syms.contains(&"delete_task".to_string()));
579
580 let main_syms = g.changed_symbols_for_file("src/main.rs");
581 assert_eq!(main_syms.len(), 1);
582 assert!(main_syms.contains(&"run_server".to_string()));
583
584 let empty = g.changed_symbols_for_file("src/nonexistent.rs");
585 assert!(empty.is_empty());
586 }
587}