use std::collections::HashMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
use dashmap::{DashMap, DashSet};
use dk_core::{CallEdge, Symbol, SymbolId};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub struct SessionGraph {
base_symbols: Option<Arc<ArcSwap<HashMap<SymbolId, Symbol>>>>,
pub(crate) modified_symbols: DashMap<SymbolId, Symbol>,
added_symbols: DashMap<SymbolId, Symbol>,
pub(crate) removed_symbols: DashSet<SymbolId>,
removed_symbol_names: DashMap<SymbolId, String>,
pub(crate) added_edges: DashMap<Uuid, CallEdge>,
pub(crate) removed_edges: DashSet<Uuid>,
}
#[derive(Serialize, Deserialize)]
struct SessionGraphSnapshot {
modified_symbols: Vec<(SymbolId, Symbol)>,
added_symbols: Vec<(SymbolId, Symbol)>,
removed_symbols: Vec<SymbolId>,
#[serde(default)]
removed_symbol_names: Vec<(SymbolId, String)>,
added_edges: Vec<(Uuid, CallEdge)>,
removed_edges: Vec<Uuid>,
}
impl SessionGraph {
pub fn fork_from(base: Arc<ArcSwap<HashMap<SymbolId, Symbol>>>) -> Self {
Self {
base_symbols: Some(base),
modified_symbols: DashMap::new(),
added_symbols: DashMap::new(),
removed_symbols: DashSet::new(),
removed_symbol_names: DashMap::new(),
added_edges: DashMap::new(),
removed_edges: DashSet::new(),
}
}
pub fn empty() -> Self {
Self {
base_symbols: None,
modified_symbols: DashMap::new(),
added_symbols: DashMap::new(),
removed_symbols: DashSet::new(),
removed_symbol_names: DashMap::new(),
added_edges: DashMap::new(),
removed_edges: DashSet::new(),
}
}
pub fn get_symbol(&self, id: SymbolId) -> Option<Symbol> {
if self.removed_symbols.contains(&id) {
return None;
}
if let Some(sym) = self.modified_symbols.get(&id) {
return Some(sym.value().clone());
}
if let Some(sym) = self.added_symbols.get(&id) {
return Some(sym.value().clone());
}
if let Some(base) = &self.base_symbols {
let snapshot = base.load();
return snapshot.get(&id).cloned();
}
None
}
pub fn add_symbol(&self, symbol: Symbol) {
self.added_symbols.insert(symbol.id, symbol);
}
pub fn modify_symbol(&self, symbol: Symbol) {
let id = symbol.id;
if self.added_symbols.contains_key(&id) {
self.added_symbols.insert(id, symbol);
} else {
self.modified_symbols.insert(id, symbol);
}
}
pub fn remove_symbol(&self, id: SymbolId) {
if self.added_symbols.remove(&id).is_some() {
return;
}
if let Some((_, sym)) = self.modified_symbols.remove(&id) {
self.removed_symbol_names
.insert(id, sym.qualified_name.clone());
} else if let Some(base) = &self.base_symbols {
let snapshot = base.load();
if let Some(sym) = snapshot.get(&id) {
self.removed_symbol_names
.insert(id, sym.qualified_name.clone());
}
}
self.removed_symbols.insert(id);
}
pub fn add_edge(&self, edge: CallEdge) {
self.added_edges.insert(edge.id, edge);
}
pub fn remove_edge(&self, edge_id: Uuid) {
if self.added_edges.remove(&edge_id).is_some() {
return;
}
self.removed_edges.insert(edge_id);
}
pub fn get_edge(&self, edge_id: Uuid) -> Option<CallEdge> {
if self.removed_edges.contains(&edge_id) {
return None;
}
self.added_edges.get(&edge_id).map(|e| e.value().clone())
}
pub fn is_edge_removed(&self, edge_id: Uuid) -> bool {
self.removed_edges.contains(&edge_id)
}
pub fn changed_symbol_names(&self) -> Vec<String> {
let mut names = Vec::new();
for entry in self.added_symbols.iter() {
names.push(entry.value().qualified_name.clone());
}
for entry in self.modified_symbols.iter() {
names.push(entry.value().qualified_name.clone());
}
for id in self.removed_symbols.iter() {
let found = self
.base_symbols
.as_ref()
.and_then(|base| base.load().get(id.key()).map(|s| s.qualified_name.clone()));
if let Some(name) = found {
names.push(name);
} else if let Some(name) = self.removed_symbol_names.get(id.key()) {
names.push(name.value().clone());
}
}
names
}
pub fn remove_session_symbols_for_file(&self, file_path: &str) {
let target = std::path::Path::new(file_path);
let added_ids: Vec<SymbolId> = self
.added_symbols
.iter()
.filter(|e| e.value().file_path == target)
.map(|e| *e.key())
.collect();
let modified_ids: Vec<SymbolId> = self
.modified_symbols
.iter()
.filter(|e| e.value().file_path == target)
.map(|e| *e.key())
.collect();
for id in added_ids {
self.added_symbols.remove(&id);
}
for id in modified_ids {
self.remove_symbol(id);
}
}
pub fn update_from_parse(
&self,
_file_path: &str,
new_symbols: Vec<Symbol>,
base_symbols_for_file: &[Symbol],
) {
let base_by_name: HashMap<&str, &Symbol> = base_symbols_for_file
.iter()
.map(|s| (s.qualified_name.as_str(), s))
.collect();
let new_by_name: HashMap<&str, &Symbol> = new_symbols
.iter()
.map(|s| (s.qualified_name.as_str(), s))
.collect();
for sym in &new_symbols {
if let Some(base_sym) = base_by_name.get(sym.qualified_name.as_str()) {
if sym.span != base_sym.span
|| sym.signature != base_sym.signature
|| sym.kind != base_sym.kind
|| sym.visibility != base_sym.visibility
{
self.modify_symbol(sym.clone());
}
} else {
self.add_symbol(sym.clone());
}
}
for base_sym in base_symbols_for_file {
if !new_by_name.contains_key(base_sym.qualified_name.as_str()) {
self.remove_symbol(base_sym.id);
}
}
}
pub fn changed_symbols_for_file(&self, file_path: &str) -> Vec<String> {
let target = std::path::Path::new(file_path);
let mut names = Vec::new();
for entry in self.added_symbols.iter() {
if entry.value().file_path == target {
names.push(entry.value().name.clone());
}
}
for entry in self.modified_symbols.iter() {
if entry.value().file_path == target {
names.push(entry.value().name.clone());
}
}
names
}
pub fn change_count(&self) -> usize {
self.added_symbols.len() + self.modified_symbols.len() + self.removed_symbols.len()
}
pub fn to_msgpack(&self) -> anyhow::Result<Vec<u8>> {
let snapshot = SessionGraphSnapshot {
modified_symbols: self
.modified_symbols
.iter()
.map(|e| (*e.key(), e.value().clone()))
.collect(),
added_symbols: self
.added_symbols
.iter()
.map(|e| (*e.key(), e.value().clone()))
.collect(),
removed_symbols: self.removed_symbols.iter().map(|r| *r).collect(),
removed_symbol_names: self
.removed_symbol_names
.iter()
.map(|e| (*e.key(), e.value().clone()))
.collect(),
added_edges: self
.added_edges
.iter()
.map(|e| (*e.key(), e.value().clone()))
.collect(),
removed_edges: self.removed_edges.iter().map(|r| *r).collect(),
};
Ok(rmp_serde::to_vec_named(&snapshot)?)
}
pub fn from_msgpack(bytes: &[u8]) -> anyhow::Result<Self> {
let snapshot: SessionGraphSnapshot = rmp_serde::from_slice(bytes)?;
let modified_symbols = DashMap::new();
for (id, sym) in snapshot.modified_symbols {
modified_symbols.insert(id, sym);
}
let added_symbols = DashMap::new();
for (id, sym) in snapshot.added_symbols {
added_symbols.insert(id, sym);
}
let removed_symbols = DashSet::new();
for id in snapshot.removed_symbols {
removed_symbols.insert(id);
}
let removed_symbol_names = DashMap::new();
for (id, name) in snapshot.removed_symbol_names {
removed_symbol_names.insert(id, name);
}
let added_edges = DashMap::new();
for (id, edge) in snapshot.added_edges {
added_edges.insert(id, edge);
}
let removed_edges = DashSet::new();
for id in snapshot.removed_edges {
removed_edges.insert(id);
}
Ok(Self {
base_symbols: None,
modified_symbols,
added_symbols,
removed_symbols,
removed_symbol_names,
added_edges,
removed_edges,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use dk_core::{Span, SymbolKind, Visibility};
use std::path::PathBuf;
fn make_symbol(name: &str) -> Symbol {
Symbol {
id: Uuid::new_v4(),
name: name.to_string(),
qualified_name: name.to_string(),
kind: SymbolKind::Function,
visibility: Visibility::Public,
file_path: PathBuf::from("test.rs"),
span: Span {
start_byte: 0,
end_byte: 10,
},
signature: None,
doc_comment: None,
parent: None,
last_modified_by: None,
last_modified_intent: None,
}
}
#[test]
fn empty_graph_returns_none() {
let g = SessionGraph::empty();
assert!(g.get_symbol(Uuid::new_v4()).is_none());
}
#[test]
fn add_and_get_symbol() {
let g = SessionGraph::empty();
let sym = make_symbol("foo");
let id = sym.id;
g.add_symbol(sym);
assert!(g.get_symbol(id).is_some());
assert_eq!(g.get_symbol(id).unwrap().name, "foo");
}
#[test]
fn remove_added_symbol() {
let g = SessionGraph::empty();
let sym = make_symbol("bar");
let id = sym.id;
g.add_symbol(sym);
g.remove_symbol(id);
assert!(g.get_symbol(id).is_none());
}
#[test]
fn modify_added_symbol_updates_in_place() {
let g = SessionGraph::empty();
let mut sym = make_symbol("baz");
let id = sym.id;
g.add_symbol(sym.clone());
sym.name = "baz_v2".to_string();
g.modify_symbol(sym);
let got = g.get_symbol(id).unwrap();
assert_eq!(got.name, "baz_v2");
}
#[test]
fn fork_from_base_lookup() {
let mut base = HashMap::new();
let sym = make_symbol("base_fn");
let id = sym.id;
base.insert(id, sym);
let shared = Arc::new(ArcSwap::from_pointee(base));
let g = SessionGraph::fork_from(shared);
assert!(g.get_symbol(id).is_some());
assert_eq!(g.get_symbol(id).unwrap().name, "base_fn");
}
#[test]
fn remove_base_symbol_hides_it() {
let mut base = HashMap::new();
let sym = make_symbol("base_fn");
let id = sym.id;
base.insert(id, sym);
let shared = Arc::new(ArcSwap::from_pointee(base));
let g = SessionGraph::fork_from(shared);
g.remove_symbol(id);
assert!(g.get_symbol(id).is_none());
}
#[test]
fn changed_symbol_names_collects_all() {
let mut base = HashMap::new();
let sym = make_symbol("removed_fn");
let removed_id = sym.id;
base.insert(removed_id, sym);
let shared = Arc::new(ArcSwap::from_pointee(base));
let g = SessionGraph::fork_from(shared);
let added = make_symbol("added_fn");
g.add_symbol(added);
let mut modified = make_symbol("modified_fn");
modified.id = Uuid::new_v4();
let mid = modified.id;
g.modified_symbols.insert(mid, modified);
g.remove_symbol(removed_id);
let names = g.changed_symbol_names();
assert!(names.contains(&"added_fn".to_string()));
assert!(names.contains(&"modified_fn".to_string()));
assert!(names.contains(&"removed_fn".to_string()));
}
#[test]
fn change_count() {
let g = SessionGraph::empty();
assert_eq!(g.change_count(), 0);
g.add_symbol(make_symbol("a"));
assert_eq!(g.change_count(), 1);
}
#[test]
fn changed_symbols_for_file_filters_by_path() {
let g = SessionGraph::empty();
let mut sym1 = make_symbol("create_task");
sym1.file_path = PathBuf::from("src/tasks.rs");
g.add_symbol(sym1);
let mut sym2 = make_symbol("delete_task");
sym2.file_path = PathBuf::from("src/tasks.rs");
g.add_symbol(sym2);
let mut sym3 = make_symbol("run_server");
sym3.file_path = PathBuf::from("src/main.rs");
g.add_symbol(sym3);
let task_syms = g.changed_symbols_for_file("src/tasks.rs");
assert_eq!(task_syms.len(), 2);
assert!(task_syms.contains(&"create_task".to_string()));
assert!(task_syms.contains(&"delete_task".to_string()));
let main_syms = g.changed_symbols_for_file("src/main.rs");
assert_eq!(main_syms.len(), 1);
assert!(main_syms.contains(&"run_server".to_string()));
let empty = g.changed_symbols_for_file("src/nonexistent.rs");
assert!(empty.is_empty());
}
}