Skip to main content

editor_core/
intelligence.rs

1//! UI-agnostic language intelligence data models.
2//!
3//! This module provides a small set of **typed** schemas that hosts can use to represent common
4//! “code editor intelligence” surfaces in a consistent way across UIs:
5//!
6//! - references result collections
7//! - call hierarchy (incoming/outgoing)
8//! - type hierarchy (supertypes/subtypes)
9//!
10//! The core idea is that integrations (LSP, Tree-sitter, bespoke engines) can populate these
11//! structures and store them in [`WorkspaceIntelligence`] for later consumption by UI layers.
12
13use crate::symbols::{SymbolKind, SymbolLocation, Utf16Range};
14use std::collections::BTreeMap;
15
16/// Opaque id for an intelligence result set stored in a workspace.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct ResultSetId(u64);
19
20impl ResultSetId {
21    /// Return the underlying numeric id.
22    pub fn get(self) -> u64 {
23        self.0
24    }
25}
26
27/// High-level kind of a stored result set.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ResultSetKind {
30    /// A references/search-like collection of locations.
31    References,
32    /// Call hierarchy (incoming/outgoing call edges).
33    CallHierarchy,
34    /// Type hierarchy (supertypes/subtypes).
35    TypeHierarchy,
36}
37
38/// A generic cross-file “hierarchy item” (call hierarchy / type hierarchy).
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct HierarchyItem {
41    /// Display name (e.g. function/type name).
42    pub name: String,
43    /// Optional detail string (e.g. signature).
44    pub detail: Option<String>,
45    /// Coarse kind tag (usually from LSP's `SymbolKind`).
46    pub kind: SymbolKind,
47    /// Cross-file location for navigation.
48    pub location: SymbolLocation,
49    /// Preferred selection range within the target (usually a tighter span than `location.range`).
50    pub selection_range: Utf16Range,
51    /// Optional raw integration payload, encoded as JSON text.
52    pub data_json: Option<String>,
53}
54
55/// A references result collection.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ReferencesResultSet {
58    /// UI-facing title for the collection (e.g. "References: foo").
59    pub title: String,
60    /// Reference locations.
61    pub locations: Vec<SymbolLocation>,
62    /// Whether any referenced document has changed since this collection was produced.
63    pub is_stale: bool,
64}
65
66impl ReferencesResultSet {
67    fn mentions_uri(&self, uri: &str) -> bool {
68        self.locations.iter().any(|loc| loc.uri == uri)
69    }
70}
71
72/// A call hierarchy incoming edge (`from -> root`).
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CallHierarchyIncomingCall {
75    /// The caller item.
76    pub from: HierarchyItem,
77    /// Callsite ranges within `from` (UTF-16 ranges).
78    pub from_ranges: Vec<Utf16Range>,
79}
80
81impl CallHierarchyIncomingCall {
82    fn mentions_uri(&self, uri: &str) -> bool {
83        self.from.location.uri == uri
84    }
85}
86
87/// A call hierarchy outgoing edge (`root -> to`).
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct CallHierarchyOutgoingCall {
90    /// The callee item.
91    pub to: HierarchyItem,
92    /// Callsite ranges within the root item (UTF-16 ranges).
93    pub from_ranges: Vec<Utf16Range>,
94}
95
96impl CallHierarchyOutgoingCall {
97    fn mentions_uri(&self, uri: &str) -> bool {
98        self.to.location.uri == uri
99    }
100}
101
102/// A call hierarchy result set.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct CallHierarchyResultSet {
105    /// UI-facing title for the collection (e.g. "Call Hierarchy: foo").
106    pub title: String,
107    /// Prepared root items (some servers return multiple).
108    pub roots: Vec<HierarchyItem>,
109    /// Incoming calls (if resolved).
110    pub incoming: Vec<CallHierarchyIncomingCall>,
111    /// Outgoing calls (if resolved).
112    pub outgoing: Vec<CallHierarchyOutgoingCall>,
113    /// Whether any referenced document has changed since this collection was produced.
114    pub is_stale: bool,
115}
116
117impl CallHierarchyResultSet {
118    fn mentions_uri(&self, uri: &str) -> bool {
119        self.roots.iter().any(|it| it.location.uri == uri)
120            || self.incoming.iter().any(|c| c.mentions_uri(uri))
121            || self.outgoing.iter().any(|c| c.mentions_uri(uri))
122    }
123}
124
125/// A type hierarchy result set.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct TypeHierarchyResultSet {
128    /// UI-facing title for the collection (e.g. "Type Hierarchy: Foo").
129    pub title: String,
130    /// Prepared root items (some servers return multiple).
131    pub roots: Vec<HierarchyItem>,
132    /// Supertypes (if resolved).
133    pub supertypes: Vec<HierarchyItem>,
134    /// Subtypes (if resolved).
135    pub subtypes: Vec<HierarchyItem>,
136    /// Whether any referenced document has changed since this collection was produced.
137    pub is_stale: bool,
138}
139
140impl TypeHierarchyResultSet {
141    fn mentions_uri(&self, uri: &str) -> bool {
142        self.roots.iter().any(|it| it.location.uri == uri)
143            || self.supertypes.iter().any(|it| it.location.uri == uri)
144            || self.subtypes.iter().any(|it| it.location.uri == uri)
145    }
146}
147
148/// A stored intelligence result set.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum IntelligenceResultSet {
151    /// References/search-like results.
152    References(ReferencesResultSet),
153    /// Call hierarchy results.
154    CallHierarchy(CallHierarchyResultSet),
155    /// Type hierarchy results.
156    TypeHierarchy(TypeHierarchyResultSet),
157}
158
159impl IntelligenceResultSet {
160    /// Return the high-level kind of this result set.
161    pub fn kind(&self) -> ResultSetKind {
162        match self {
163            Self::References(_) => ResultSetKind::References,
164            Self::CallHierarchy(_) => ResultSetKind::CallHierarchy,
165            Self::TypeHierarchy(_) => ResultSetKind::TypeHierarchy,
166        }
167    }
168
169    /// Return the UI-facing title.
170    pub fn title(&self) -> &str {
171        match self {
172            Self::References(s) => s.title.as_str(),
173            Self::CallHierarchy(s) => s.title.as_str(),
174            Self::TypeHierarchy(s) => s.title.as_str(),
175        }
176    }
177
178    /// Return whether the result set is stale.
179    pub fn is_stale(&self) -> bool {
180        match self {
181            Self::References(s) => s.is_stale,
182            Self::CallHierarchy(s) => s.is_stale,
183            Self::TypeHierarchy(s) => s.is_stale,
184        }
185    }
186
187    /// Mark the result set as stale.
188    pub fn mark_stale(&mut self) {
189        match self {
190            Self::References(s) => s.is_stale = true,
191            Self::CallHierarchy(s) => s.is_stale = true,
192            Self::TypeHierarchy(s) => s.is_stale = true,
193        }
194    }
195
196    fn mentions_uri(&self, uri: &str) -> bool {
197        match self {
198            Self::References(s) => s.mentions_uri(uri),
199            Self::CallHierarchy(s) => s.mentions_uri(uri),
200            Self::TypeHierarchy(s) => s.mentions_uri(uri),
201        }
202    }
203}
204
205/// Workspace-owned storage for language intelligence result sets.
206#[derive(Debug, Default)]
207pub struct WorkspaceIntelligence {
208    next_id: u64,
209    sets: BTreeMap<ResultSetId, IntelligenceResultSet>,
210}
211
212impl WorkspaceIntelligence {
213    /// Return the number of stored result sets.
214    pub fn len(&self) -> usize {
215        self.sets.len()
216    }
217
218    /// Returns `true` if there are no stored result sets.
219    pub fn is_empty(&self) -> bool {
220        self.sets.is_empty()
221    }
222
223    /// List all stored ids in deterministic order.
224    pub fn ids(&self) -> Vec<ResultSetId> {
225        self.sets.keys().cloned().collect()
226    }
227
228    /// Get a stored result set by id.
229    pub fn get(&self, id: ResultSetId) -> Option<&IntelligenceResultSet> {
230        self.sets.get(&id)
231    }
232
233    /// Get a mutable stored result set by id.
234    pub fn get_mut(&mut self, id: ResultSetId) -> Option<&mut IntelligenceResultSet> {
235        self.sets.get_mut(&id)
236    }
237
238    /// Remove a stored result set.
239    pub fn remove(&mut self, id: ResultSetId) -> Option<IntelligenceResultSet> {
240        self.sets.remove(&id)
241    }
242
243    /// Clear all stored result sets.
244    pub fn clear(&mut self) {
245        self.sets.clear();
246    }
247
248    /// Insert a new result set and return its generated id.
249    pub fn create(&mut self, set: IntelligenceResultSet) -> ResultSetId {
250        let id = ResultSetId(self.next_id);
251        self.next_id = self.next_id.saturating_add(1);
252        self.sets.insert(id, set);
253        id
254    }
255
256    /// Convenience helper: create a references result set.
257    pub fn create_references(
258        &mut self,
259        title: impl Into<String>,
260        locations: Vec<SymbolLocation>,
261    ) -> ResultSetId {
262        self.create(IntelligenceResultSet::References(ReferencesResultSet {
263            title: title.into(),
264            locations,
265            is_stale: false,
266        }))
267    }
268
269    /// Mark any result set that mentions `uri` as stale.
270    ///
271    /// Returns `true` if at least one result set changed.
272    pub fn mark_stale_for_uri(&mut self, uri: &str) -> bool {
273        let mut changed = false;
274        for set in self.sets.values_mut() {
275            if set.is_stale() {
276                continue;
277            }
278            if set.mentions_uri(uri) {
279                set.mark_stale();
280                changed = true;
281            }
282        }
283        changed
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use crate::symbols::{Utf16Position, Utf16Range};
291
292    #[test]
293    fn mark_stale_for_uri_marks_matching_sets_only() {
294        let mut intel = WorkspaceIntelligence::default();
295
296        let a_loc = SymbolLocation {
297            uri: "file:///a.rs".to_string(),
298            range: Utf16Range::new(Utf16Position::new(0, 0), Utf16Position::new(0, 1)),
299        };
300        let b_loc = SymbolLocation {
301            uri: "file:///b.rs".to_string(),
302            range: Utf16Range::new(Utf16Position::new(1, 0), Utf16Position::new(1, 1)),
303        };
304
305        let a_id = intel.create_references("refs a", vec![a_loc.clone()]);
306        let b_id = intel.create_references("refs b", vec![b_loc.clone()]);
307
308        assert!(!intel.get(a_id).unwrap().is_stale());
309        assert!(!intel.get(b_id).unwrap().is_stale());
310
311        assert!(intel.mark_stale_for_uri("file:///a.rs"));
312        assert!(intel.get(a_id).unwrap().is_stale());
313        assert!(!intel.get(b_id).unwrap().is_stale());
314
315        // No-op on a second mark.
316        assert!(!intel.mark_stale_for_uri("file:///a.rs"));
317    }
318}