1mod input;
21mod render;
22
23use ratatui::layout::Rect;
24use ratatui::style::Color;
25
26pub use input::MapEvent;
27pub use render::render_map;
28
29use super::FocusState;
30
31#[derive(Debug, Clone)]
33pub struct MapState {
34 pub entries: Vec<(String, serde_json::Value)>,
36 pub focused_entry: Option<usize>,
38 pub new_key_text: String,
40 pub cursor: usize,
42 pub label: String,
44 pub focus: FocusState,
46 pub expanded: Vec<usize>,
48 pub value_schema: Option<Box<crate::view::settings::schema::SettingSchema>>,
50 pub display_field: Option<String>,
52 pub no_add: bool,
54}
55
56impl MapState {
57 pub fn new(label: impl Into<String>) -> Self {
59 Self {
60 entries: Vec::new(),
61 focused_entry: None,
62 new_key_text: String::new(),
63 cursor: 0,
64 label: label.into(),
65 focus: FocusState::Normal,
66 expanded: Vec::new(),
67 value_schema: None,
68 display_field: None,
69 no_add: false,
70 }
71 }
72
73 pub fn with_display_field(mut self, field: String) -> Self {
75 self.display_field = Some(field);
76 self
77 }
78
79 pub fn with_no_add(mut self, no_add: bool) -> Self {
81 self.no_add = no_add;
82 self
83 }
84
85 pub fn get_display_value(&self, value: &serde_json::Value) -> String {
87 if let Some(ref field) = self.display_field {
88 if let serde_json::Value::Array(arr) = value {
94 let parts: Vec<String> = arr
95 .iter()
96 .filter_map(|el| el.pointer(field))
97 .filter_map(|v| match v {
98 serde_json::Value::String(s) => Some(s.clone()),
99 serde_json::Value::Bool(b) => Some(b.to_string()),
100 serde_json::Value::Number(n) => Some(n.to_string()),
101 _ => None,
102 })
103 .collect();
104 if !parts.is_empty() {
105 let joined = parts.join(", ");
111 if joined.chars().count() <= 20 || parts.len() == 1 {
112 return joined;
113 }
114 return format!("{}, +{} more", parts[0], parts.len() - 1);
115 }
116 } else if let Some(v) = value.pointer(field) {
117 return match v {
118 serde_json::Value::String(s) => s.clone(),
119 serde_json::Value::Bool(b) => b.to_string(),
120 serde_json::Value::Number(n) => n.to_string(),
121 serde_json::Value::Null => "null".to_string(),
122 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
123 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
124 };
125 }
126 }
127 match value {
129 serde_json::Value::Object(obj) => {
130 let n = obj.len();
131 if n == 1 {
132 "1 field".to_string()
133 } else {
134 format!("{n} fields")
135 }
136 }
137 serde_json::Value::Array(arr) => {
138 let n = arr.len();
139 if n == 1 {
140 "1 item".to_string()
141 } else {
142 format!("{n} items")
143 }
144 }
145 other => other.to_string(),
146 }
147 }
148
149 pub fn with_entries(mut self, value: &serde_json::Value) -> Self {
151 if let Some(obj) = value.as_object() {
152 self.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
153 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
155 if !self.entries.is_empty() {
157 self.focused_entry = Some(0);
158 }
159 }
160 self
161 }
162
163 pub fn with_value_schema(
165 mut self,
166 schema: crate::view::settings::schema::SettingSchema,
167 ) -> Self {
168 self.value_schema = Some(Box::new(schema));
169 self
170 }
171
172 pub fn with_focus(mut self, focus: FocusState) -> Self {
174 self.focus = focus;
175 self
176 }
177
178 pub fn is_enabled(&self) -> bool {
180 self.focus != FocusState::Disabled
181 }
182
183 pub fn add_entry(&mut self, key: String, value: serde_json::Value) {
185 if self.focus == FocusState::Disabled || key.is_empty() {
186 return;
187 }
188 if self.entries.iter().any(|(k, _)| k == &key) {
190 return;
191 }
192 self.entries.push((key, value));
193 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
194 }
195
196 pub fn add_entry_from_input(&mut self) {
198 if self.new_key_text.is_empty() {
199 return;
200 }
201 let key = std::mem::take(&mut self.new_key_text);
202 self.cursor = 0;
203 self.add_entry(key, serde_json::json!({}));
205 }
206
207 pub fn remove_entry(&mut self, index: usize) {
209 if self.focus == FocusState::Disabled || index >= self.entries.len() {
210 return;
211 }
212 self.entries.remove(index);
213 if let Some(focused) = self.focused_entry {
215 if focused >= self.entries.len() {
216 self.focused_entry = if self.entries.is_empty() {
217 None
218 } else {
219 Some(self.entries.len() - 1)
220 };
221 }
222 }
223 self.expanded.retain(|&idx| idx != index);
225 self.expanded = self
227 .expanded
228 .iter()
229 .map(|&idx| if idx > index { idx - 1 } else { idx })
230 .collect();
231 }
232
233 pub fn focus_entry(&mut self, index: usize) {
235 if index < self.entries.len() {
236 self.focused_entry = Some(index);
237 }
238 }
239
240 pub fn focus_new_entry(&mut self) {
242 self.focused_entry = None;
243 self.cursor = self.new_key_text.len();
244 }
245
246 pub fn toggle_expand(&mut self, index: usize) {
248 if index >= self.entries.len() {
249 return;
250 }
251 if let Some(pos) = self.expanded.iter().position(|&i| i == index) {
252 self.expanded.remove(pos);
253 } else {
254 self.expanded.push(index);
255 }
256 }
257
258 pub fn is_expanded(&self, index: usize) -> bool {
260 self.expanded.contains(&index)
261 }
262
263 pub fn insert(&mut self, c: char) {
265 if self.focus == FocusState::Disabled || self.focused_entry.is_some() {
266 return;
267 }
268 self.new_key_text.insert(self.cursor, c);
269 self.cursor += 1;
270 }
271
272 pub fn backspace(&mut self) {
274 if self.focus == FocusState::Disabled || self.focused_entry.is_some() || self.cursor == 0 {
275 return;
276 }
277 self.cursor -= 1;
278 self.new_key_text.remove(self.cursor);
279 }
280
281 pub fn move_left(&mut self) {
283 if self.cursor > 0 {
284 self.cursor -= 1;
285 }
286 }
287
288 pub fn move_right(&mut self) {
290 if self.cursor < self.new_key_text.len() {
291 self.cursor += 1;
292 }
293 }
294
295 pub fn focus_prev(&mut self) -> bool {
297 match self.focused_entry {
298 Some(0) => false, Some(idx) => {
300 self.focused_entry = Some(idx - 1);
301 true
302 }
303 None if !self.entries.is_empty() => {
304 self.focused_entry = Some(self.entries.len() - 1);
305 true
306 }
307 None => false, }
309 }
310
311 pub fn focus_next(&mut self) -> bool {
313 match self.focused_entry {
314 Some(idx) if idx + 1 < self.entries.len() => {
315 self.focused_entry = Some(idx + 1);
316 true
317 }
318 Some(_) if self.no_add => {
319 false
321 }
322 Some(_) => {
323 self.focused_entry = None;
325 self.cursor = self.new_key_text.len();
326 true
327 }
328 None => false, }
330 }
331
332 pub fn init_focus(&mut self, from_above: bool) {
335 if from_above && !self.entries.is_empty() {
336 self.focused_entry = Some(0);
337 } else if !from_above && !self.entries.is_empty() && self.no_add {
338 self.focused_entry = Some(self.entries.len() - 1);
340 } else if !self.no_add {
341 self.focused_entry = None;
343 self.cursor = self.new_key_text.len();
344 } else if !self.entries.is_empty() {
345 self.focused_entry = Some(0);
347 } else {
348 self.focused_entry = None;
350 }
351 }
352
353 pub fn len(&self) -> usize {
355 self.entries.len()
356 }
357
358 pub fn is_empty(&self) -> bool {
360 self.entries.is_empty()
361 }
362
363 pub fn to_value(&self) -> serde_json::Value {
365 let map: serde_json::Map<String, serde_json::Value> = self
366 .entries
367 .iter()
368 .map(|(k, v)| (k.clone(), v.clone()))
369 .collect();
370 serde_json::Value::Object(map)
371 }
372}
373
374#[derive(Debug, Clone, Copy)]
376pub struct MapColors {
377 pub label: Color,
378 pub key: Color,
379 pub value_preview: Color,
380 pub border: Color,
381 pub remove_button: Color,
382 pub add_button: Color,
383 pub focused: Color,
385 pub focused_fg: Color,
387 pub cursor: Color,
388 pub disabled: Color,
389 pub expand_arrow: Color,
390}
391
392impl Default for MapColors {
393 fn default() -> Self {
394 Self {
395 label: Color::White,
396 key: Color::Cyan,
397 value_preview: Color::Gray,
398 border: Color::Gray,
399 remove_button: Color::Red,
400 add_button: Color::Green,
401 focused: Color::Yellow,
402 focused_fg: Color::Black,
403 cursor: Color::Yellow,
404 disabled: Color::DarkGray,
405 expand_arrow: Color::White,
406 }
407 }
408}
409
410impl MapColors {
411 pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
412 Self {
413 label: theme.editor_fg,
414 key: theme.help_key_fg,
416 value_preview: theme.line_number_fg,
417 border: theme.line_number_fg,
418 remove_button: theme.diagnostic_error_fg,
419 add_button: theme.diagnostic_info_fg,
420 focused: theme.settings_selected_bg,
422 focused_fg: theme.settings_selected_fg,
423 cursor: theme.cursor,
424 disabled: theme.line_number_fg,
425 expand_arrow: theme.editor_fg,
426 }
427 }
428}
429
430#[derive(Debug, Clone, Default)]
432pub struct MapLayout {
433 pub full_area: Rect,
434 pub entry_areas: Vec<MapEntryLayout>,
435 pub add_row_area: Option<Rect>,
436}
437
438#[derive(Debug, Clone)]
440pub struct MapEntryLayout {
441 pub index: usize,
442 pub row_area: Rect,
443 pub expand_area: Rect,
444 pub key_area: Rect,
445 pub remove_area: Rect,
446}
447
448impl MapLayout {
449 pub fn hit_test(&self, x: u16, y: u16) -> Option<MapHit> {
451 for entry in &self.entry_areas {
453 if y == entry.row_area.y {
454 if x >= entry.remove_area.x && x < entry.remove_area.x + entry.remove_area.width {
455 return Some(MapHit::RemoveButton(entry.index));
456 }
457 if x >= entry.expand_area.x && x < entry.expand_area.x + entry.expand_area.width {
458 return Some(MapHit::ExpandArrow(entry.index));
459 }
460 if x >= entry.key_area.x && x < entry.key_area.x + entry.key_area.width {
461 return Some(MapHit::EntryKey(entry.index));
462 }
463 }
464 }
465
466 if let Some(ref add_row) = self.add_row_area {
468 if y == add_row.y && x >= add_row.x && x < add_row.x + add_row.width {
469 return Some(MapHit::AddRow);
470 }
471 }
472
473 None
474 }
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub enum MapHit {
480 ExpandArrow(usize),
482 EntryKey(usize),
484 RemoveButton(usize),
486 AddRow,
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_map_state_new() {
496 let state = MapState::new("Test");
497 assert_eq!(state.label, "Test");
498 assert!(state.entries.is_empty());
499 assert!(state.focused_entry.is_none());
500 }
501
502 #[test]
503 fn test_get_display_value_single_object() {
504 let state = MapState::new("Test").with_display_field("/command".to_string());
505 let value = serde_json::json!({"command": "pylsp"});
506 assert_eq!(state.get_display_value(&value), "pylsp");
507 }
508
509 #[test]
510 fn test_get_display_value_array_single_element() {
511 let state = MapState::new("Test").with_display_field("/command".to_string());
512 let value = serde_json::json!([{"command": "pylsp"}]);
513 assert_eq!(state.get_display_value(&value), "pylsp");
514 }
515
516 #[test]
517 fn test_get_display_value_array_two_short_elements_joined() {
518 let state = MapState::new("Test").with_display_field("/command".to_string());
519 let value = serde_json::json!([{"command": "a"}, {"command": "b"}]);
520 assert_eq!(state.get_display_value(&value), "a, b");
522 }
523
524 #[test]
525 fn test_get_display_value_array_overflow_uses_plus_n_more() {
526 let state = MapState::new("Test").with_display_field("/command".to_string());
527 let value = serde_json::json!([
528 {"command": "pylsp"},
529 {"command": "pyright-langserver"},
530 ]);
531 assert_eq!(state.get_display_value(&value), "pylsp, +1 more");
533 }
534
535 #[test]
536 fn test_get_display_value_array_three_elements_uses_plus_n_more() {
537 let state = MapState::new("Test").with_display_field("/command".to_string());
538 let value = serde_json::json!([
539 {"command": "alpha-server"},
540 {"command": "beta-server"},
541 {"command": "gamma-server"},
542 ]);
543 assert_eq!(state.get_display_value(&value), "alpha-server, +2 more");
544 }
545
546 #[test]
547 fn test_map_state_add_entry() {
548 let mut state = MapState::new("Test");
549 state.add_entry("key1".to_string(), serde_json::json!({"foo": "bar"}));
550 assert_eq!(state.entries.len(), 1);
551 assert_eq!(state.entries[0].0, "key1");
552 }
553
554 #[test]
555 fn test_map_state_remove_entry() {
556 let mut state = MapState::new("Test");
557 state.add_entry("a".to_string(), serde_json::json!({}));
558 state.add_entry("b".to_string(), serde_json::json!({}));
559 state.remove_entry(0);
560 assert_eq!(state.entries.len(), 1);
561 assert_eq!(state.entries[0].0, "b");
562 }
563
564 #[test]
565 fn test_map_state_navigation() {
566 let mut state = MapState::new("Test").with_focus(FocusState::Focused);
567 state.add_entry("a".to_string(), serde_json::json!({}));
568 state.add_entry("b".to_string(), serde_json::json!({}));
569
570 assert!(state.focused_entry.is_none());
572
573 state.focus_prev();
575 assert_eq!(state.focused_entry, Some(1));
576
577 state.focus_prev();
579 assert_eq!(state.focused_entry, Some(0));
580
581 state.focus_next();
583 assert_eq!(state.focused_entry, Some(1));
584
585 state.focus_next();
587 assert!(state.focused_entry.is_none());
588 }
589
590 #[test]
591 fn test_map_state_expand() {
592 let mut state = MapState::new("Test");
593 state.add_entry("key1".to_string(), serde_json::json!({}));
594
595 assert!(!state.is_expanded(0));
596 state.toggle_expand(0);
597 assert!(state.is_expanded(0));
598 state.toggle_expand(0);
599 assert!(!state.is_expanded(0));
600 }
601
602 #[test]
603 fn test_map_hit_test() {
604 let layout = MapLayout {
605 full_area: Rect::new(0, 0, 50, 5),
606 entry_areas: vec![MapEntryLayout {
607 index: 0,
608 row_area: Rect::new(0, 1, 50, 1),
609 expand_area: Rect::new(2, 1, 1, 1),
610 key_area: Rect::new(4, 1, 10, 1),
611 remove_area: Rect::new(40, 1, 3, 1),
612 }],
613 add_row_area: Some(Rect::new(0, 2, 50, 1)),
614 };
615
616 assert_eq!(layout.hit_test(2, 1), Some(MapHit::ExpandArrow(0)));
617 assert_eq!(layout.hit_test(5, 1), Some(MapHit::EntryKey(0)));
618 assert_eq!(layout.hit_test(40, 1), Some(MapHit::RemoveButton(0)));
619 assert_eq!(layout.hit_test(5, 2), Some(MapHit::AddRow));
620 assert_eq!(layout.hit_test(13, 2), Some(MapHit::AddRow));
621 assert_eq!(layout.hit_test(0, 0), None);
622 }
623}