1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3use std::collections::HashMap;
14
15use reovim_client_driver::{
16 Attributes, ClientModule, ClientModuleError, InlineDecoration, ModuleContext, ProbeResult,
17 Style, Version,
18};
19
20use {reovim_arch::Color, serde::Deserialize};
21
22const RAINBOW_COLORS: [Color; 6] = [
24 Color::Rgb {
25 r: 255,
26 g: 215,
27 b: 0,
28 }, Color::Rgb {
30 r: 218,
31 g: 112,
32 b: 214,
33 }, Color::Rgb {
35 r: 0,
36 g: 191,
37 b: 255,
38 }, Color::Rgb {
40 r: 50,
41 g: 205,
42 b: 50,
43 }, Color::Rgb {
45 r: 255,
46 g: 127,
47 b: 80,
48 }, Color::Rgb {
50 r: 147,
51 g: 112,
52 b: 219,
53 }, ];
55
56const UNMATCHED_COLOR: Color = Color::Rgb {
58 r: 255,
59 g: 80,
60 b: 80,
61};
62
63const RAINBOW_COUNT: usize = RAINBOW_COLORS.len();
65
66#[derive(Debug, Deserialize)]
67struct BracketEntry {
68 line: u32,
69 col: u32,
70 depth: u64,
71 #[allow(dead_code)]
72 char: String,
73 unmatched: bool,
74}
75
76#[derive(Debug, Deserialize)]
77struct MatchedEntry {
78 open: PosEntry,
79 close: PosEntry,
80}
81
82#[derive(Debug, Deserialize)]
83struct PosEntry {
84 line: u32,
85 col: u32,
86}
87
88#[derive(Deserialize)]
89struct PairNotification {
90 #[serde(default)]
91 active: bool,
92 #[serde(default = "default_true")]
93 rainbow: bool,
94 #[serde(default = "default_true")]
95 matchpair: bool,
96 #[serde(default)]
97 brackets: Vec<BracketEntry>,
98 #[serde(default)]
99 matched: Option<MatchedEntry>,
100}
101
102const fn default_true() -> bool {
103 true
104}
105
106pub struct PairModule {
113 active: bool,
114 rainbow_enabled: bool,
115 matchpair_enabled: bool,
116 brackets: Vec<BracketEntry>,
117 matched: Option<MatchedEntry>,
118 decorations_by_line: HashMap<usize, Vec<InlineDecoration>>,
120}
121
122impl PairModule {
123 #[must_use]
124 pub fn new() -> Self {
125 Self {
126 active: false,
127 rainbow_enabled: true,
128 matchpair_enabled: true,
129 brackets: Vec::new(),
130 matched: None,
131 decorations_by_line: HashMap::new(),
132 }
133 }
134
135 #[allow(clippy::cast_possible_truncation)]
137 fn rebuild_decorations(&mut self) {
138 self.decorations_by_line.clear();
139
140 if !self.active {
141 return;
142 }
143
144 if self.rainbow_enabled {
146 for bracket in &self.brackets {
147 let style = if bracket.unmatched {
148 let mut attrs = Attributes::new();
149 attrs.set(Attributes::UNDERLINE);
150 Style {
151 fg: Some(UNMATCHED_COLOR),
152 attributes: attrs,
153 ..Style::default()
154 }
155 } else {
156 let color_idx = (bracket.depth as usize) % RAINBOW_COUNT;
157 Style {
158 fg: Some(RAINBOW_COLORS[color_idx]),
159 ..Style::default()
160 }
161 };
162
163 self.decorations_by_line
164 .entry(bracket.line as usize)
165 .or_default()
166 .push(InlineDecoration {
167 col_start: bracket.col as u16,
168 col_end: bracket.col as u16 + 1,
169 style,
170 });
171 }
172 }
173
174 if self.matchpair_enabled
176 && let Some(ref matched) = self.matched
177 {
178 let mut attrs = Attributes::new();
179 attrs.set(Attributes::BOLD | Attributes::UNDERLINE);
180 let style = Style {
181 attributes: attrs,
182 ..Style::default()
183 };
184
185 self.decorations_by_line
186 .entry(matched.open.line as usize)
187 .or_default()
188 .push(InlineDecoration {
189 col_start: matched.open.col as u16,
190 col_end: matched.open.col as u16 + 1,
191 style: style.clone(),
192 });
193
194 self.decorations_by_line
195 .entry(matched.close.line as usize)
196 .or_default()
197 .push(InlineDecoration {
198 col_start: matched.close.col as u16,
199 col_end: matched.close.col as u16 + 1,
200 style,
201 });
202 }
203 }
204}
205
206impl Default for PairModule {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212#[cfg_attr(coverage_nightly, coverage(off))]
213impl ClientModule for PairModule {
214 fn id(&self) -> &'static str {
215 "pair"
216 }
217
218 fn kind(&self) -> &'static str {
219 "pair"
220 }
221
222 fn name(&self) -> &'static str {
223 "Bracket Pair"
224 }
225
226 fn version(&self) -> Version {
227 Version::new(0, 1, 0)
228 }
229
230 fn init(&mut self, _ctx: &ModuleContext) -> ProbeResult {
231 ProbeResult::Success
232 }
233
234 fn exit(&mut self) -> Result<(), ClientModuleError> {
235 Ok(())
236 }
237
238 fn has_buffer_contrib(&self) -> bool {
239 self.active
240 }
241
242 fn on_notification(&mut self, data: &str) {
243 let Ok(notification) = serde_json::from_str::<PairNotification>(data) else {
244 return;
245 };
246
247 self.active = notification.active;
248 self.rainbow_enabled = notification.rainbow;
249 self.matchpair_enabled = notification.matchpair;
250 self.brackets = notification.brackets;
251 self.matched = notification.matched;
252
253 self.rebuild_decorations();
254 }
255
256 fn inline_decorations(&self, line: usize) -> &[InlineDecoration] {
257 self.decorations_by_line
258 .get(&line)
259 .map_or(&[], Vec::as_slice)
260 }
261}
262
263#[cfg(test)]
264mod tests;