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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! Self-contained hover state.
//!
//! Owns five fields previously scattered on `Editor`:
//!
//! - In-flight LSP hover request: `(request_id, lsp_position)`. Used to
//! ignore stale responses from earlier mouse moves.
//! - Highlighted-symbol range and its overlay handle. The handle is kept
//! so we can remove the old overlay before drawing a new one, or when
//! focus is lost.
//! - Cached mouse screen position. Set at request time so the popup can
//! be placed under the same cell the user last hovered, even if they
//! have since moved the mouse elsewhere.
//!
//! All cross-cutting effects — sending the LSP request, applying the
//! `RemoveOverlay` event to the buffer after `take_symbol_overlay`,
//! positioning the popup — stay on `Editor` as orchestrators. This module
//! is pure data with no `Editor` or I/O access.
use crate::view::overlay::OverlayHandle;
/// Owner of in-flight hover request and highlighted-symbol tracking.
#[derive(Debug, Default)]
pub(crate) struct HoverState {
/// LSP request id of the in-flight hover request, if any.
pending_request: Option<u64>,
/// LSP position `(line, character)` of the in-flight request. Retained
/// so the response handler can correlate diagnostics with the hover
/// point and fuse them into the hover card.
pending_position: Option<(u32, u32)>,
/// Byte range `(start, end)` of the currently-highlighted symbol.
/// Used by mouse-move handlers to detect "still on same symbol" and
/// skip re-querying.
symbol_range: Option<(usize, usize)>,
/// Overlay handle for the symbol highlight, so the caller can remove
/// the previous highlight via `RemoveOverlay` before adding a new one.
symbol_overlay: Option<OverlayHandle>,
/// Screen cell `(col, row)` where the popup should be placed. Set
/// when a mouse-triggered hover request is fired; consumed when the
/// popup is rendered.
screen_position: Option<(u16, u16)>,
}
impl HoverState {
// ---- Pending-request correlation --------------------------------------
/// Record that a hover request with `request_id` was sent at LSP
/// position `(line, character)`.
pub(crate) fn record_request(&mut self, request_id: u64, line: u32, character: u32) {
self.pending_request = Some(request_id);
self.pending_position = Some((line, character));
}
/// Claim a response as matching the in-flight request. If it matches,
/// both `pending_request` and `pending_position` are cleared, and the
/// position is returned for the caller's use (diagnostic correlation).
///
/// Returns `None` if the response is stale — the caller should drop it.
pub(crate) fn claim_pending(&mut self, request_id: u64) -> Option<(u32, u32)> {
if self.pending_request != Some(request_id) {
return None;
}
self.pending_request = None;
self.pending_position.take()
}
/// Clear any in-flight request without consuming a position — used
/// when focus is lost or the user cancels hover.
pub(crate) fn clear_pending(&mut self) {
self.pending_request = None;
self.pending_position = None;
}
// ---- Symbol range -----------------------------------------------------
pub(crate) fn symbol_range(&self) -> Option<(usize, usize)> {
self.symbol_range
}
pub(crate) fn set_symbol_range(&mut self, range: Option<(usize, usize)>) {
self.symbol_range = range;
}
// ---- Symbol overlay handle --------------------------------------------
/// Take the current overlay handle (if any) so the caller can apply a
/// `RemoveOverlay` event to the buffer before adding a new overlay.
pub(crate) fn take_symbol_overlay(&mut self) -> Option<OverlayHandle> {
self.symbol_overlay.take()
}
pub(crate) fn set_symbol_overlay(&mut self, handle: OverlayHandle) {
self.symbol_overlay = Some(handle);
}
// ---- Screen position --------------------------------------------------
pub(crate) fn set_screen_position(&mut self, pos: (u16, u16)) {
self.screen_position = Some(pos);
}
pub(crate) fn take_screen_position(&mut self) -> Option<(u16, u16)> {
self.screen_position.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_empty() {
let mut h = HoverState::default();
assert_eq!(h.symbol_range(), None);
assert_eq!(h.claim_pending(42), None);
}
#[test]
fn claim_pending_returns_position_and_clears_state() {
let mut h = HoverState::default();
h.record_request(7, 10, 20);
assert_eq!(h.claim_pending(7), Some((10, 20)));
// Subsequent claim for the same id returns None — position drained.
assert_eq!(h.claim_pending(7), None);
}
#[test]
fn claim_pending_rejects_stale_response() {
let mut h = HoverState::default();
h.record_request(7, 10, 20);
// Older response arrives after a newer request went out.
assert_eq!(h.claim_pending(3), None);
// Correct one still works.
assert_eq!(h.claim_pending(7), Some((10, 20)));
}
#[test]
fn record_request_overwrites_previous_pending() {
let mut h = HoverState::default();
h.record_request(1, 0, 0);
h.record_request(2, 5, 5);
assert_eq!(h.claim_pending(1), None);
assert_eq!(h.claim_pending(2), Some((5, 5)));
}
#[test]
fn clear_pending_drops_without_returning_position() {
let mut h = HoverState::default();
h.record_request(7, 10, 20);
h.clear_pending();
assert_eq!(h.claim_pending(7), None);
}
#[test]
fn symbol_range_roundtrips() {
let mut h = HoverState::default();
assert_eq!(h.symbol_range(), None);
h.set_symbol_range(Some((10, 20)));
assert_eq!(h.symbol_range(), Some((10, 20)));
h.set_symbol_range(None);
assert_eq!(h.symbol_range(), None);
}
#[test]
fn take_screen_position_drains_on_first_call() {
let mut h = HoverState::default();
h.set_screen_position((15, 8));
assert_eq!(h.take_screen_position(), Some((15, 8)));
assert_eq!(h.take_screen_position(), None);
}
}