1use bynk_check::hints::{Hint, HintKind};
15use bynk_check::requirements::Requirement;
16use bynk_syntax::span::Span;
17use std::collections::HashSet;
18use tower_lsp::lsp_types::*;
19
20use crate::position::{offset_to_position, span_to_range};
21
22pub fn inlay_hints(text: &str, hints: &[Hint], requested: Span) -> Vec<InlayHint> {
26 hints
27 .iter()
28 .filter_map(|h| {
29 let anchor = match h.kind {
30 HintKind::Type => h.span.end,
31 HintKind::Parameter => h.span.start,
32 };
33 (requested.start <= anchor && anchor <= requested.end).then(|| {
34 let (kind, padding_right) = match h.kind {
35 HintKind::Type => (InlayHintKind::TYPE, None),
36 HintKind::Parameter => (InlayHintKind::PARAMETER, Some(true)),
37 };
38 InlayHint {
39 position: offset_to_position(text, anchor),
40 label: InlayHintLabel::String(h.label.clone()),
41 kind: Some(kind),
42 text_edits: None,
43 tooltip: None,
44 padding_left: None,
45 padding_right,
46 data: None,
47 }
48 })
49 })
50 .collect()
51}
52
53pub fn given_hints(text: &str, requirements: &[Requirement], requested: Span) -> Vec<InlayHint> {
64 let mut seen: HashSet<(usize, String)> = HashSet::new();
65 let mut out = Vec::new();
66 for req in requirements {
67 let Some(m) = &req.materialize else {
70 continue;
71 };
72 let anchor = m.edit_span.start;
73 if anchor < requested.start || anchor > requested.end {
74 continue;
75 }
76 if !seen.insert((anchor, req.capability.clone())) {
77 continue;
78 }
79 let label = m.edit_text.trim_start().to_string();
82 out.push(InlayHint {
83 position: offset_to_position(text, anchor),
84 label: InlayHintLabel::String(label),
85 kind: Some(InlayHintKind::TYPE),
86 text_edits: Some(vec![TextEdit {
87 range: span_to_range(text, m.edit_span),
88 new_text: m.edit_text.clone(),
89 }]),
90 tooltip: Some(InlayHintTooltip::String(format!(
91 "{} — {}",
92 req.capability,
93 req.source.reason(&req.capability)
94 ))),
95 padding_left: Some(true),
96 padding_right: None,
97 data: None,
98 });
99 }
100 out
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use bynk_check::requirements::{Materialize, RequirementSource, StoreKind};
107
108 fn label_of(h: &InlayHint) -> &str {
109 match &h.label {
110 InlayHintLabel::String(s) => s,
111 other => panic!("expected a plain string label, got {other:?}"),
112 }
113 }
114
115 fn type_hint(start: usize, end: usize, label: &str) -> Hint {
116 Hint {
117 span: Span::new(start, end),
118 label: label.to_string(),
119 kind: HintKind::Type,
120 }
121 }
122
123 #[test]
124 fn type_hints_anchor_after_the_name_with_no_padding() {
125 let text = "let x = 1\nlet y = 2\n";
127 let hints = vec![type_hint(4, 5, ": Int"), type_hint(14, 15, ": Int")];
128 let got = inlay_hints(text, &hints, Span::new(0, text.len()));
129 assert_eq!(got.len(), 2);
130 assert_eq!(got[0].position, Position::new(0, 5));
132 assert_eq!(label_of(&got[0]), ": Int");
133 assert_eq!(got[0].kind, Some(InlayHintKind::TYPE));
134 assert_eq!(got[0].padding_right, None);
135 assert_eq!(got[1].position, Position::new(1, 5));
136 }
137
138 #[test]
139 fn parameter_hints_anchor_before_the_argument_with_trailing_padding() {
140 let text = "f(5)\n";
142 let hints = vec![Hint {
143 span: Span::new(2, 3),
144 label: "count:".to_string(),
145 kind: HintKind::Parameter,
146 }];
147 let got = inlay_hints(text, &hints, Span::new(0, text.len()));
148 assert_eq!(got.len(), 1);
149 assert_eq!(got[0].position, Position::new(0, 2));
151 assert_eq!(label_of(&got[0]), "count:");
152 assert_eq!(got[0].kind, Some(InlayHintKind::PARAMETER));
153 assert_eq!(got[0].padding_right, Some(true));
154 }
155
156 #[test]
157 fn out_of_range_hints_are_filtered() {
158 let text = "let x = 1\nlet y = 2\n";
159 let hints = vec![type_hint(4, 5, ": Int"), type_hint(14, 15, ": Int")];
160 let got = inlay_hints(text, &hints, Span::new(0, 9));
162 assert_eq!(got.len(), 1);
163 assert_eq!(got[0].position, Position::new(0, 5));
164 }
165
166 #[test]
167 fn empty_hint_set_returns_empty() {
168 assert!(inlay_hints("let x = 1\n", &[], Span::new(0, 9)).is_empty());
169 }
170
171 fn uncovered(cap: &str, site: usize, anchor: usize, edit_text: &str) -> Requirement {
172 Requirement {
173 capability: cap.to_string(),
174 site: Span::new(site, site + 1),
175 source: RequirementSource::StoreOp {
176 kind: StoreKind::Cache,
177 op: "put".to_string(),
178 },
179 covered: false,
180 materialize: Some(Materialize {
181 anchor: Span::new(anchor, anchor),
182 edit_span: Span::new(anchor, anchor),
183 edit_text: edit_text.to_string(),
184 }),
185 }
186 }
187
188 #[test]
189 fn ghost_given_renders_at_the_insertion_point_with_a_materialization_edit() {
190 let text = "on call f() -> Effect[()] {\n}\n";
192 let reqs = vec![uncovered("Clock", 20, 25, " given Clock")];
193 let got = given_hints(text, &reqs, Span::new(0, text.len()));
194 assert_eq!(got.len(), 1);
195 assert_eq!(label_of(&got[0]), "given Clock");
196 assert_eq!(got[0].padding_left, Some(true));
197 let edits = got[0].text_edits.as_ref().expect("materialization edit");
198 assert_eq!(edits.len(), 1);
199 assert_eq!(edits[0].new_text, " given Clock");
200 }
201
202 #[test]
203 fn ghost_given_dedups_per_handler_and_capability() {
204 let text = "on call f() -> Effect[()] {\n}\n";
206 let reqs = vec![
207 uncovered("Clock", 20, 25, " given Clock"),
208 uncovered("Clock", 30, 25, " given Clock"),
209 ];
210 let got = given_hints(text, &reqs, Span::new(0, text.len()));
211 assert_eq!(got.len(), 1, "deduped to a single ghost");
212 }
213
214 #[test]
215 fn covered_requirements_render_no_ghost() {
216 let text = "on call f() -> Effect[()] given Clock {\n}\n";
217 let covered = Requirement {
218 capability: "Clock".to_string(),
219 site: Span::new(20, 21),
220 source: RequirementSource::StoreOp {
221 kind: StoreKind::Cache,
222 op: "put".to_string(),
223 },
224 covered: true,
225 materialize: None,
226 };
227 assert!(given_hints(text, &[covered], Span::new(0, text.len())).is_empty());
228 }
229}