Skip to main content

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;