reovim_plugin_pair/
lib.rs

1//! Pair plugin for reovim - bracket matching, highlighting, and auto-pair insertion
2//!
3//! This plugin provides:
4//! - Rainbow bracket coloring based on nesting depth (6-color cycle)
5//! - Matched pair highlighting when cursor is inside brackets
6//! - Bold + underline highlighting when cursor is directly ON a bracket
7//! - Unmatched bracket warning (red underline)
8//! - Auto-insertion of closing brackets with cursor positioning
9//!
10//! # Supported Brackets
11//!
12//! - Round brackets: `(` `)`
13//! - Square brackets: `[` `]`
14//! - Curly brackets: `{` `}`
15//! - Backticks: `` ` `` (for markdown code, template literals)
16//! - Single quotes: `'`
17//! - Double quotes: `"`
18//!
19//! Note: Angle brackets `<>` are intentionally excluded as they conflict with
20//! operators (`->`, `=>`, `<=`, `>=`, `<<`, `>>`) in most languages.
21//!
22//! Symmetric pairs (backticks, quotes) use a tracking mechanism to prevent
23//! infinite recursion when auto-inserting the closing character.
24//!
25//! # Highlighting Behavior
26//!
27//! | Cursor Position | Style |
28//! |-----------------|-------|
29//! | On bracket `(` or `)` | Rainbow color + bold + underline |
30//! | Inside `(...)` | Rainbow color only |
31//! | Unmatched bracket | Red + underline |
32//!
33//! # State Management
34//!
35//! The plugin stores `PairState` in `PluginStateRegistry`, tracking:
36//! - Cached bracket depth information per buffer
37//! - Current matched pair (if cursor is inside or on a bracket)
38
39pub mod rainbow;
40pub mod stage;
41pub mod state;
42
43use std::{
44    any::TypeId,
45    sync::{
46        Arc,
47        atomic::{AtomicU8, Ordering},
48    },
49};
50
51use reovim_core::{
52    event_bus::{
53        BufferClosed, BufferModification, BufferModified, CursorMoved, EventBus, EventResult,
54        RequestInsertText,
55    },
56    plugin::{Plugin, PluginContext, PluginId, PluginStateRegistry},
57};
58
59use {stage::PairRenderStage, state::SharedPairState};
60
61/// Tracks the last auto-inserted character to prevent infinite recursion with symmetric pairs.
62/// Uses a simple encoding: 0 = none, 1 = backtick, 2 = single quote, 3 = double quote
63static LAST_AUTO_INSERT: AtomicU8 = AtomicU8::new(0);
64
65fn char_to_code(c: &str) -> u8 {
66    match c {
67        "`" => 1,
68        "'" => 2,
69        "\"" => 3,
70        _ => 0,
71    }
72}
73
74fn is_last_auto_insert(c: &str) -> bool {
75    let code = char_to_code(c);
76    code != 0 && LAST_AUTO_INSERT.swap(0, Ordering::SeqCst) == code
77}
78
79/// Pair plugin for bracket matching, rainbow coloring, and auto-pair insertion
80pub struct PairPlugin {
81    state: Arc<SharedPairState>,
82}
83
84impl Default for PairPlugin {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl PairPlugin {
91    /// Create a new pair plugin
92    #[must_use]
93    pub fn new() -> Self {
94        Self {
95            state: Arc::new(SharedPairState::new()),
96        }
97    }
98}
99
100impl Plugin for PairPlugin {
101    fn id(&self) -> PluginId {
102        PluginId::new("reovim:pair")
103    }
104
105    fn name(&self) -> &'static str {
106        "Pair"
107    }
108
109    fn description(&self) -> &'static str {
110        "Rainbow brackets, matched pair highlighting, and auto-pair insertion"
111    }
112
113    fn dependencies(&self) -> Vec<TypeId> {
114        vec![]
115    }
116
117    fn build(&self, ctx: &mut PluginContext) {
118        // Register render stage for bracket highlighting
119        let stage = Arc::new(PairRenderStage::new(Arc::clone(&self.state)));
120        ctx.register_render_stage(stage);
121
122        tracing::debug!("PairPlugin: registered render stage");
123    }
124
125    fn init_state(&self, registry: &PluginStateRegistry) {
126        // Register shared pair state
127        registry.register(Arc::clone(&self.state));
128
129        tracing::debug!("PairPlugin: initialized SharedPairState");
130    }
131
132    fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
133        // Handle cursor movement - update matched pair
134        let state_clone = Arc::clone(&state);
135        bus.subscribe::<CursorMoved, _>(100, move |event, ctx| {
136            state_clone.with::<Arc<SharedPairState>, _, _>(|pair_state| {
137                pair_state.update_cursor(event.buffer_id, event.to);
138            });
139            // Request re-render to show matched pair
140            ctx.request_render();
141            EventResult::Handled
142        });
143
144        // Handle buffer content changes - invalidate depth cache
145        let state_clone = Arc::clone(&state);
146        bus.subscribe::<BufferModified, _>(100, move |event, _ctx| {
147            state_clone.with::<Arc<SharedPairState>, _, _>(|pair_state| {
148                pair_state.invalidate_buffer(event.buffer_id);
149            });
150            tracing::trace!(buffer_id = event.buffer_id, "PairPlugin: invalidated bracket cache");
151            EventResult::Handled
152        });
153
154        // Auto-pair insertion: when an opening bracket is typed, insert the closing one
155        bus.subscribe::<BufferModified, _>(90, move |event, ctx| {
156            if let BufferModification::Insert { text, .. } = &event.modification {
157                // For symmetric pairs (`, ', "), check if this is our own auto-inserted character
158                // to prevent infinite recursion
159                if is_last_auto_insert(text) {
160                    tracing::trace!(
161                        buffer_id = event.buffer_id,
162                        text = text,
163                        "PairPlugin: skipping auto-insert for our own symmetric pair"
164                    );
165                    return EventResult::Handled;
166                }
167
168                // Only handle single-character insertions of opening brackets
169                let (close, is_symmetric) = match text.as_str() {
170                    "(" => (Some(")"), false),
171                    "[" => (Some("]"), false),
172                    "{" => (Some("}"), false),
173                    "`" => (Some("`"), true),
174                    "'" => (Some("'"), true),
175                    "\"" => (Some("\""), true),
176                    // Note: < is intentionally excluded as it's ambiguous (less-than vs angle bracket)
177                    _ => (None, false),
178                };
179
180                if let Some(closing) = close {
181                    tracing::trace!(
182                        buffer_id = event.buffer_id,
183                        open = text,
184                        close = closing,
185                        is_symmetric = is_symmetric,
186                        "PairPlugin: auto-inserting closing bracket"
187                    );
188
189                    // For symmetric pairs, track that we're inserting to prevent recursion
190                    if is_symmetric {
191                        LAST_AUTO_INSERT.store(char_to_code(closing), Ordering::SeqCst);
192                    }
193
194                    // Insert the closing bracket and move cursor back
195                    ctx.emit(RequestInsertText {
196                        text: closing.to_string(),
197                        move_cursor_left: true,
198                        delete_prefix_len: 0,
199                    });
200                }
201            }
202            EventResult::Handled
203        });
204
205        // Handle buffer close - clean up state
206        let state_clone = Arc::clone(&state);
207        bus.subscribe::<BufferClosed, _>(100, move |event, _ctx| {
208            state_clone.with::<Arc<SharedPairState>, _, _>(|pair_state| {
209                pair_state.remove_buffer(event.buffer_id);
210            });
211            tracing::trace!(
212                buffer_id = event.buffer_id,
213                "PairPlugin: cleaned up pair state for closed buffer"
214            );
215            EventResult::Handled
216        });
217
218        tracing::debug!("PairPlugin: subscribed to cursor and buffer events");
219    }
220}
221
222// Re-export types for external use
223pub use state::{BracketInfo, PairState};