1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//! State for the in-editor WordNet panel (1.8.5).
//!
//! When the cursor is on a word and the WordNet chord fires, the app builds a
//! [`WordnetPanel`] from the word's [`Suggestion`](crate::wordnet::Suggestion)
//! list and shows it as an overlay: an arrow-navigable list of candidate
//! replacements (synonyms, then hypernyms/hyponyms/antonyms). Selecting one
//! replaces the word in the editor. This module is the pure state + navigation;
//! rendering and key wiring live in the app.
use crate::wordnet::Suggestion;
/// The overlay's live state.
#[derive(Debug, Clone, PartialEq)]
pub struct WordnetPanel {
/// The word being looked up (for the title and to avoid self-replacement).
pub word: String,
/// The language code searched (`en`, `ru`, …).
pub lang: String,
/// Candidate replacements, already ordered and de-duplicated.
pub suggestions: Vec<Suggestion>,
/// The highlighted row.
pub selected: usize,
}
impl WordnetPanel {
pub fn new(word: impl Into<String>, lang: impl Into<String>, suggestions: Vec<Suggestion>) -> Self {
Self { word: word.into(), lang: lang.into(), suggestions, selected: 0 }
}
/// Move the highlight down, saturating at the last row.
pub fn down(&mut self) {
if self.selected + 1 < self.suggestions.len() {
self.selected += 1;
}
}
/// Move the highlight up, saturating at the first row.
pub fn up(&mut self) {
self.selected = self.selected.saturating_sub(1);
}
/// The currently highlighted replacement word, if any.
pub fn selected_word(&self) -> Option<&str> {
self.suggestions.get(self.selected).map(|s| s.word.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sug(kind: &'static str, word: &str) -> Suggestion {
Suggestion { kind, word: word.into() }
}
#[test]
fn navigation_saturates_at_both_ends() {
let mut p = WordnetPanel::new(
"big",
"en",
vec![sug("synonym", "large"), sug("antonym", "small")],
);
assert_eq!(p.selected, 0);
p.up(); // already at top
assert_eq!(p.selected, 0);
p.down();
assert_eq!(p.selected_word(), Some("small"));
p.down(); // already at bottom
assert_eq!(p.selected, 1);
p.up();
assert_eq!(p.selected_word(), Some("large"));
}
#[test]
fn empty_panel_has_no_selection() {
let p = WordnetPanel::new("xyzzy", "en", vec![]);
assert_eq!(p.selected_word(), None);
}
}