reovim_driver_syntax/scope.rs
1//! Scope context types for navigation and breadcrumbs.
2//!
3//! This module defines types for representing scope boundaries in source code,
4//! such as function definitions, class/struct declarations, and module blocks.
5//! Language modules provide scope queries; consumer modules (context, sticky-context)
6//! use them for statusline breadcrumbs and viewport headers.
7
8use std::fmt;
9
10/// Kind of scope (what construct it represents).
11///
12/// Classifies scope boundaries by the type of syntax construct.
13///
14/// # Example
15///
16/// ```
17/// use reovim_driver_syntax::ScopeKind;
18///
19/// let kind = ScopeKind::Function;
20/// assert!(kind.is_definition());
21/// assert_eq!(kind.as_str(), "fn");
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum ScopeKind {
25 /// Function or method definition.
26 Function,
27 /// Class, struct, enum, trait, or impl block.
28 Class,
29 /// Module or namespace.
30 Module,
31 /// Generic block (if, for, while, match, etc.).
32 Block,
33 /// Heading (markdown, org-mode, etc.).
34 Heading,
35 /// Namespace (C++, etc.).
36 Namespace,
37}
38
39impl ScopeKind {
40 /// Check if this scope represents a definition (function, class, module, namespace).
41 #[must_use]
42 pub const fn is_definition(self) -> bool {
43 matches!(self, Self::Function | Self::Class | Self::Module | Self::Namespace)
44 }
45
46 /// Get a short human-readable label for this scope kind.
47 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Self::Function => "fn",
51 Self::Class => "class",
52 Self::Module => "mod",
53 Self::Block => "block",
54 Self::Heading => "heading",
55 Self::Namespace => "namespace",
56 }
57 }
58}
59
60impl fmt::Display for ScopeKind {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(self.as_str())
63 }
64}
65
66/// A scope boundary range in the buffer.
67///
68/// Represents a contiguous region of source code that defines a scope boundary,
69/// identified by the syntax driver. Lines are 0-indexed.
70///
71/// # Example
72///
73/// ```
74/// use reovim_driver_syntax::{ScopeRange, ScopeKind};
75///
76/// let scope = ScopeRange::new(5, 20, ScopeKind::Function, "fn main", Some("main".to_string()));
77/// assert!(scope.contains_line(10));
78/// assert!(scope.is_multiline());
79/// assert_eq!(scope.line_count(), 16);
80/// ```
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ScopeRange {
83 /// Starting line (0-indexed).
84 pub start_line: u32,
85 /// Ending line (0-indexed, inclusive).
86 pub end_line: u32,
87 /// Kind of scope.
88 pub kind: ScopeKind,
89 /// Display text (e.g., "fn main", "impl Foo", "H2 Architecture").
90 pub display_text: String,
91 /// Just the identifier name (e.g., "main", "Foo"), if available.
92 pub name: Option<String>,
93}
94
95impl ScopeRange {
96 /// Create a new scope range.
97 #[must_use]
98 pub fn new(
99 start_line: u32,
100 end_line: u32,
101 kind: ScopeKind,
102 display_text: impl Into<String>,
103 name: Option<String>,
104 ) -> Self {
105 Self {
106 start_line,
107 end_line,
108 kind,
109 display_text: display_text.into(),
110 name,
111 }
112 }
113
114 /// Check if this scope contains a line.
115 #[must_use]
116 pub const fn contains_line(&self, line: u32) -> bool {
117 line >= self.start_line && line <= self.end_line
118 }
119
120 /// Get the number of lines in this scope.
121 #[must_use]
122 pub const fn line_count(&self) -> u32 {
123 self.end_line - self.start_line + 1
124 }
125
126 /// Check if this scope spans multiple lines.
127 #[must_use]
128 pub const fn is_multiline(&self) -> bool {
129 self.end_line > self.start_line
130 }
131}
132
133/// Hierarchy of enclosing scopes at a cursor position.
134///
135/// Items are ordered outermost-first (module -> class -> function).
136/// Used by consumer modules to build statusline breadcrumbs and
137/// sticky-context viewport headers.
138///
139/// # Example
140///
141/// ```
142/// use reovim_driver_syntax::{ContextHierarchy, ScopeRange, ScopeKind};
143///
144/// let scopes = vec![
145/// ScopeRange::new(0, 100, ScopeKind::Module, "mod utils", Some("utils".to_string())),
146/// ScopeRange::new(5, 20, ScopeKind::Function, "fn main", Some("main".to_string())),
147/// ];
148/// let ctx = ContextHierarchy::new(0, 10, 5, scopes);
149///
150/// assert_eq!(ctx.len(), 2);
151/// assert_eq!(ctx.to_breadcrumb(" > "), "mod utils > fn main");
152/// ```
153#[derive(Debug, Clone, Default)]
154pub struct ContextHierarchy {
155 /// Scopes from outermost to innermost.
156 pub items: Vec<ScopeRange>,
157 /// Buffer identifier (set by the module layer, not the driver).
158 pub buffer_id: usize,
159 /// Line where context was computed (0-indexed).
160 pub line: u32,
161 /// Column where context was computed (0-indexed).
162 pub col: u32,
163}
164
165impl ContextHierarchy {
166 /// Create a new context hierarchy.
167 #[must_use]
168 pub const fn new(buffer_id: usize, line: u32, col: u32, items: Vec<ScopeRange>) -> Self {
169 Self {
170 items,
171 buffer_id,
172 line,
173 col,
174 }
175 }
176
177 /// Create an empty context hierarchy.
178 #[must_use]
179 pub const fn empty() -> Self {
180 Self {
181 items: Vec::new(),
182 buffer_id: 0,
183 line: 0,
184 col: 0,
185 }
186 }
187
188 /// Check if the hierarchy is empty (no enclosing scopes).
189 #[must_use]
190 pub const fn is_empty(&self) -> bool {
191 self.items.is_empty()
192 }
193
194 /// Get the number of enclosing scopes.
195 #[must_use]
196 pub const fn len(&self) -> usize {
197 self.items.len()
198 }
199
200 /// Get the innermost (current) scope.
201 #[must_use]
202 pub fn current_scope(&self) -> Option<&ScopeRange> {
203 self.items.last()
204 }
205
206 /// Format as a breadcrumb string with the given separator.
207 ///
208 /// Example: `"mod utils > fn main > impl Foo"`
209 #[must_use]
210 pub fn to_breadcrumb(&self, separator: &str) -> String {
211 self.items
212 .iter()
213 .map(|s| s.display_text.as_str())
214 .collect::<Vec<_>>()
215 .join(separator)
216 }
217
218 /// Format as a breadcrumb string with a maximum number of items.
219 ///
220 /// When there are more items than `max`, the outermost items are
221 /// dropped and replaced with "...".
222 ///
223 /// # Example
224 ///
225 /// With 5 scopes and max=3: `"... > class Foo > fn bar"`
226 #[must_use]
227 pub fn to_breadcrumb_max(&self, separator: &str, max: usize) -> String {
228 if self.items.len() <= max {
229 return self.to_breadcrumb(separator);
230 }
231
232 let skip = self.items.len() - max;
233 let mut parts = vec!["..."];
234 parts.extend(self.items[skip..].iter().map(|s| s.display_text.as_str()));
235 parts.join(separator)
236 }
237}
238
239#[cfg(test)]
240#[path = "scope_tests.rs"]
241mod tests;