1use lattice_protocol::position::Position;
43
44#[allow(unused_imports)]
49use super::{
50 App, AsyncCompletionOutcome, CodeActionOutcome, CodeActionRow, CompletionItemRow,
51 CompletionOutcome, CompletionResolveOutcome, EchoLevel, FormatOutcome, LSP_COMPLETION_KIND_ID,
52 LspCompletionMeta, LspNavKind, ReferencesOutcome, RenameOutcome, SignatureHelpOutcome,
53 SymbolRow, SymbolsOutcome, TagStackEntry, app_to_lsp_position, call_hierarchy_to_row,
54 code_action_kind_glyph, dedup_rendered_by_text, flatten_workspace_edit, last_addressable_line,
55 line_byte_len, lsp_position_to_app_byte, prepare_rename_placeholder, range_covers,
56 type_hierarchy_to_row, word_under_cursor,
57};
58#[cfg(test)]
64use super::{
65 definition_response_to_locations, flatten_document_symbol_response, hover_contents_to_markdown,
66 signature_help_to_markdown,
67};
68use crate::buffers::BufferId;
69use lattice_protocol::edit::Edit;
70
71impl App {
83 pub fn lsp_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
103 #[cfg(test)]
105 {
106 self.editor.lsp_mode_enabled_for(buffer_id)
107 }
108 #[cfg(not(test))]
109 {
110 self.modes()
111 .map
112 .get(&buffer_id)
113 .map(|m| m.has_minor(lattice_lsp::modes::LspMode::mode_id()))
114 .unwrap_or(false)
115 }
116 }
117
118 fn minor_mode_enabled_for(&self, buffer_id: BufferId, mode_id: lattice_mode::ModeId) -> bool {
123 self.read_editor(move |e| e.minor_mode_enabled_for(buffer_id, mode_id))
124 }
125
126 pub fn completion_mode_active_for(&self, buffer_id: BufferId) -> bool {
132 self.read_editor(move |e| e.completion_mode_active_for(buffer_id))
134 }
135
136 pub fn completion_popup_mode_active_for(&self, buffer_id: BufferId) -> bool {
144 self.minor_mode_enabled_for(buffer_id, lattice_mode::CompletionPopupMode::mode_id())
145 }
146
147 pub fn completion_popup_active(&self) -> bool {
152 #[cfg(test)]
165 {
166 self.completion_popup_mode_active_for(self.document_buffer_id())
167 }
168 #[cfg(not(test))]
169 {
170 let buffer_id = self.document_buffer_id();
171 self.modes()
172 .map
173 .get(&buffer_id)
174 .map(|m| m.has_minor(lattice_mode::CompletionPopupMode::mode_id()))
175 .unwrap_or(false)
176 }
177 }
178
179 pub fn lsp_completion_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
184 self.minor_mode_enabled_for(buffer_id, lattice_lsp::modes::LspCompletionMode::mode_id())
185 }
186
187 pub fn lsp_diagnostics_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
191 #[cfg(test)]
192 {
193 self.editor.lsp_diagnostics_mode_enabled_for(buffer_id)
194 }
195 #[cfg(not(test))]
196 {
197 self.modes()
198 .map
199 .get(&buffer_id)
200 .map(|m| m.has_minor(lattice_lsp::modes::LspDiagnosticsMode::mode_id()))
201 .unwrap_or(false)
202 }
203 }
204
205 pub fn lsp_hover_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
208 self.minor_mode_enabled_for(buffer_id, lattice_lsp::modes::LspHoverMode::mode_id())
209 }
210
211 pub fn lsp_signature_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
213 self.minor_mode_enabled_for(buffer_id, lattice_lsp::modes::LspSignatureMode::mode_id())
214 }
215
216 pub fn lsp_format_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
219 self.read_editor(move |e| e.lsp_format_mode_enabled_for(buffer_id))
220 }
221
222 pub fn lsp_rename_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
223 self.read_editor(move |e| e.lsp_rename_mode_enabled_for(buffer_id))
224 }
225
226 pub fn lsp_symbols_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
227 self.read_editor(move |e| e.lsp_symbols_mode_enabled_for(buffer_id))
228 }
229
230 pub fn lsp_code_action_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
231 self.read_editor(move |e| e.lsp_code_action_mode_enabled_for(buffer_id))
232 }
233
234 pub fn lsp_nav_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
235 self.read_editor(move |e| e.lsp_nav_mode_enabled_for(buffer_id))
236 }
237
238 pub fn lsp_progress_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
245 #[cfg(test)]
246 {
247 self.editor.lsp_progress_mode_enabled_for(buffer_id)
248 }
249 #[cfg(not(test))]
250 {
251 self.modes()
252 .map
253 .get(&buffer_id)
254 .map(|m| m.has_minor(lattice_lsp::modes::LspProgressMode::mode_id()))
255 .unwrap_or(false)
256 }
257 }
258
259 pub fn lsp_document_highlight_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
264 #[cfg(test)]
265 {
266 self.editor
267 .lsp_document_highlight_mode_enabled_for(buffer_id)
268 }
269 #[cfg(not(test))]
270 {
271 self.modes()
272 .map
273 .get(&buffer_id)
274 .map(|m| m.has_minor(lattice_lsp::modes::LspDocumentHighlightMode::mode_id()))
275 .unwrap_or(false)
276 }
277 }
278
279 pub fn lsp_selection_range_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
283 self.read_editor(move |e| e.lsp_selection_range_mode_enabled_for(buffer_id))
284 }
285
286 pub fn lsp_folding_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
292 self.read_editor(move |e| e.lsp_folding_mode_enabled_for(buffer_id))
293 }
294
295 pub fn lsp_inlay_hint_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
296 #[cfg(test)]
297 {
298 self.editor.lsp_inlay_hint_mode_enabled_for(buffer_id)
299 }
300 #[cfg(not(test))]
301 {
302 self.modes()
303 .map
304 .get(&buffer_id)
305 .map(|m| m.has_minor(lattice_lsp::modes::LspInlayHintMode::mode_id()))
306 .unwrap_or(false)
307 }
308 }
309
310 pub fn lsp_semantic_tokens_mode_enabled_for(&self, buffer_id: BufferId) -> bool {
311 #[cfg(test)]
312 {
313 self.editor.lsp_semantic_tokens_mode_enabled_for(buffer_id)
314 }
315 #[cfg(not(test))]
316 {
317 self.modes()
318 .map
319 .get(&buffer_id)
320 .map(|m| m.has_minor(lattice_lsp::modes::LspSemanticTokensMode::mode_id()))
321 .unwrap_or(false)
322 }
323 }
324
325 pub(super) fn check_lsp_mode_gate(&mut self) -> bool {
337 if self.lsp_mode_enabled_for(self.document_buffer_id()) {
338 return true;
339 }
340 self.set_message(
341 EchoLevel::Info,
342 "lsp-mode disabled for this buffer (`:lsp-mode` to enable)".to_string(),
343 );
344 false
345 }
346
347 fn check_lsp_sub_mode_gate(
360 &mut self,
361 sub_mode_id: lattice_mode::ModeId,
362 sub_mode_name: &str,
363 ) -> bool {
364 if !self.check_lsp_mode_gate() {
365 return false;
366 }
367 if self.minor_mode_enabled_for(self.document_buffer_id(), sub_mode_id) {
368 return true;
369 }
370 self.set_message(
371 EchoLevel::Info,
372 format!("{sub_mode_name} disabled for this buffer (`:{sub_mode_name}` to enable)"),
373 );
374 false
375 }
376
377 pub fn drain_pending_hover(&mut self) {
393 let signals = self.mutate_editor_with(|e| e.drain_pending_hover());
401 for signal in signals {
402 self.handle_renderer_signal(signal);
403 }
404 }
405
406 pub(super) fn apply_lsp_completion_accept(
413 &mut self,
414 meta: LspCompletionMeta,
415 anchor: lattice_protocol::position::Position,
416 ) {
417 self.mutate_editor(move |e| e.apply_lsp_completion_accept(meta, anchor));
419 }
420
421 pub(super) fn do_completion_resolve_focused(&mut self) {
428 self.mutate_editor(|e| e.do_completion_resolve_focused());
431 }
432
433 pub(super) fn do_async_insert_completion_requests(&mut self) {
444 self.mutate_editor(|e| e.do_async_insert_completion_requests());
446 }
447
448 pub fn drain_pending_completion_resolve(&mut self) {
455 self.mutate_editor(|e| e.drain_pending_completion_resolve());
457 }
458
459 pub fn drain_pending_insert_completion_lsp(&mut self) {
463 self.mutate_editor(|e| e.drain_pending_insert_completion_lsp());
465 }
466
467 pub fn drain_lsp_log_events(&mut self) {
479 self.mutate_editor(|e| e.drain_lsp_log_events());
481 }
482
483 pub fn drain_lsp_detach_events(&mut self) {
502 self.mutate_editor(|e| e.drain_lsp_detach_events());
504 }
505
506 pub fn fan_out_did_change_configuration(&mut self, server_id: &str) {
547 let server_id = server_id.to_string();
549 self.mutate_editor(move |e| e.fan_out_did_change_configuration(&server_id));
550 }
551
552 pub fn drain_inbound_show_message_requests(&mut self) {
575 self.mutate_editor(|e| e.drain_inbound_show_message_requests());
577 }
578
579 pub(super) fn open_show_message_request_picker(&mut self, request_id: u32) {
589 self.mutate_editor(move |e| e.open_show_message_request_picker(request_id));
590 }
591
592 pub(crate) fn finalize_show_message_request(
599 &mut self,
600 request_id: u32,
601 selected_index: Option<u32>,
602 ) {
603 let Some(req) = self
606 .mutate_editor_with(move |e| e.lsp_pending_show_message_requests.remove(&request_id))
607 else {
608 return;
609 };
610 let selected = selected_index.and_then(|i| req.actions.get(i as usize).cloned());
611 let _ = req
612 .response
613 .send(lattice_lsp::ShowMessageRequestOutcome { selected });
614 }
615
616 pub(crate) fn open_next_queued_show_message_request(&mut self) {
621 let next = self.mutate_editor_with(|e| {
627 while let Some(id) = e.lsp_show_message_request_queue.pop_front() {
628 if e.lsp_pending_show_message_requests.contains_key(&id) {
629 return Some(id);
630 }
631 }
632 None
633 });
634 if let Some(id) = next {
635 self.open_show_message_request_picker(id);
636 }
637 }
638
639 pub fn drain_inbound_apply_edits(&mut self) {
654 let signals = self.mutate_editor_with(|e| e.drain_inbound_apply_edits());
656 for s in signals {
657 self.handle_renderer_signal(s);
658 }
659 }
660
661 pub(super) fn publish_document_opened_for_active(&mut self) {
680 self.mutate_editor(|e| e.publish_document_opened_for_active());
681 }
682
683 pub(crate) fn lsp_completion_meta_for(
698 &self,
699 candidate: &lattice_completion::RenderedCandidate,
700 ) -> Option<LspCompletionMeta> {
701 let candidate = candidate.clone();
704 self.read_editor(move |e| e.lsp_completion_meta_for(&candidate))
705 }
706
707 pub fn buffer_uri(&self, id: BufferId) -> Option<lattice_lsp::Uri> {
711 self.buffers().uris.get(&id).cloned()
717 }
718
719 pub fn lsp_flush(&self, buffer_id: BufferId) {
724 let Some(uri) = self.buffers().uris.get(&buffer_id).cloned() else {
725 return;
726 };
727 self.read_editor(move |e| e.lsp.flush(uri));
731 }
732
733 pub fn lsp_close_buffer(&mut self, buffer_id: BufferId) {
739 self.mutate_editor(move |e| e.lsp_close_buffer(buffer_id));
740 }
741
742 pub(super) fn apply_persistent_lsp_editor_options(&mut self) {
764 self.mutate_editor(|e| e.apply_persistent_lsp_editor_options());
767 }
768
769 pub(super) fn apply_lsp_text_edits(
775 &mut self,
776 edits: Vec<lattice_lsp::lsp_types::TextEdit>,
777 ) -> Result<(), String> {
778 self.mutate_editor_with(move |e| e.apply_lsp_text_edits(edits))
780 }
781
782 pub(super) fn on_type_formatting_trigger_chars(&self) -> Vec<char> {
787 self.read_editor(move |e| e.on_type_formatting_trigger_chars())
788 }
789
790 pub(super) fn signature_help_trigger_chars(&self) -> Vec<char> {
795 self.read_editor(move |e| e.signature_help_trigger_chars())
796 }
797
798 pub(super) fn do_lsp_on_type_formatting_request(&mut self, trigger: char) {
802 self.mutate_editor(move |e| e.do_lsp_on_type_formatting_request(trigger));
804 }
805
806 pub(super) fn do_lsp_rename_request(&mut self, new_name: &str) {
818 let new_name = new_name.to_string();
820 self.mutate_editor(move |e| e.do_lsp_rename_request(&new_name));
821 }
822
823 pub fn drain_pending_rename(&mut self) {
827 let signals = self.mutate_editor_with(|e| e.drain_pending_rename());
829 for s in signals {
830 self.handle_renderer_signal(s);
831 }
832 }
833
834 pub(super) fn apply_rename_workspace_edit(
838 &mut self,
839 per_file: Vec<(
840 lattice_lsp::lsp_types::Uri,
841 Vec<lattice_lsp::lsp_types::TextEdit>,
842 )>,
843 new_name: String,
844 ) {
845 let signals =
848 self.mutate_editor_with(move |e| e.apply_rename_workspace_edit(per_file, new_name));
849 for s in signals {
850 self.handle_renderer_signal(s);
851 }
852 }
853
854 pub(super) fn apply_lsp_code_action(
858 &mut self,
859 row: CodeActionRow,
860 handle: Option<lattice_lsp::ServerHandle>,
861 ) {
862 let signals = self.mutate_editor_with(move |e| e.apply_lsp_code_action(row, handle));
863 for s in signals {
864 self.handle_renderer_signal(s);
865 }
866 }
867
868 pub(super) fn execute_lsp_command(
869 &mut self,
870 handle: Option<lattice_lsp::ServerHandle>,
871 cmd: lattice_lsp::lsp_types::Command,
872 ) {
873 self.mutate_editor(move |e| e.execute_lsp_command(handle, cmd));
875 }
876
877 pub(super) fn apply_lsp_completion_item(&mut self, item: &CompletionItemRow) {
882 let (start_byte, end_byte) = item.replace_range;
883 let range = lattice_protocol::position::Range::new(
884 Position::new(item.line, start_byte),
885 Position::new(item.line, end_byte),
886 );
887 let edit = Edit::replace(range, item.insert_text.clone());
888 match self.apply_edit_blocking(edit) {
889 Ok(applied) => {
890 self.set_cursor(applied.inserted_range.end);
891 }
892 Err(e) => {
893 self.set_message(EchoLevel::Error, format!("complete: apply failed: {e:?}"));
894 }
895 }
896 }
897
898 pub(super) fn do_lsp_code_action_request(&mut self) {
903 self.mutate_editor(|e| e.do_lsp_code_action_request());
906 }
907
908 pub fn drain_pending_code_actions(&mut self) {
915 let signals = self.mutate_editor_with(|e| e.drain_pending_code_actions());
916 for s in signals {
917 self.handle_renderer_signal(s);
918 }
919 }
920
921 fn first_code_action_handle(&self) -> Option<lattice_lsp::ServerHandle> {
925 let uri = self.buffers().uris.get(&self.document_buffer_id())?.clone();
928 self.render_state
929 .load()
930 .lsp
931 .supervisor
932 .servers_for(&uri)
933 .into_iter()
934 .find(|h| h.capabilities().supports_code_action())
935 }
936
937 pub fn drain_pending_completion(&mut self) {
951 self.mutate_editor(|e| e.drain_pending_completion());
953 }
954
955 pub(super) fn do_format_request(&mut self) {
969 self.mutate_editor(|e| e.do_format_request());
970 }
971
972 pub(super) fn do_lsp_format_request(&mut self, is_range: bool) {
973 self.mutate_editor(move |e| e.do_lsp_format_request(is_range));
975 }
976
977 pub fn drain_pending_format(&mut self) {
981 self.mutate_editor(|e| e.drain_pending_format());
983 }
984
985 pub fn drain_pending_symbols(&mut self) {
1000 self.mutate_editor(|e| e.drain_pending_symbols());
1003 }
1004
1005 pub(super) fn do_lsp_call_hierarchy_request(&mut self, outgoing: bool) {
1021 self.mutate_editor(move |e| e.do_lsp_call_hierarchy_request(outgoing));
1023 }
1024
1025 pub(super) fn do_lsp_type_hierarchy_request(&mut self, subtypes: bool) {
1041 self.mutate_editor(move |e| e.do_lsp_type_hierarchy_request(subtypes));
1043 }
1044
1045 pub(super) fn do_lsp_moniker_request(&mut self) {
1053 self.mutate_editor(|e| e.do_lsp_moniker_request());
1055 }
1056
1057 pub fn drain_pending_moniker(&mut self) {
1061 self.mutate_editor(|e| e.drain_pending_moniker());
1063 }
1064
1065 pub fn drain_pending_signature_help(&mut self) {
1081 let signals = self.mutate_editor_with(|e| e.drain_pending_signature_help());
1087 for signal in signals {
1088 self.handle_renderer_signal(signal);
1089 }
1090 }
1091
1092 pub fn drain_pending_definitions(&mut self) {
1111 let signals = self.mutate_editor_with(|e| e.drain_pending_definitions());
1115 for s in signals {
1116 self.handle_renderer_signal(s);
1117 }
1118 }
1119
1120 pub fn drain_pending_references(&mut self) {
1140 self.mutate_editor(|e| e.drain_pending_references());
1145 }
1146
1147 pub(super) fn jump_to_lsp_location(&mut self, loc: &lattice_lsp::lsp_types::Location) {
1159 let loc = loc.clone();
1161 let signals = self.mutate_editor_with(move |e| e.jump_to_lsp_location(&loc));
1162 for s in signals {
1163 self.handle_renderer_signal(s);
1164 }
1165 }
1166
1167 #[allow(dead_code)]
1172 pub fn do_list_diagnostics(&mut self) {
1173 self.mutate_editor(|e| e.do_list_diagnostics());
1174 }
1175
1176 pub fn do_next_diagnostic(&mut self) {
1179 self.mutate_editor(|e| e.do_next_diagnostic());
1181 }
1182
1183 pub fn do_prev_diagnostic(&mut self) {
1186 self.mutate_editor(|e| e.do_prev_diagnostic());
1188 }
1189
1190 pub fn do_open_lsp_log(&mut self, server_id: Option<&str>) {
1207 let server_id = server_id.map(|s| s.to_string());
1209 self.mutate_editor(move |e| e.do_open_lsp_log(server_id.as_deref()));
1210 }
1211
1212 pub fn do_open_lsp_trace_log(&mut self, server_id: Option<&str>) {
1214 let server_id = server_id.map(|s| s.to_string());
1215 self.mutate_editor(move |e| e.do_open_lsp_trace_log(server_id.as_deref()));
1216 }
1217
1218 pub fn do_toggle_lsp_trace(&mut self, name: &str) {
1220 let name = name.to_string();
1221 self.mutate_editor(move |e| e.do_toggle_lsp_trace(&name));
1222 }
1223
1224 pub fn do_lsp_diagnostics_to_error_list(&mut self) {
1229 self.mutate_editor(|e| e.do_lsp_diagnostics_to_error_list());
1230 }
1231
1232 pub fn do_lsp_status(&mut self) {
1235 let signals = self.mutate_editor_with(|e| e.do_lsp_status());
1240 for s in signals {
1241 self.handle_renderer_signal(s);
1242 }
1243 }
1244
1245 pub fn do_lsp_server_log_listing(&mut self) {
1249 self.mutate_editor(|e| e.do_lsp_server_log_listing());
1251 }
1252
1253 pub fn do_lsp_restart(&mut self, server_id: &str) {
1257 let server_id = server_id.to_string();
1259 self.mutate_editor(move |e| e.do_lsp_restart(&server_id));
1260 }
1261
1262 pub fn maybe_request_folding_range(&mut self) {
1276 self.mutate_editor(|e| e.maybe_request_folding_range());
1278 }
1279
1280 pub fn maybe_request_semantic_tokens(&mut self) {
1294 self.mutate_editor(|e| e.maybe_request_semantic_tokens());
1296 }
1297
1298 pub fn maybe_request_pull_diagnostics(&mut self) {
1323 self.mutate_editor(|e| e.maybe_request_pull_diagnostics());
1325 }
1326
1327 pub fn drain_diagnostic_refresh(&mut self) {
1338 self.mutate_editor(|e| e.drain_diagnostic_refresh());
1340 }
1341
1342 pub fn maybe_request_inlay_hint(&mut self) {
1353 self.mutate_editor(|e| e.maybe_request_inlay_hint());
1355 }
1356
1357 pub fn drain_inlay_hint_refresh(&mut self) {
1362 self.mutate_editor(|e| e.drain_inlay_hint_refresh());
1364 }
1365
1366 pub fn drain_semantic_tokens_refresh(&mut self) {
1374 self.mutate_editor(|e| e.drain_semantic_tokens_refresh());
1376 }
1377
1378 pub fn maybe_request_document_link(&mut self) {
1390 self.mutate_editor(|e| e.maybe_request_document_link());
1392 }
1393
1394 pub fn do_lsp_follow_link_at_cursor(&mut self) {
1407 let signals = self.mutate_editor_with(|e| e.do_lsp_follow_link_at_cursor());
1409 for s in signals {
1410 self.handle_renderer_signal(s);
1411 }
1412 }
1413
1414 pub fn maybe_request_code_lens(&mut self) {
1418 self.mutate_editor(|e| e.maybe_request_code_lens());
1420 }
1421
1422 pub(super) fn do_lsp_code_lens_picker(&mut self) {
1432 self.mutate_editor(|e| e.do_lsp_code_lens_picker());
1434 }
1435
1436 pub(super) fn accept_lsp_code_lens(&mut self, index: u32) {
1444 self.mutate_editor(move |e| e.accept_lsp_code_lens(index));
1446 }
1447
1448 pub fn maybe_request_document_color(&mut self) {
1452 self.mutate_editor(|e| e.maybe_request_document_color());
1454 }
1455
1456 pub(super) fn do_lsp_color_presentation(&mut self) {
1468 self.mutate_editor(|e| e.do_lsp_color_presentation());
1470 }
1471
1472 pub(super) fn accept_lsp_color_presentation(&mut self, index: u32) {
1474 self.mutate_editor(move |e| e.accept_lsp_color_presentation(index));
1475 }
1476
1477 pub fn drain_code_lens_refresh(&mut self) {
1482 self.mutate_editor(|e| e.drain_code_lens_refresh());
1484 }
1485
1486 pub fn maybe_request_document_highlight(&mut self) {
1501 self.mutate_editor(|e| e.maybe_request_document_highlight());
1503 }
1504 pub fn do_lsp_expand_region(&mut self) {
1516 self.mutate_editor(|e| e.do_lsp_expand_region());
1519 }
1520
1521 pub fn do_lsp_shrink_region(&mut self) {
1523 self.mutate_editor(|e| e.do_lsp_shrink_region());
1524 }
1525
1526 pub fn drain_pending_selection_range(&mut self) {
1530 self.mutate_editor(|e| e.drain_pending_selection_range());
1532 }
1533
1534 pub fn do_lsp_progress_cancel(&mut self, server_id: Option<&str>) {
1542 let server_id = server_id.map(|s| s.to_string());
1544 self.mutate_editor(move |e| e.do_lsp_progress_cancel(server_id.as_deref()));
1545 }
1546
1547 pub fn do_set_lsp_log_level(&mut self, server_id: Option<&str>, level: &str) {
1551 let server_id = server_id.map(|s| s.to_string());
1553 let level = level.to_string();
1554 self.mutate_editor(move |e| e.do_set_lsp_log_level(server_id.as_deref(), &level));
1555 }
1556
1557 pub fn do_lsp_log_clear(&mut self, server_id: Option<&str>) {
1559 let server_id = server_id.map(|s| s.to_string());
1561 self.mutate_editor(move |e| e.do_lsp_log_clear(server_id.as_deref()));
1562 }
1563
1564 pub(super) fn open_lsp_log_in_pane(&mut self, server_id: &str) {
1574 let server_id = server_id.to_string();
1576 self.mutate_editor(move |e| e.open_lsp_log_in_pane(&server_id));
1577 }
1578
1579 pub(super) fn open_lsp_trace_log_in_pane(&mut self, server_id: &str) {
1581 let server_id = server_id.to_string();
1582 self.mutate_editor(move |e| e.open_lsp_trace_log_in_pane(&server_id));
1583 }
1584
1585 pub(super) fn publish_position_change(&self) {
1591 }
1595
1596 pub(super) fn resolve_server_id(&self, name: &str) -> Option<String> {
1610 let name = name.to_string();
1612 self.read_editor(move |e| e.resolve_server_id(&name))
1613 }
1614
1615 pub(super) fn running_server_ids(&self) -> Vec<String> {
1617 self.read_editor(move |e| e.running_server_ids())
1619 }
1620}
1621
1622#[cfg(test)]
1623mod tests {
1624 #![allow(clippy::unwrap_used, clippy::panic)]
1625
1626 use super::*;
1627 use crate::app::test_helpers::{app_with, seed_diags_at_lines};
1628 use crate::app::*;
1629
1630 fn lsp_meta_candidate(
1636 meta: lattice_lsp::completion::LspCompletionMeta,
1637 ) -> lattice_completion::RawCandidate {
1638 let display = match meta.detail.as_ref() {
1639 Some(d) => format!("{} {}", meta.label, d),
1640 None => meta.label.clone(),
1641 };
1642 let match_text = meta
1643 .filter_text
1644 .clone()
1645 .unwrap_or_else(|| meta.label.clone());
1646 let payload = lattice_lsp::completion::encode_meta(&meta);
1647 let mut raw = lattice_completion::RawCandidate::plain(
1648 match_text,
1649 lattice_completion::CandidateKind::Plain,
1650 )
1651 .with_source(lattice_completion::SourceId::new(
1652 lattice_completion::LSP_COMPLETION_SOURCE_ID,
1653 ));
1654 raw.display = display;
1655 raw.data = lattice_completion::CandidateData::Extension {
1656 kind_id: LSP_COMPLETION_KIND_ID,
1657 payload,
1658 };
1659 raw
1660 }
1661
1662 #[test]
1663 fn lsp_mode_gates_document_changed_typed_event_at_publish_site() {
1664 let mut a = app_with("xx", 10);
1672 let (tx, mut rx) =
1673 tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspDocumentChanged>();
1674 a.editor.event_bus.subscribe_typed(tx);
1675 a.apply(Action::Insert("a".into()));
1678 assert!(
1679 rx.try_recv().is_err(),
1680 "lsp-mode off should suppress LspDocumentChanged"
1681 );
1682 a.toggle_mode_by_name("lsp-mode");
1685 a.apply(Action::Insert("b".into()));
1686 let received = rx.try_recv();
1687 assert!(
1688 received.is_ok(),
1689 "lsp-mode on should emit LspDocumentChanged on edit"
1690 );
1691 }
1692
1693 #[tokio::test(flavor = "multi_thread")]
1694 async fn lsp_mode_round_trip_end_to_end() {
1695 use crate::app::test_helpers::app_with_path;
1707 let mut a = app_with_path("fn main() {}", 5, std::path::PathBuf::from("foo.rs"));
1708 let id = a.editor.pane_tree.active().buffer_id;
1709 assert!(a.lsp_mode_enabled_for(id), "M.5.2 auto-activation");
1710
1711 let (detach_tx, mut detach_rx) =
1712 tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspBufferDetached>();
1713 a.editor.event_bus.subscribe_typed(detach_tx);
1714 let (changed_tx, mut changed_rx) =
1715 tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspDocumentChanged>();
1716 a.editor.event_bus.subscribe_typed(changed_tx);
1717
1718 a.toggle_mode_by_name("lsp-mode");
1720 assert!(!a.lsp_mode_enabled_for(id));
1721 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
1725 while detach_rx.try_recv().is_err() {
1726 if std::time::Instant::now() >= deadline {
1727 panic!("LspBufferDetached did not arrive within 2s");
1728 }
1729 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1730 }
1731 a.apply_effect(lattice_grammar::Effect::Lsp(
1733 lattice_grammar::LspRequest::Hover,
1734 ));
1735 let msg = a.editor.last_message.as_ref().expect("gate echo");
1736 assert!(
1737 msg.text.contains("lsp-mode disabled"),
1738 "expected gate echo, got: {}",
1739 msg.text
1740 );
1741 a.apply(Action::Insert("a".into()));
1743 assert!(
1744 changed_rx.try_recv().is_err(),
1745 "lsp-mode off should suppress LspDocumentChanged"
1746 );
1747
1748 a.toggle_mode_by_name("lsp-mode");
1750 assert!(a.lsp_mode_enabled_for(id));
1751 a.apply(Action::Insert("b".into()));
1752 assert!(
1753 changed_rx.try_recv().is_ok(),
1754 "lsp-mode on should re-emit LspDocumentChanged"
1755 );
1756 }
1757
1758 #[test]
1759 fn lsp_mode_off_gates_request_entry_points_with_info_echo() {
1760 let mut a = app_with("xx", 10);
1766 assert!(!a.lsp_mode_enabled_for(a.editor.document_buffer_id));
1768 a.apply_effect(lattice_grammar::Effect::Lsp(
1769 lattice_grammar::LspRequest::Hover,
1770 ));
1771 let msg = a.editor.last_message.as_ref().expect("gate echo");
1772 assert_eq!(msg.level, EchoLevel::Info);
1773 assert!(
1774 msg.text.contains("lsp-mode disabled"),
1775 "expected lsp-mode-disabled echo, got: {}",
1776 msg.text
1777 );
1778 }
1779
1780 #[test]
1781 fn lsp_hover_mode_off_with_umbrella_on_echoes_sub_mode_message() {
1782 let mut a = app_with("xx", 10);
1786 a.toggle_mode_by_name("lsp-mode");
1787 a.toggle_mode_by_name("lsp-hover-mode");
1790 assert!(a.lsp_mode_enabled_for(a.editor.document_buffer_id));
1791 assert!(!a.lsp_hover_mode_enabled_for(a.editor.document_buffer_id));
1792 a.apply_effect(lattice_grammar::Effect::Lsp(
1795 lattice_grammar::LspRequest::Hover,
1796 ));
1797 let msg = a.editor.last_message.as_ref().expect("gate echo");
1798 assert_eq!(msg.level, EchoLevel::Info);
1799 assert!(
1800 msg.text.contains("lsp-hover-mode disabled"),
1801 "expected lsp-hover-mode-disabled echo, got: {}",
1802 msg.text
1803 );
1804 }
1805
1806 #[test]
1807 fn lsp_format_mode_off_gates_format_request() {
1808 let mut a = app_with("xx", 10);
1812 a.toggle_mode_by_name("lsp-mode");
1813 a.toggle_mode_by_name("lsp-format-mode");
1814 a.do_lsp_format_request(false);
1818 let msg = a.editor.last_message.as_ref().expect("format gate echo");
1819 assert!(
1820 msg.text.contains("lsp-format-mode disabled"),
1821 "expected lsp-format-mode-disabled echo, got: {}",
1822 msg.text
1823 );
1824 }
1825
1826 #[test]
1827 fn lsp_nav_mode_off_gates_definition_request() {
1828 let mut a = app_with("xx", 10);
1831 a.toggle_mode_by_name("lsp-mode");
1832 a.toggle_mode_by_name("lsp-nav-mode");
1833 a.apply_effect(lattice_grammar::Effect::Lsp(
1834 lattice_grammar::LspRequest::Definition,
1835 )); let msg = a.editor.last_message.as_ref().expect("nav gate echo");
1837 assert!(
1838 msg.text.contains("lsp-nav-mode disabled"),
1839 "expected lsp-nav-mode-disabled echo, got: {}",
1840 msg.text
1841 );
1842 }
1843
1844 #[test]
1845 fn umbrella_off_wins_over_sub_mode_state() {
1846 let mut a = app_with("xx", 10);
1852 a.toggle_mode_by_name("lsp-mode");
1857 a.toggle_mode_by_name("lsp-mode");
1858 assert!(!a.lsp_mode_enabled_for(a.editor.document_buffer_id));
1859 a.apply_effect(lattice_grammar::Effect::Lsp(
1860 lattice_grammar::LspRequest::Hover,
1861 ));
1862 let msg = a.editor.last_message.as_ref().expect("umbrella echo");
1863 assert!(
1865 msg.text.contains("lsp-mode disabled") && !msg.text.contains("lsp-hover-mode"),
1866 "expected umbrella echo (not sub-mode echo), got: {}",
1867 msg.text
1868 );
1869 }
1870
1871 #[test]
1872 fn lsp_mode_enabled_for_returns_false_by_default() {
1873 let a = app_with("fn main() {}", 5);
1878 let id = a.editor.pane_tree.active().buffer_id;
1879 assert!(!a.lsp_mode_enabled_for(id));
1880 }
1881
1882 #[test]
1883 fn lsp_mode_enabled_for_tracks_minor_activation() {
1884 let mut a = app_with("fn main() {}", 5);
1889 let id = a.editor.pane_tree.active().buffer_id;
1890 let proto_id = lattice_protocol::ids::BufferId::new(id.0 as u64);
1891 let mut active = a.editor.active_modes.remove(&id).unwrap_or_default();
1892 a.editor
1893 .mode_registry
1894 .load_full()
1895 .activate_minor(
1896 &mut active,
1897 &a.editor.mode_guards,
1898 &a.editor.config,
1899 &a.editor.event_bus,
1900 &a.editor.services,
1901 proto_id,
1902 lattice_lsp::modes::LspMode::mode_id(),
1903 lattice_mode::CapabilitySet::empty(),
1904 )
1905 .expect("activate lsp-mode");
1906 a.editor.active_modes.insert(id, active);
1907 a.editor.publish_render_state();
1908 assert!(a.lsp_mode_enabled_for(id));
1909 }
1910
1911 #[test]
1912 fn hover_dismisses_on_document_cursor_motion() {
1913 let mut a = app_with("fn main() {}\nlet x = 1;\n", 5);
1917 a.do_open_hover("hover body");
1918 assert!(a.editor.popup_buffer.is_some());
1919 assert!(a.editor.prev_pane_for_popup.is_none());
1921 assert!(matches!(a.editor.active_buffer, BufferKind::Document));
1922 let inv = lattice_grammar::CommandInvocation::of(a.editor.builtins.char_right.0);
1924 a.apply(Action::Invoke(inv));
1925 assert!(
1926 a.editor.popup_buffer.is_none(),
1927 "hover popup should dismiss on cursor motion in State A"
1928 );
1929 }
1930
1931 #[test]
1932 fn hover_does_not_dismiss_when_cursor_unchanged() {
1933 let mut a = app_with("fn main() {}\n", 5);
1938 a.do_open_hover("hover body");
1939 assert!(a.editor.popup_buffer.is_some());
1940 a.apply(Action::PushDigit(5));
1941 assert!(
1942 a.editor.popup_buffer.is_some(),
1943 "hover should survive a count-prefix push"
1944 );
1945 }
1946
1947 #[test]
1948 fn hover_open_populates_help_buffer() {
1949 let mut a = app_with("alpha\nbeta\ngamma", 10);
1950 a.editor.cursor = Position::new(1, 2);
1951 a.editor.set_command_line_text("hover documentation");
1952 a.editor.modal = ModalState::Command;
1953 a.apply(Action::CommandLineSubmit);
1954 let h = a.popup_help().expect("hover open");
1955 assert_eq!(h.title, "hover");
1956 assert!(h.content.as_string().contains("documentation"));
1957 assert!(matches!(a.editor.active_buffer, BufferKind::Document));
1959 assert!(a.editor.prev_pane_for_popup.is_none());
1960 }
1961
1962 #[test]
1963 fn hover_close_dismisses_popup() {
1964 let mut a = app_with("xx", 10);
1965 a.editor.set_command_line_text("hover x");
1966 a.editor.modal = ModalState::Command;
1967 a.apply(Action::CommandLineSubmit);
1968 assert!(a.editor.popup_buffer.is_some());
1969 a.editor.set_command_line_text("HoverClose");
1970 a.editor.modal = ModalState::Command;
1971 a.apply(Action::CommandLineSubmit);
1972 assert!(a.editor.popup_buffer.is_none());
1973 }
1974
1975 #[test]
1976 fn hover_with_no_arg_uses_placeholder() {
1977 let mut a = app_with("xx", 10);
1978 a.editor.set_command_line_text("hover");
1979 a.editor.modal = ModalState::Command;
1980 a.apply(Action::CommandLineSubmit);
1981 let h = a.popup_help().expect("hover open");
1982 assert!(h.content.as_string().contains("empty"));
1983 }
1984
1985 #[test]
1986 fn hover_contents_scalar_string_renders_verbatim() {
1987 let m = lattice_lsp::lsp_types::HoverContents::Scalar(
1988 lattice_lsp::lsp_types::MarkedString::String("fn foo() -> u32".into()),
1989 );
1990 assert_eq!(super::hover_contents_to_markdown(&m), "fn foo() -> u32");
1991 }
1992
1993 #[test]
1994 fn hover_contents_language_string_renders_as_fenced_block() {
1995 let m = lattice_lsp::lsp_types::HoverContents::Scalar(
1996 lattice_lsp::lsp_types::MarkedString::LanguageString(
1997 lattice_lsp::lsp_types::LanguageString {
1998 language: "rust".into(),
1999 value: "let x: u32 = 5;".into(),
2000 },
2001 ),
2002 );
2003 let md = super::hover_contents_to_markdown(&m);
2004 assert!(md.contains("```rust"));
2005 assert!(md.contains("let x: u32 = 5;"));
2006 assert!(md.ends_with("```"));
2007 }
2008
2009 #[test]
2010 fn hover_contents_array_joins_with_double_newline() {
2011 let m = lattice_lsp::lsp_types::HoverContents::Array(vec![
2012 lattice_lsp::lsp_types::MarkedString::String("first".into()),
2013 lattice_lsp::lsp_types::MarkedString::String("second".into()),
2014 ]);
2015 let md = super::hover_contents_to_markdown(&m);
2016 assert_eq!(md, "first\n\nsecond");
2017 }
2018
2019 #[test]
2020 fn hover_contents_markup_uses_value_as_markdown() {
2021 let m =
2022 lattice_lsp::lsp_types::HoverContents::Markup(lattice_lsp::lsp_types::MarkupContent {
2023 kind: lattice_lsp::lsp_types::MarkupKind::Markdown,
2024 value: "# heading\n\nbody".into(),
2025 });
2026 assert_eq!(super::hover_contents_to_markdown(&m), "# heading\n\nbody");
2027 }
2028
2029 #[test]
2030 fn lsp_hover_request_with_no_uri_echoes_no_lsp_attached() {
2031 let mut a = app_with("xx", 10);
2037 a.toggle_mode_by_name("lsp-mode");
2038 a.apply_effect(lattice_grammar::Effect::Lsp(
2039 lattice_grammar::LspRequest::Hover,
2040 ));
2041 let msg = a.editor.last_message.as_ref().expect("echo");
2042 assert_eq!(msg.level, EchoLevel::Info);
2043 assert!(msg.text.contains("no LSP server"));
2044 }
2045
2046 #[test]
2047 fn lsp_hover_request_pre_cancels_in_flight_token() {
2048 let mut a = app_with("xx", 10);
2053 let stale = lattice_protocol::CancellationToken::new();
2055 a.editor.pending_hover_token = Some(stale.clone());
2056 a.apply_effect(lattice_grammar::Effect::Lsp(
2060 lattice_grammar::LspRequest::Hover,
2061 ));
2062 assert!(
2063 stale.is_cancelled(),
2064 "prior in-flight hover token should flip on a new K press"
2065 );
2066 }
2067
2068 #[test]
2069 fn drain_pending_hover_body_outcome_opens_popup() {
2070 let mut a = app_with("xx", 10);
2071 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<crate::app::HoverOutcome>();
2072 a.editor.pending_hover_rx = Some(rx);
2073 a.editor.pending_hover_token = Some(lattice_protocol::CancellationToken::new());
2074 tx.send(crate::app::HoverOutcome::Body("**bold body**".into()))
2075 .unwrap();
2076 a.drain_pending_hover();
2077 let h = a.popup_help().expect("popup");
2078 assert!(h.content.as_string().contains("**bold body**"));
2079 assert!(matches!(a.editor.active_buffer, BufferKind::Document));
2081 assert!(a.editor.prev_pane_for_popup.is_none());
2082 assert!(
2083 a.editor.pending_hover_token.is_none(),
2084 "delivering the outcome should clear the in-flight token"
2085 );
2086 }
2087
2088 #[test]
2089 fn drain_pending_hover_no_body_outcome_echoes_no_hover_info() {
2090 let mut a = app_with("xx", 10);
2095 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<crate::app::HoverOutcome>();
2096 a.editor.pending_hover_rx = Some(rx);
2097 a.editor.pending_hover_token = Some(lattice_protocol::CancellationToken::new());
2098 tx.send(crate::app::HoverOutcome::NoBody { servers_tried: 1 })
2099 .unwrap();
2100 a.drain_pending_hover();
2101 assert!(a.editor.popup_buffer.is_none(), "no popup for empty hover");
2102 let msg = a
2103 .editor
2104 .last_message
2105 .as_ref()
2106 .expect("echo on no-hover-info");
2107 assert_eq!(msg.level, EchoLevel::Info);
2108 assert!(
2109 msg.text.contains("no hover info"),
2110 "expected 'no hover info' echo; got `{}`",
2111 msg.text
2112 );
2113 }
2114
2115 #[test]
2116 fn drain_pending_hover_no_servers_outcome_echoes_warn() {
2117 let mut a = app_with("xx", 10);
2121 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<crate::app::HoverOutcome>();
2122 a.editor.pending_hover_rx = Some(rx);
2123 a.editor.pending_hover_token = Some(lattice_protocol::CancellationToken::new());
2124 tx.send(crate::app::HoverOutcome::NoServers).unwrap();
2125 a.drain_pending_hover();
2126 let msg = a
2127 .editor
2128 .last_message
2129 .as_ref()
2130 .expect("echo on no-servers-attached");
2131 assert_eq!(msg.level, EchoLevel::Warn);
2132 assert!(
2133 msg.text.contains("no LSP servers"),
2134 "expected NoServers warn echo; got `{}`",
2135 msg.text
2136 );
2137 }
2138
2139 #[test]
2140 fn drain_pending_hover_idle_channel_is_noop() {
2141 let mut a = app_with("xx", 10);
2142 let (_tx, rx) = tokio::sync::mpsc::unbounded_channel::<crate::app::HoverOutcome>();
2143 a.editor.pending_hover_rx = Some(rx);
2144 a.drain_pending_hover();
2145 assert!(a.editor.popup_buffer.is_none());
2146 assert!(a.editor.last_message.is_none());
2147 }
2148
2149 #[test]
2150 fn app_to_lsp_position_converts_utf8_byte_to_utf16_column() {
2151 let buf = lattice_core::Buffer::from_text("hello\nαβγ\nworld\n");
2152 let p = super::app_to_lsp_position(&buf, Position::new(1, 4)).expect("in-range");
2155 assert_eq!(p.line, 1);
2156 assert_eq!(p.character, 2);
2157 }
2158
2159 #[test]
2160 fn app_to_lsp_position_returns_none_for_out_of_range_line() {
2161 let buf = lattice_core::Buffer::from_text("only-one-line\n");
2162 assert!(super::app_to_lsp_position(&buf, Position::new(99, 0)).is_none());
2163 }
2164
2165 #[test]
2168 fn flatten_selection_range_chain_walks_parent_links() {
2169 let outer = lattice_lsp::lsp_types::SelectionRange {
2170 range: lattice_lsp::lsp_types::Range {
2171 start: lattice_lsp::lsp_types::Position {
2172 line: 0,
2173 character: 0,
2174 },
2175 end: lattice_lsp::lsp_types::Position {
2176 line: 5,
2177 character: 0,
2178 },
2179 },
2180 parent: None,
2181 };
2182 let middle = lattice_lsp::lsp_types::SelectionRange {
2183 range: lattice_lsp::lsp_types::Range {
2184 start: lattice_lsp::lsp_types::Position {
2185 line: 1,
2186 character: 0,
2187 },
2188 end: lattice_lsp::lsp_types::Position {
2189 line: 3,
2190 character: 0,
2191 },
2192 },
2193 parent: Some(Box::new(outer)),
2194 };
2195 let inner = lattice_lsp::lsp_types::SelectionRange {
2196 range: lattice_lsp::lsp_types::Range {
2197 start: lattice_lsp::lsp_types::Position {
2198 line: 2,
2199 character: 0,
2200 },
2201 end: lattice_lsp::lsp_types::Position {
2202 line: 2,
2203 character: 8,
2204 },
2205 },
2206 parent: Some(Box::new(middle)),
2207 };
2208 let flat = crate::app::flatten_selection_range_chain(&inner);
2209 assert_eq!(flat.len(), 3);
2210 assert_eq!(flat[0].start.line, 2); assert_eq!(flat[1].start.line, 1);
2212 assert_eq!(flat[2].start.line, 0); }
2214
2215 #[test]
2224 fn decode_semantic_tokens_absolute_positions() {
2225 let token_types = vec![
2226 lattice_lsp::lsp_types::SemanticTokenType::KEYWORD,
2227 lattice_lsp::lsp_types::SemanticTokenType::FUNCTION,
2228 ];
2229 let token_modifiers = vec![
2230 lattice_lsp::lsp_types::SemanticTokenModifier::STATIC,
2231 lattice_lsp::lsp_types::SemanticTokenModifier::READONLY,
2232 ];
2233 let data = vec![
2238 lattice_lsp::lsp_types::SemanticToken {
2239 delta_line: 0,
2240 delta_start: 0,
2241 length: 3,
2242 token_type: 0,
2243 token_modifiers_bitset: 0,
2244 },
2245 lattice_lsp::lsp_types::SemanticToken {
2246 delta_line: 0,
2247 delta_start: 4,
2248 length: 4,
2249 token_type: 1,
2250 token_modifiers_bitset: 0b01,
2251 },
2252 lattice_lsp::lsp_types::SemanticToken {
2253 delta_line: 2,
2254 delta_start: 2,
2255 length: 1,
2256 token_type: 0,
2257 token_modifiers_bitset: 0b11,
2258 },
2259 ];
2260 let decoded = crate::app::decode_semantic_tokens(&data, &token_types, &token_modifiers);
2261 assert_eq!(decoded.len(), 3);
2262 assert_eq!(decoded[0].line, 0);
2263 assert_eq!(decoded[0].start_char, 0);
2264 assert_eq!(decoded[0].token_type, "keyword");
2265 assert!(decoded[0].modifiers.is_empty());
2266 assert_eq!(decoded[1].line, 0);
2268 assert_eq!(decoded[1].start_char, 4);
2269 assert_eq!(decoded[1].token_type, "function");
2270 assert_eq!(decoded[1].modifiers, vec!["static"]);
2271 assert_eq!(decoded[2].line, 2);
2274 assert_eq!(decoded[2].start_char, 2);
2275 assert_eq!(decoded[2].modifiers, vec!["static", "readonly"]);
2276 }
2277
2278 #[test]
2281 fn decode_semantic_tokens_drops_out_of_range_type() {
2282 let token_types = vec![lattice_lsp::lsp_types::SemanticTokenType::KEYWORD];
2283 let token_modifiers: Vec<lattice_lsp::lsp_types::SemanticTokenModifier> = Vec::new();
2284 let data = vec![
2285 lattice_lsp::lsp_types::SemanticToken {
2286 delta_line: 0,
2287 delta_start: 0,
2288 length: 3,
2289 token_type: 0,
2290 token_modifiers_bitset: 0,
2291 },
2292 lattice_lsp::lsp_types::SemanticToken {
2293 delta_line: 0,
2294 delta_start: 4,
2295 length: 4,
2296 token_type: 99, token_modifiers_bitset: 0,
2298 },
2299 ];
2300 let decoded = crate::app::decode_semantic_tokens(&data, &token_types, &token_modifiers);
2301 assert_eq!(decoded.len(), 1);
2303 assert_eq!(decoded[0].token_type, "keyword");
2304 }
2305
2306 #[test]
2310 fn apply_semantic_token_edits_replace_middle() {
2311 let mut raw = vec![
2312 lattice_lsp::lsp_types::SemanticToken {
2313 delta_line: 0,
2314 delta_start: 0,
2315 length: 2,
2316 token_type: 0,
2317 token_modifiers_bitset: 0,
2318 },
2319 lattice_lsp::lsp_types::SemanticToken {
2320 delta_line: 0,
2321 delta_start: 3,
2322 length: 4,
2323 token_type: 1,
2324 token_modifiers_bitset: 0,
2325 },
2326 lattice_lsp::lsp_types::SemanticToken {
2327 delta_line: 1,
2328 delta_start: 2,
2329 length: 1,
2330 token_type: 0,
2331 token_modifiers_bitset: 0,
2332 },
2333 ];
2334 let edit = lattice_lsp::lsp_types::SemanticTokensEdit {
2336 start: 1,
2337 delete_count: 1,
2338 data: Some(vec![
2339 lattice_lsp::lsp_types::SemanticToken {
2340 delta_line: 0,
2341 delta_start: 3,
2342 length: 2,
2343 token_type: 2,
2344 token_modifiers_bitset: 0,
2345 },
2346 lattice_lsp::lsp_types::SemanticToken {
2347 delta_line: 0,
2348 delta_start: 5,
2349 length: 3,
2350 token_type: 1,
2351 token_modifiers_bitset: 0,
2352 },
2353 ]),
2354 };
2355 crate::app::apply_semantic_token_edits(&mut raw, &[edit])
2356 .expect("splice succeeds in range");
2357 assert_eq!(raw.len(), 4);
2358 assert_eq!(raw[1].token_type, 2);
2359 assert_eq!(raw[2].token_type, 1);
2360 assert_eq!(raw[3].delta_line, 1);
2362 }
2363
2364 #[test]
2367 fn apply_semantic_token_edits_insert_only() {
2368 let mut raw = vec![lattice_lsp::lsp_types::SemanticToken {
2369 delta_line: 0,
2370 delta_start: 0,
2371 length: 2,
2372 token_type: 0,
2373 token_modifiers_bitset: 0,
2374 }];
2375 let edit = lattice_lsp::lsp_types::SemanticTokensEdit {
2376 start: 1,
2377 delete_count: 0,
2378 data: Some(vec![lattice_lsp::lsp_types::SemanticToken {
2379 delta_line: 0,
2380 delta_start: 3,
2381 length: 4,
2382 token_type: 1,
2383 token_modifiers_bitset: 0,
2384 }]),
2385 };
2386 crate::app::apply_semantic_token_edits(&mut raw, &[edit]).expect("insert at end succeeds");
2387 assert_eq!(raw.len(), 2);
2388 assert_eq!(raw[1].token_type, 1);
2389 }
2390
2391 #[test]
2394 fn apply_semantic_token_edits_delete_only() {
2395 let mut raw = vec![
2396 lattice_lsp::lsp_types::SemanticToken {
2397 delta_line: 0,
2398 delta_start: 0,
2399 length: 2,
2400 token_type: 0,
2401 token_modifiers_bitset: 0,
2402 },
2403 lattice_lsp::lsp_types::SemanticToken {
2404 delta_line: 0,
2405 delta_start: 3,
2406 length: 4,
2407 token_type: 1,
2408 token_modifiers_bitset: 0,
2409 },
2410 ];
2411 let edit = lattice_lsp::lsp_types::SemanticTokensEdit {
2412 start: 1,
2413 delete_count: 1,
2414 data: None,
2415 };
2416 crate::app::apply_semantic_token_edits(&mut raw, &[edit])
2417 .expect("delete in range succeeds");
2418 assert_eq!(raw.len(), 1);
2419 assert_eq!(raw[0].token_type, 0);
2420 }
2421
2422 #[test]
2426 fn apply_semantic_token_edits_out_of_bounds_errs() {
2427 let mut raw = vec![lattice_lsp::lsp_types::SemanticToken {
2428 delta_line: 0,
2429 delta_start: 0,
2430 length: 2,
2431 token_type: 0,
2432 token_modifiers_bitset: 0,
2433 }];
2434 let edit = lattice_lsp::lsp_types::SemanticTokensEdit {
2435 start: 5,
2436 delete_count: 2,
2437 data: None,
2438 };
2439 assert!(
2440 crate::app::apply_semantic_token_edits(&mut raw, &[edit]).is_err(),
2441 "out-of-bounds edit must err"
2442 );
2443 assert_eq!(raw.len(), 1);
2445 }
2446
2447 #[test]
2451 fn drain_semantic_tokens_delta_splices_and_redecodes() {
2452 let a = app_with("fn main() {}\n", 5);
2453 let buffer_id = a.editor.document_buffer_id;
2454 let initial_raw = vec![lattice_lsp::lsp_types::SemanticToken {
2459 delta_line: 0,
2460 delta_start: 0,
2461 length: 2,
2462 token_type: 0,
2463 token_modifiers_bitset: 0,
2464 }];
2465 {
2470 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2471 a.editor.lsp_semantic_tokens_cache.insert_for(
2472 buffer_id,
2473 crate::app::LspSemanticTokensCache {
2474 document_version: 1,
2475 result_id: Some("r1".into()),
2476 raw_data: initial_raw,
2477 tokens: vec![crate::app::DecodedSemanticToken {
2478 line: 0,
2479 start_char: 0,
2480 length: 2,
2481 token_type: "keyword".into(),
2482 modifiers: Vec::new(),
2483 }],
2484 },
2485 );
2486 }
2487 let token_types = vec![
2491 lattice_lsp::lsp_types::SemanticTokenType::KEYWORD,
2492 lattice_lsp::lsp_types::SemanticTokenType::FUNCTION,
2493 ];
2494 let token_modifiers: Vec<lattice_lsp::lsp_types::SemanticTokenModifier> = Vec::new();
2495 let edits = vec![lattice_lsp::lsp_types::SemanticTokensEdit {
2496 start: 1,
2497 delete_count: 0,
2498 data: Some(vec![lattice_lsp::lsp_types::SemanticToken {
2499 delta_line: 0,
2500 delta_start: 3,
2501 length: 4,
2502 token_type: 1,
2503 token_modifiers_bitset: 0,
2504 }]),
2505 }];
2506 lattice_host::editor::Editor::apply_semantic_tokens_delta_outcome(
2507 &a.editor.lsp_semantic_tokens_cache,
2508 buffer_id,
2509 2,
2510 "r1",
2511 Some("r2".into()),
2512 &edits,
2513 &token_types,
2514 &token_modifiers,
2515 );
2516 let cache = {
2517 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2518 a.editor
2519 .lsp_semantic_tokens_cache
2520 .get_for(buffer_id)
2521 .expect("cache seated")
2522 };
2523 assert_eq!(cache.document_version, 2);
2524 assert_eq!(cache.result_id.as_deref(), Some("r2"));
2525 assert_eq!(cache.raw_data.len(), 2);
2526 assert_eq!(cache.tokens.len(), 2);
2527 assert_eq!(cache.tokens[1].token_type, "function");
2528 assert_eq!(cache.tokens[1].start_char, 3);
2529 }
2530
2531 #[test]
2536 fn drain_semantic_tokens_delta_stale_baseline_evicts_cache() {
2537 let a = app_with("fn main() {}\n", 5);
2538 let buffer_id = a.editor.document_buffer_id;
2539 {
2542 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2543 a.editor.lsp_semantic_tokens_cache.insert_for(
2544 buffer_id,
2545 crate::app::LspSemanticTokensCache {
2546 document_version: 1,
2547 result_id: Some("different-id".into()),
2548 raw_data: Vec::new(),
2549 tokens: Vec::new(),
2550 },
2551 );
2552 }
2553 lattice_host::editor::Editor::apply_semantic_tokens_delta_outcome(
2554 &a.editor.lsp_semantic_tokens_cache,
2555 buffer_id,
2556 2,
2557 "r1",
2558 Some("r2".into()),
2559 &[],
2560 &[],
2561 &[],
2562 );
2563 let evicted = {
2564 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2565 a.editor
2566 .lsp_semantic_tokens_cache
2567 .get_for(buffer_id)
2568 .is_none()
2569 };
2570 assert!(evicted, "stale-baseline delta should evict the cache",);
2571 }
2572
2573 #[test]
2578 fn apply_semantic_token_edits_sequential() {
2579 let mut raw = vec![
2580 lattice_lsp::lsp_types::SemanticToken {
2581 delta_line: 0,
2582 delta_start: 0,
2583 length: 2,
2584 token_type: 0,
2585 token_modifiers_bitset: 0,
2586 },
2587 lattice_lsp::lsp_types::SemanticToken {
2588 delta_line: 0,
2589 delta_start: 3,
2590 length: 4,
2591 token_type: 1,
2592 token_modifiers_bitset: 0,
2593 },
2594 ];
2595 let edits = vec![
2596 lattice_lsp::lsp_types::SemanticTokensEdit {
2598 start: 0,
2599 delete_count: 1,
2600 data: None,
2601 },
2602 lattice_lsp::lsp_types::SemanticTokensEdit {
2604 start: 0,
2605 delete_count: 0,
2606 data: Some(vec![lattice_lsp::lsp_types::SemanticToken {
2607 delta_line: 0,
2608 delta_start: 0,
2609 length: 5,
2610 token_type: 2,
2611 token_modifiers_bitset: 0,
2612 }]),
2613 },
2614 ];
2615 crate::app::apply_semantic_token_edits(&mut raw, &edits).expect("sequential edits succeed");
2616 assert_eq!(raw.len(), 2);
2617 assert_eq!(raw[0].token_type, 2);
2618 assert_eq!(raw[1].token_type, 1);
2619 }
2620
2621 #[test]
2626 fn drain_pending_pull_diagnostics_full_applies_to_layer() {
2627 use std::str::FromStr;
2628 let app = app_with("fn main() {}\n", 5);
2629 let buffer_id = app.editor.document_buffer_id;
2630 let uri = lattice_lsp::Uri::from_str("file:///tmp/x.rs").unwrap();
2631 let server_id: std::sync::Arc<str> = std::sync::Arc::from("rust");
2632 let diag = lattice_lsp::lsp_types::Diagnostic {
2633 range: lattice_lsp::lsp_types::Range {
2634 start: lattice_lsp::lsp_types::Position {
2635 line: 0,
2636 character: 0,
2637 },
2638 end: lattice_lsp::lsp_types::Position {
2639 line: 0,
2640 character: 2,
2641 },
2642 },
2643 severity: Some(lattice_lsp::lsp_types::DiagnosticSeverity::ERROR),
2644 code: None,
2645 code_description: None,
2646 source: None,
2647 message: "boom".into(),
2648 related_information: None,
2649 tags: None,
2650 data: None,
2651 };
2652 lattice_host::editor::Editor::apply_pull_diagnostics_outcome(
2656 &app.editor.lsp_pull_diagnostics_cache,
2657 &app.editor.lsp_diagnostics,
2658 crate::app::PullDiagnosticsOutcome::Full {
2659 buffer_id,
2660 server_id: server_id.clone(),
2661 uri: uri.clone(),
2662 document_version: 1,
2663 result_id: Some("r1".into()),
2664 diagnostics: vec![diag.clone()],
2665 },
2666 );
2667 let cache = {
2668 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2669 app.editor
2670 .lsp_pull_diagnostics_cache
2671 .get_for(buffer_id)
2672 .expect("cache seated")
2673 };
2674 assert_eq!(cache.document_version, 1);
2675 assert_eq!(cache.result_id.as_deref(), Some("r1"));
2676 let snap = app.editor.lsp_diagnostics.snapshot();
2680 let entry = snap
2681 .iter()
2682 .find(|(u, _)| u.as_str() == uri.as_str())
2683 .expect("uri in layer");
2684 let msgs: Vec<_> = entry.1.iter().map(|d| d.message.as_str()).collect();
2685 assert!(msgs.contains(&"boom"));
2686 }
2687
2688 #[test]
2693 fn drain_pending_pull_diagnostics_unchanged_keeps_layer_state() {
2694 let app = app_with("fn main() {}\n", 5);
2695 let buffer_id = app.editor.document_buffer_id;
2696 {
2698 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2699 app.editor.lsp_pull_diagnostics_cache.insert_for(
2700 buffer_id,
2701 crate::app::LspPullDiagnosticsCache {
2702 document_version: 1,
2703 result_id: Some("r1".into()),
2704 },
2705 );
2706 }
2707 lattice_host::editor::Editor::apply_pull_diagnostics_outcome(
2710 &app.editor.lsp_pull_diagnostics_cache,
2711 &app.editor.lsp_diagnostics,
2712 crate::app::PullDiagnosticsOutcome::Unchanged {
2713 buffer_id,
2714 document_version: 2,
2715 result_id: "r2".into(),
2716 },
2717 );
2718 let cache = {
2719 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2720 app.editor
2721 .lsp_pull_diagnostics_cache
2722 .get_for(buffer_id)
2723 .expect("cache seated")
2724 };
2725 assert_eq!(cache.document_version, 2);
2726 assert_eq!(cache.result_id.as_deref(), Some("r2"));
2727 }
2728
2729 #[test]
2739 fn drain_diagnostic_refresh_handles_no_attached_buffers() {
2740 let mut app = app_with("fn main() {}\n", 5);
2741 let buffer_id = app.editor.document_buffer_id;
2742 {
2744 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2745 app.editor.lsp_pull_diagnostics_cache.insert_for(
2746 buffer_id,
2747 crate::app::LspPullDiagnosticsCache {
2748 document_version: 1,
2749 result_id: Some("r1".into()),
2750 },
2751 );
2752 }
2753 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspDiagnosticRefresh>();
2754 tx.send(lattice_lsp::LspDiagnosticRefresh {
2755 server_id: std::sync::Arc::from("rust"),
2756 })
2757 .expect("send refresh");
2758 app.editor.pending_diagnostic_refresh_rx = Some(rx);
2759 app.drain_diagnostic_refresh();
2760 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2762 assert!(
2763 app.editor
2764 .lsp_pull_diagnostics_cache
2765 .get_for(buffer_id)
2766 .is_some()
2767 );
2768 }
2769
2770 #[test]
2774 fn pull_diagnostics_pump_skips_when_version_unchanged() {
2775 let mut app = app_with("fn main() {}\n", 5);
2776 let buffer_id = app.editor.document_buffer_id;
2777 let version = app.editor.document.snapshot().version;
2778 {
2782 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2783 app.editor.lsp_pull_diagnostics_cache.insert_for(
2784 buffer_id,
2785 crate::app::LspPullDiagnosticsCache {
2786 document_version: version,
2787 result_id: Some("r1".into()),
2788 },
2789 );
2790 }
2791 app.maybe_request_pull_diagnostics();
2792 assert!(
2793 app.editor.pending_pull_diagnostics_token.is_none(),
2794 "pump should short-circuit on unchanged version",
2795 );
2796 }
2797
2798 #[test]
2802 fn inlay_hint_pump_skips_when_viewport_inside_cached_range() {
2803 use std::str::FromStr;
2804 let mut app = app_with("fn main() {}\n", 5);
2805 if !app.lsp_inlay_hint_mode_enabled_for(app.editor.document_buffer_id) {
2807 app.toggle_mode_by_name("lsp-inlay-hint-mode");
2808 }
2809 let uri = lattice_lsp::Uri::from_str("file:///tmp/x.rs").unwrap();
2810 app.editor
2811 .buffer_uris
2812 .insert(app.editor.document_buffer_id, uri);
2813 {
2818 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2819 app.editor.lsp_inlay_hints_cache.insert_for(
2820 app.editor.document_buffer_id,
2821 crate::app::LspInlayHintCache {
2822 document_version: app.editor.document.snapshot().text_version,
2825 hints: Vec::new(),
2826 requested_first_line: 0,
2827 requested_last_line: 1000,
2828 },
2829 );
2830 }
2831 app.editor.publish_render_state();
2832 app.editor.scroll = 0;
2839 app.maybe_request_inlay_hint();
2840 assert!(
2841 app.editor.pending_inlay_hint_token.is_none(),
2842 "pump should short-circuit when viewport is inside cached range",
2843 );
2844 }
2845
2846 #[test]
2851 fn inlay_hint_pump_refetches_when_viewport_outside_cached_range() {
2852 use std::str::FromStr;
2853 let mut app = app_with(&"a\n".repeat(2000), 5);
2854 if !app.lsp_inlay_hint_mode_enabled_for(app.editor.document_buffer_id) {
2855 app.toggle_mode_by_name("lsp-inlay-hint-mode");
2856 }
2857 let uri = lattice_lsp::Uri::from_str("file:///tmp/x.rs").unwrap();
2858 app.editor
2859 .buffer_uris
2860 .insert(app.editor.document_buffer_id, uri);
2861 {
2863 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2864 app.editor.lsp_inlay_hints_cache.insert_for(
2865 app.editor.document_buffer_id,
2866 crate::app::LspInlayHintCache {
2867 document_version: app.editor.document.snapshot().text_version,
2870 hints: Vec::new(),
2871 requested_first_line: 0,
2872 requested_last_line: 200,
2873 },
2874 );
2875 }
2876 app.editor.publish_render_state();
2877 app.editor.scroll = 1500;
2882 app.maybe_request_inlay_hint();
2883 assert!(
2884 app.editor.pending_inlay_hint_token.is_some(),
2885 "pump should issue a new request when viewport leaves cached range",
2886 );
2887 }
2888
2889 #[test]
2896 fn inlay_hint_pump_small_scroll_within_overscan_keeps_cache() {
2897 use std::str::FromStr;
2898 let mut app = app_with(&"a\n".repeat(2000), 5);
2899 if !app.lsp_inlay_hint_mode_enabled_for(app.editor.document_buffer_id) {
2900 app.toggle_mode_by_name("lsp-inlay-hint-mode");
2901 }
2902 let uri = lattice_lsp::Uri::from_str("file:///tmp/x.rs").unwrap();
2903 app.editor
2904 .buffer_uris
2905 .insert(app.editor.document_buffer_id, uri);
2906 {
2909 use lattice_host::per_buffer_cache::PerBufferCacheExt;
2910 app.editor.lsp_inlay_hints_cache.insert_for(
2911 app.editor.document_buffer_id,
2912 crate::app::LspInlayHintCache {
2913 document_version: app.editor.document.snapshot().text_version,
2916 hints: Vec::new(),
2917 requested_first_line: 100,
2918 requested_last_line: 400,
2919 },
2920 );
2921 }
2922 app.editor.publish_render_state();
2923 app.editor.scroll = 250;
2926 app.maybe_request_inlay_hint();
2927 assert!(
2928 app.editor.pending_inlay_hint_token.is_none(),
2929 "small scroll inside cached window should not refetch",
2930 );
2931 }
2932
2933 #[test]
2935 fn folding_range_to_fold_preserves_extents_and_keys_identity() {
2936 let r = lattice_lsp::lsp_types::FoldingRange {
2937 start_line: 2,
2938 end_line: 5,
2939 start_character: None,
2940 end_character: None,
2941 kind: Some(lattice_lsp::lsp_types::FoldingRangeKind::Comment),
2942 collapsed_text: None,
2943 };
2944 let f = crate::app::folding_range_to_fold(r.clone());
2945 assert_eq!(f.start_line, 2);
2946 assert_eq!(f.end_line, 5);
2947 assert!(!f.closed);
2948 assert!(f.identity.is_some());
2949
2950 let f2 = crate::app::folding_range_to_fold(r);
2953 assert_eq!(f.identity, f2.identity);
2954
2955 let r3 = lattice_lsp::lsp_types::FoldingRange {
2957 start_line: 2,
2958 end_line: 9,
2959 start_character: None,
2960 end_character: None,
2961 kind: Some(lattice_lsp::lsp_types::FoldingRangeKind::Comment),
2962 collapsed_text: None,
2963 };
2964 let f3 = crate::app::folding_range_to_fold(r3);
2965 assert_ne!(f.identity, f3.identity);
2966 }
2967
2968 #[test]
2976 fn lsp_folding_mode_toggle_syncs_foldmethod() {
2977 use lattice_core::FoldMethod;
2978 let mut app = app_with("fn a() {}\n", 5);
2979 app.set_foldmethod_for_test(FoldMethod::Syntax);
2980 assert_eq!(app.foldmethod(), FoldMethod::Syntax);
2981 if app.lsp_folding_mode_enabled_for(app.editor.document_buffer_id) {
2982 app.toggle_mode_by_name("lsp-folding-mode");
2983 }
2984 app.toggle_mode_by_name("lsp-folding-mode");
2986 assert!(app.lsp_folding_mode_enabled_for(app.editor.document_buffer_id));
2987 assert_eq!(app.foldmethod(), FoldMethod::Lsp);
2988 app.toggle_mode_by_name("lsp-folding-mode");
2990 assert!(!app.lsp_folding_mode_enabled_for(app.editor.document_buffer_id));
2991 assert_eq!(app.foldmethod(), FoldMethod::Syntax);
2992 }
2993
2994 #[test]
2997 fn recompute_folds_with_foldmethod_lsp_reads_cache() {
2998 use lattice_core::FoldMethod;
2999 let mut app = app_with("fn a() {}\nfn b() {}\nfn c() {}\n", 5);
3000 app.set_foldmethod_for_test(FoldMethod::Lsp);
3001 let fold = crate::app::folding_range_to_fold(lattice_lsp::lsp_types::FoldingRange {
3002 start_line: 0,
3003 end_line: 1,
3004 start_character: None,
3005 end_character: None,
3006 kind: None,
3007 collapsed_text: None,
3008 });
3009 {
3012 use lattice_host::per_buffer_cache::PerBufferCacheExt;
3013 app.editor.lsp_folds_cache.insert_for(
3014 app.editor.document_buffer_id,
3015 crate::app::LspFoldsCache {
3016 document_version: app.editor.document.snapshot().text_version,
3019 folds: vec![fold],
3020 },
3021 );
3022 }
3023 app.editor.publish_render_state();
3024 if !app.lsp_folding_mode_enabled_for(app.editor.document_buffer_id) {
3028 app.toggle_mode_by_name("lsp-folding-mode");
3029 }
3030 app.recompute_folds();
3031 assert!(
3032 app.editor
3033 .folds
3034 .iter()
3035 .any(|f| f.start_line == 0 && f.end_line == 1),
3036 "expected LSP fold from cache; got {:?}",
3037 app.editor.folds,
3038 );
3039 }
3040
3041 #[test]
3044 fn cursor_inside_range_is_half_open() {
3045 let r = lattice_lsp::lsp_types::Range {
3046 start: lattice_lsp::lsp_types::Position {
3047 line: 1,
3048 character: 4,
3049 },
3050 end: lattice_lsp::lsp_types::Position {
3051 line: 1,
3052 character: 8,
3053 },
3054 };
3055 assert!(crate::app::cursor_inside_range(Position::new(1, 4), &r));
3056 assert!(crate::app::cursor_inside_range(Position::new(1, 6), &r));
3057 assert!(!crate::app::cursor_inside_range(Position::new(1, 8), &r));
3058 assert!(!crate::app::cursor_inside_range(Position::new(0, 6), &r));
3059 assert!(!crate::app::cursor_inside_range(Position::new(2, 6), &r));
3060 }
3061
3062 fn fake_uri(path: &str) -> lattice_lsp::lsp_types::Uri {
3064 lattice_lsp::actor::uri_from_path(std::path::Path::new(path))
3065 }
3066
3067 fn loc(path: &str, line: u32, col: u32) -> lattice_lsp::lsp_types::Location {
3068 lattice_lsp::lsp_types::Location {
3069 uri: fake_uri(path),
3070 range: lattice_lsp::lsp_types::Range {
3071 start: lattice_lsp::lsp_types::Position {
3072 line,
3073 character: col,
3074 },
3075 end: lattice_lsp::lsp_types::Position {
3076 line,
3077 character: col + 1,
3078 },
3079 },
3080 }
3081 }
3082
3083 #[test]
3084 fn definition_response_scalar_flattens_to_one_location() {
3085 let resp = lattice_lsp::lsp_types::GotoDefinitionResponse::Scalar(loc("/x.rs", 1, 2));
3086 let v = super::definition_response_to_locations(resp);
3087 assert_eq!(v.len(), 1);
3088 assert_eq!(v[0].range.start.line, 1);
3089 }
3090
3091 #[test]
3092 fn definition_response_array_flattens_verbatim() {
3093 let resp = lattice_lsp::lsp_types::GotoDefinitionResponse::Array(vec![
3094 loc("/a.rs", 0, 0),
3095 loc("/b.rs", 5, 5),
3096 ]);
3097 let v = super::definition_response_to_locations(resp);
3098 assert_eq!(v.len(), 2);
3099 }
3100
3101 #[test]
3102 fn definition_response_link_uses_target_selection_range() {
3103 let link = lattice_lsp::lsp_types::LocationLink {
3106 origin_selection_range: None,
3107 target_uri: fake_uri("/x.rs"),
3108 target_range: lattice_lsp::lsp_types::Range {
3109 start: lattice_lsp::lsp_types::Position {
3110 line: 0,
3111 character: 0,
3112 },
3113 end: lattice_lsp::lsp_types::Position {
3114 line: 10,
3115 character: 0,
3116 },
3117 },
3118 target_selection_range: lattice_lsp::lsp_types::Range {
3119 start: lattice_lsp::lsp_types::Position {
3120 line: 5,
3121 character: 4,
3122 },
3123 end: lattice_lsp::lsp_types::Position {
3124 line: 5,
3125 character: 7,
3126 },
3127 },
3128 };
3129 let resp = lattice_lsp::lsp_types::GotoDefinitionResponse::Link(vec![link]);
3130 let v = super::definition_response_to_locations(resp);
3131 assert_eq!(v.len(), 1);
3132 assert_eq!(v[0].range.start.line, 5);
3134 assert_eq!(v[0].range.start.character, 4);
3135 }
3136
3137 #[test]
3138 fn lsp_definition_request_with_no_uri_echoes_no_lsp_attached() {
3139 let mut a = app_with("xx", 10);
3140 a.toggle_mode_by_name("lsp-mode");
3141 a.apply_effect(lattice_grammar::Effect::Lsp(
3142 lattice_grammar::LspRequest::Definition,
3143 ));
3144 let msg = a.editor.last_message.as_ref().expect("echo");
3145 assert_eq!(msg.level, EchoLevel::Info);
3146 assert!(msg.text.contains("no LSP server"));
3147 }
3148
3149 #[test]
3150 fn lsp_declaration_request_routes_through_unified_nav_dispatch() {
3151 let mut a = app_with("xx", 10);
3152 a.toggle_mode_by_name("lsp-mode");
3153 a.apply_effect(lattice_grammar::Effect::Lsp(
3154 lattice_grammar::LspRequest::Declaration,
3155 ));
3156 let msg = a.editor.last_message.as_ref().expect("echo");
3158 assert_eq!(msg.level, EchoLevel::Info);
3159 assert!(msg.text.contains("no LSP server"));
3160 }
3161
3162 #[test]
3163 fn lsp_type_definition_request_routes_through_unified_nav_dispatch() {
3164 let mut a = app_with("xx", 10);
3165 a.toggle_mode_by_name("lsp-mode");
3166 a.apply_effect(lattice_grammar::Effect::Lsp(
3167 lattice_grammar::LspRequest::TypeDefinition,
3168 ));
3169 let msg = a.editor.last_message.as_ref().expect("echo");
3170 assert!(msg.text.contains("no LSP server"));
3171 }
3172
3173 #[test]
3174 fn lsp_implementation_request_routes_through_unified_nav_dispatch() {
3175 let mut a = app_with("xx", 10);
3176 a.toggle_mode_by_name("lsp-mode");
3177 a.apply_effect(lattice_grammar::Effect::Lsp(
3178 lattice_grammar::LspRequest::Implementation,
3179 ));
3180 let msg = a.editor.last_message.as_ref().expect("echo");
3181 assert!(msg.text.contains("no LSP server"));
3182 }
3183
3184 #[test]
3185 fn drain_pending_no_implementations_echoes_kind_specific_message() {
3186 let mut a = app_with("xx", 10);
3188 let (tx, rx) =
3189 tokio::sync::mpsc::unbounded_channel::<Vec<lattice_lsp::lsp_types::Location>>();
3190 a.editor.pending_definition_rx = Some(rx);
3191 a.editor.pending_definition_token = Some(lattice_protocol::CancellationToken::new());
3192 a.editor.pending_nav_kind = Some(super::LspNavKind::Implementation);
3193 tx.send(Vec::new()).unwrap();
3194 a.drain_pending_definitions();
3195 let msg = a.editor.last_message.as_ref().expect("echo");
3196 assert!(
3197 msg.text.contains("no implementations"),
3198 "expected implementations echo, got: {}",
3199 msg.text
3200 );
3201 assert!(a.editor.pending_nav_kind.is_none());
3202 }
3203
3204 #[test]
3205 fn drain_pending_no_type_definitions_echoes_kind_specific_message() {
3206 let mut a = app_with("xx", 10);
3207 let (tx, rx) =
3208 tokio::sync::mpsc::unbounded_channel::<Vec<lattice_lsp::lsp_types::Location>>();
3209 a.editor.pending_definition_rx = Some(rx);
3210 a.editor.pending_definition_token = Some(lattice_protocol::CancellationToken::new());
3211 a.editor.pending_nav_kind = Some(super::LspNavKind::TypeDefinition);
3212 tx.send(Vec::new()).unwrap();
3213 a.drain_pending_definitions();
3214 let msg = a.editor.last_message.as_ref().expect("echo");
3215 assert!(msg.text.contains("no type definitions"));
3216 }
3217
3218 #[test]
3219 fn drain_pending_no_declarations_echoes_kind_specific_message() {
3220 let mut a = app_with("xx", 10);
3221 let (tx, rx) =
3222 tokio::sync::mpsc::unbounded_channel::<Vec<lattice_lsp::lsp_types::Location>>();
3223 a.editor.pending_definition_rx = Some(rx);
3224 a.editor.pending_definition_token = Some(lattice_protocol::CancellationToken::new());
3225 a.editor.pending_nav_kind = Some(super::LspNavKind::Declaration);
3226 tx.send(Vec::new()).unwrap();
3227 a.drain_pending_definitions();
3228 let msg = a.editor.last_message.as_ref().expect("echo");
3229 assert!(msg.text.contains("no declarations"));
3230 }
3231
3232 #[test]
3233 fn lsp_references_request_with_no_uri_echoes_no_lsp_attached() {
3234 let mut a = app_with("xx", 10);
3235 a.toggle_mode_by_name("lsp-mode");
3236 a.apply_effect(lattice_grammar::Effect::Lsp(
3237 lattice_grammar::LspRequest::References,
3238 ));
3239 let msg = a.editor.last_message.as_ref().expect("echo");
3240 assert!(msg.text.contains("no LSP server"));
3241 }
3242
3243 #[test]
3244 fn lsp_references_request_pre_cancels_in_flight_token() {
3245 let mut a = app_with("xx", 10);
3246 let stale = lattice_protocol::CancellationToken::new();
3247 a.editor.pending_references_token = Some(stale.clone());
3248 a.apply_effect(lattice_grammar::Effect::Lsp(
3249 lattice_grammar::LspRequest::References,
3250 ));
3251 assert!(stale.is_cancelled());
3252 }
3253
3254 #[test]
3255 fn drain_pending_references_no_servers_outcome_echoes() {
3256 let mut a = app_with("xx", 10);
3257 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::ReferencesOutcome>();
3258 a.editor.pending_references_rx = Some(rx);
3259 a.editor.pending_references_token = Some(lattice_protocol::CancellationToken::new());
3260 tx.send(super::ReferencesOutcome::NoServers).unwrap();
3261 a.drain_pending_references();
3262 let msg = a.editor.last_message.as_ref().expect("echo");
3263 assert!(msg.text.contains("no LSP server"));
3264 assert!(a.editor.pending_references_token.is_none());
3265 }
3266
3267 #[test]
3268 fn drain_pending_references_found_opens_lsp_locations_picker() {
3269 let mut a = app_with("xx", 10);
3270 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::ReferencesOutcome>();
3271 a.editor.pending_references_rx = Some(rx);
3272 a.editor.pending_references_token = Some(lattice_protocol::CancellationToken::new());
3273 tx.send(super::ReferencesOutcome::Found {
3274 symbol: "foo".into(),
3275 locations: vec![loc("/tmp/notarealfile.rs", 3, 5)],
3276 })
3277 .unwrap();
3278 a.drain_pending_references();
3279 let picker = a.editor.picker.as_ref().expect("picker");
3281 assert_eq!(picker.title, "references: foo");
3282 assert!(matches!(
3283 picker.source,
3284 lattice_picker::PickerSource::LspLocations
3285 ));
3286 assert!(matches!(
3287 picker.on_accept,
3288 lattice_picker::PickerAction::JumpToLspLocation
3289 ));
3290 let c = picker.selected_candidate().expect("one row");
3294 let routing = picker.routing_for(c).expect("routing payload set");
3295 let lattice_picker::RoutingPayload::LspLocation { path, line, .. } = routing else {
3296 panic!("expected LspLocation routing, got {routing:?}");
3297 };
3298 let expected = lattice_lsp::actor::uri_to_path(&lattice_lsp::actor::uri_from_path(
3301 std::path::Path::new("/tmp/notarealfile.rs"),
3302 ))
3303 .expect("file uri");
3304 assert_eq!(*path, expected);
3305 assert_eq!(*line, 3);
3306 }
3313
3314 #[test]
3315 fn drain_pending_references_empty_echoes_not_found() {
3316 let mut a = app_with("xx", 10);
3322 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::ReferencesOutcome>();
3323 a.editor.pending_references_rx = Some(rx);
3324 a.editor.pending_references_token = Some(lattice_protocol::CancellationToken::new());
3325 tx.send(super::ReferencesOutcome::Found {
3326 symbol: "missing".into(),
3327 locations: Vec::new(),
3328 })
3329 .unwrap();
3330 a.drain_pending_references();
3331 assert!(a.editor.picker.is_none());
3332 let msg = a.editor.last_message.as_ref().expect("echo");
3333 assert!(msg.text.contains("no references"));
3334 assert!(msg.text.contains("missing"));
3335 }
3336
3337 #[test]
3338 fn flatten_document_symbol_response_flat_preserves_order() {
3339 use lattice_lsp::lsp_types::{Location as LLoc, Position as LPos, Range as LRange};
3340 let path = std::path::PathBuf::from("/tmp/x.rs");
3341 #[allow(deprecated)]
3342 let syms = vec![
3343 lattice_lsp::lsp_types::SymbolInformation {
3344 name: "foo".into(),
3345 kind: lattice_lsp::lsp_types::SymbolKind::FUNCTION,
3346 tags: None,
3347 deprecated: None,
3348 location: LLoc {
3349 uri: super::tests::fake_uri("/tmp/x.rs"),
3350 range: LRange {
3351 start: LPos {
3352 line: 5,
3353 character: 0,
3354 },
3355 end: LPos {
3356 line: 5,
3357 character: 3,
3358 },
3359 },
3360 },
3361 container_name: None,
3362 },
3363 lattice_lsp::lsp_types::SymbolInformation {
3364 name: "bar".into(),
3365 kind: lattice_lsp::lsp_types::SymbolKind::METHOD,
3366 tags: None,
3367 deprecated: None,
3368 location: LLoc {
3369 uri: super::tests::fake_uri("/tmp/x.rs"),
3370 range: LRange {
3371 start: LPos {
3372 line: 10,
3373 character: 4,
3374 },
3375 end: LPos {
3376 line: 10,
3377 character: 7,
3378 },
3379 },
3380 },
3381 container_name: Some("Bag".into()),
3382 },
3383 ];
3384 let resp = lattice_lsp::lsp_types::DocumentSymbolResponse::Flat(syms);
3385 let mut out = Vec::new();
3386 super::flatten_document_symbol_response(resp, &path, &mut out);
3387 assert_eq!(out.len(), 2);
3388 assert_eq!(out[0].name, "foo");
3389 assert_eq!(out[0].depth, 0);
3390 assert_eq!(out[1].name, "bar");
3391 assert_eq!(out[1].container.as_deref(), Some("Bag"));
3392 }
3393
3394 #[test]
3395 fn flatten_document_symbol_response_nested_assigns_depth_via_dfs() {
3396 use lattice_lsp::lsp_types::{DocumentSymbol, Position as LPos, Range as LRange};
3397 let path = std::path::PathBuf::from("/tmp/x.rs");
3398 let inner_range = LRange {
3400 start: LPos {
3401 line: 1,
3402 character: 4,
3403 },
3404 end: LPos {
3405 line: 3,
3406 character: 5,
3407 },
3408 };
3409 let outer_range = LRange {
3410 start: LPos {
3411 line: 0,
3412 character: 0,
3413 },
3414 end: LPos {
3415 line: 4,
3416 character: 0,
3417 },
3418 };
3419 #[allow(deprecated)]
3420 let inner = DocumentSymbol {
3421 name: "bar".into(),
3422 detail: None,
3423 kind: lattice_lsp::lsp_types::SymbolKind::FUNCTION,
3424 tags: None,
3425 deprecated: None,
3426 range: inner_range,
3427 selection_range: inner_range,
3428 children: None,
3429 };
3430 #[allow(deprecated)]
3431 let outer = DocumentSymbol {
3432 name: "foo".into(),
3433 detail: None,
3434 kind: lattice_lsp::lsp_types::SymbolKind::MODULE,
3435 tags: None,
3436 deprecated: None,
3437 range: outer_range,
3438 selection_range: outer_range,
3439 children: Some(vec![inner]),
3440 };
3441 let resp = lattice_lsp::lsp_types::DocumentSymbolResponse::Nested(vec![outer]);
3442 let mut out = Vec::new();
3443 super::flatten_document_symbol_response(resp, &path, &mut out);
3444 assert_eq!(out.len(), 2);
3445 assert_eq!(out[0].name, "foo");
3446 assert_eq!(out[0].depth, 0);
3447 assert_eq!(out[1].name, "bar");
3448 assert_eq!(out[1].depth, 1);
3449 }
3450
3451 #[test]
3452 fn drain_pending_symbols_no_servers_outcome_echoes() {
3453 let mut a = app_with("xx", 10);
3454 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::SymbolsOutcome>();
3455 a.editor.pending_symbols_rx = Some(rx);
3456 a.editor.pending_symbols_token = Some(lattice_protocol::CancellationToken::new());
3457 tx.send(super::SymbolsOutcome::NoServers).unwrap();
3458 a.drain_pending_symbols();
3459 let msg = a.editor.last_message.as_ref().expect("echo");
3460 assert!(msg.text.contains("no LSP server"));
3461 assert!(a.editor.pending_symbols_token.is_none());
3462 }
3463
3464 #[test]
3465 fn drain_pending_symbols_found_opens_picker() {
3466 let mut a = app_with("xx", 10);
3467 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::SymbolsOutcome>();
3468 a.editor.pending_symbols_rx = Some(rx);
3469 a.editor.pending_symbols_token = Some(lattice_protocol::CancellationToken::new());
3470 tx.send(super::SymbolsOutcome::Found {
3471 title: "symbols (2)".into(),
3472 rows: vec![
3473 super::SymbolRow {
3474 name: "foo".into(),
3475 kind_glyph: "ƒ",
3476 container: None,
3477 depth: 0,
3478 path: std::path::PathBuf::from("/tmp/x.rs"),
3479 line: 5,
3480 col: 0,
3481 },
3482 super::SymbolRow {
3483 name: "bar".into(),
3484 kind_glyph: "v",
3485 container: None,
3486 depth: 1,
3487 path: std::path::PathBuf::from("/tmp/x.rs"),
3488 line: 10,
3489 col: 4,
3490 },
3491 ],
3492 })
3493 .unwrap();
3494 a.drain_pending_symbols();
3495 let picker = a.editor.picker.as_ref().expect("picker");
3496 assert_eq!(picker.title, "symbols (2)");
3497 assert_eq!(picker.candidates.len(), 2);
3498 let display = &picker.candidates[1].raw.display;
3500 assert!(display.contains(" v bar"), "got: {display}");
3501 }
3502
3503 #[test]
3504 fn drain_pending_symbols_empty_echoes() {
3505 let mut a = app_with("xx", 10);
3506 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::SymbolsOutcome>();
3507 a.editor.pending_symbols_rx = Some(rx);
3508 a.editor.pending_symbols_token = Some(lattice_protocol::CancellationToken::new());
3509 tx.send(super::SymbolsOutcome::Found {
3510 title: "symbols (0)".into(),
3511 rows: Vec::new(),
3512 })
3513 .unwrap();
3514 a.drain_pending_symbols();
3515 assert!(a.editor.picker.is_none());
3516 let msg = a.editor.last_message.as_ref().expect("echo");
3517 assert!(msg.text.contains("no symbols"));
3518 }
3519
3520 #[test]
3521 fn code_action_kind_glyph_distinct_for_common_kinds() {
3522 use lattice_lsp::lsp_types::CodeActionKind as K;
3523 let qf = super::code_action_kind_glyph(Some(&K::QUICKFIX));
3524 let rf = super::code_action_kind_glyph(Some(&K::REFACTOR));
3525 let sr = super::code_action_kind_glyph(Some(&K::SOURCE));
3526 assert_ne!(qf, rf);
3527 assert_ne!(qf, sr);
3528 assert_ne!(rf, sr);
3529 }
3530
3531 #[test]
3532 fn drain_pending_code_actions_no_provider_echoes() {
3533 let mut a = app_with("xx", 10);
3534 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CodeActionOutcome>();
3535 a.editor.pending_code_action_rx = Some(rx);
3536 a.editor.pending_code_action_token = Some(lattice_protocol::CancellationToken::new());
3537 tx.send(super::CodeActionOutcome::NoProvider).unwrap();
3538 a.drain_pending_code_actions();
3539 let msg = a.editor.last_message.as_ref().expect("echo");
3540 assert!(msg.text.contains("codeActionProvider"));
3541 }
3542
3543 #[test]
3544 fn drain_pending_code_actions_empty_echoes_no_actions() {
3545 let mut a = app_with("xx", 10);
3546 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CodeActionOutcome>();
3547 a.editor.pending_code_action_rx = Some(rx);
3548 a.editor.pending_code_action_token = Some(lattice_protocol::CancellationToken::new());
3549 tx.send(super::CodeActionOutcome::Items(Vec::new()))
3550 .unwrap();
3551 a.drain_pending_code_actions();
3552 assert!(a.editor.picker.is_none());
3553 let msg = a.editor.last_message.as_ref().expect("echo");
3554 assert!(msg.text.contains("no code actions"));
3555 }
3556
3557 #[test]
3558 fn drain_pending_code_actions_items_open_picker() {
3559 let mut a = app_with("foo\n", 10);
3560 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CodeActionOutcome>();
3561 a.editor.pending_code_action_rx = Some(rx);
3562 a.editor.pending_code_action_token = Some(lattice_protocol::CancellationToken::new());
3563 let act = lattice_lsp::lsp_types::CodeAction {
3564 title: "Add `mut` modifier".into(),
3565 kind: Some(lattice_lsp::lsp_types::CodeActionKind::QUICKFIX),
3566 diagnostics: None,
3567 edit: None,
3568 command: None,
3569 is_preferred: None,
3570 disabled: None,
3571 data: None,
3572 };
3573 tx.send(super::CodeActionOutcome::Items(vec![
3574 super::CodeActionRow {
3575 title: act.title.clone(),
3576 kind_glyph: "🛠",
3577 action: lattice_lsp::lsp_types::CodeActionOrCommand::CodeAction(act),
3578 },
3579 ]))
3580 .unwrap();
3581 a.drain_pending_code_actions();
3582 let picker = a.editor.picker.as_ref().expect("picker");
3583 assert!(picker.title.starts_with("code-actions"));
3584 assert!(matches!(
3585 picker.on_accept,
3586 lattice_picker::PickerAction::AcceptLspCodeAction
3587 ));
3588 assert_eq!(picker.candidates.len(), 1);
3589 let display = &picker.candidates[0].raw.display;
3590 assert!(display.contains("🛠 Add `mut` modifier"));
3591 assert!(a.editor.pending_code_action_items.is_some());
3593 }
3594
3595 #[test]
3596 fn flatten_workspace_edit_collects_legacy_changes_map() {
3597 use std::collections::HashMap;
3598 let uri = super::tests::fake_uri("/tmp/x.rs");
3599 let mut changes: HashMap<
3600 lattice_lsp::lsp_types::Uri,
3601 Vec<lattice_lsp::lsp_types::TextEdit>,
3602 > = HashMap::new();
3603 changes.insert(
3604 uri.clone(),
3605 vec![lattice_lsp::lsp_types::TextEdit {
3606 range: lattice_lsp::lsp_types::Range {
3607 start: lattice_lsp::lsp_types::Position {
3608 line: 0,
3609 character: 0,
3610 },
3611 end: lattice_lsp::lsp_types::Position {
3612 line: 0,
3613 character: 3,
3614 },
3615 },
3616 new_text: "bar".into(),
3617 }],
3618 );
3619 let we = lattice_lsp::lsp_types::WorkspaceEdit {
3620 changes: Some(changes),
3621 document_changes: None,
3622 change_annotations: None,
3623 };
3624 let flat = super::flatten_workspace_edit(we);
3625 assert_eq!(flat.len(), 1);
3626 assert_eq!(flat[0].0, uri);
3627 assert_eq!(flat[0].1[0].new_text, "bar");
3628 }
3629
3630 #[test]
3631 fn drain_pending_rename_no_provider_echoes() {
3632 let mut a = app_with("xx", 10);
3633 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::RenameOutcome>();
3634 a.editor.pending_rename_rx = Some(rx);
3635 a.editor.pending_rename_token = Some(lattice_protocol::CancellationToken::new());
3636 tx.send(super::RenameOutcome::NoProvider).unwrap();
3637 a.drain_pending_rename();
3638 let msg = a.editor.last_message.as_ref().expect("echo");
3639 assert!(msg.text.contains("renameProvider"));
3640 }
3641
3642 #[test]
3643 fn drain_pending_rename_not_renameable_echoes_reason() {
3644 let mut a = app_with("xx", 10);
3645 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::RenameOutcome>();
3646 a.editor.pending_rename_rx = Some(rx);
3647 a.editor.pending_rename_token = Some(lattice_protocol::CancellationToken::new());
3648 tx.send(super::RenameOutcome::NotRenameable {
3649 reason: "out of bounds".into(),
3650 })
3651 .unwrap();
3652 a.drain_pending_rename();
3653 let msg = a.editor.last_message.as_ref().expect("echo");
3654 assert_eq!(msg.level, EchoLevel::Error);
3655 assert!(msg.text.contains("out of bounds"));
3656 }
3657
3658 #[test]
3659 fn drain_pending_rename_empty_echoes_no_changes() {
3660 let mut a = app_with("xx", 10);
3661 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::RenameOutcome>();
3662 a.editor.pending_rename_rx = Some(rx);
3663 a.editor.pending_rename_token = Some(lattice_protocol::CancellationToken::new());
3664 tx.send(super::RenameOutcome::Empty).unwrap();
3665 a.drain_pending_rename();
3666 let msg = a.editor.last_message.as_ref().expect("echo");
3667 assert!(msg.text.contains("no changes"));
3668 }
3669
3670 #[test]
3671 fn drain_pending_rename_applies_active_buffer_edits_as_one_undo_unit() {
3672 let path = std::env::temp_dir().join(format!("lattice-rename-{}.rs", std::process::id()));
3676 std::fs::write(&path, "let foo = 1;\nlet x = foo + 2;\n").unwrap();
3677 let doc = Document::open(&path).unwrap();
3678 let mut a = App::new(doc);
3679 a.set_viewport_height(10);
3680 let uri = super::tests::fake_uri(path.to_str().unwrap());
3681 let edits = vec![
3682 lattice_lsp::lsp_types::TextEdit {
3684 range: lattice_lsp::lsp_types::Range {
3685 start: lattice_lsp::lsp_types::Position {
3686 line: 0,
3687 character: 4,
3688 },
3689 end: lattice_lsp::lsp_types::Position {
3690 line: 0,
3691 character: 7,
3692 },
3693 },
3694 new_text: "bar".into(),
3695 },
3696 lattice_lsp::lsp_types::TextEdit {
3698 range: lattice_lsp::lsp_types::Range {
3699 start: lattice_lsp::lsp_types::Position {
3700 line: 1,
3701 character: 8,
3702 },
3703 end: lattice_lsp::lsp_types::Position {
3704 line: 1,
3705 character: 11,
3706 },
3707 },
3708 new_text: "bar".into(),
3709 },
3710 ];
3711 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::RenameOutcome>();
3712 a.editor.pending_rename_rx = Some(rx);
3713 a.editor.pending_rename_token = Some(lattice_protocol::CancellationToken::new());
3714 tx.send(super::RenameOutcome::Edits {
3715 per_file: vec![(uri, edits)],
3716 new_name: "bar".into(),
3717 })
3718 .unwrap();
3719 a.drain_pending_rename();
3720 let body = a.editor.document.snapshot().buffer.as_string();
3721 assert!(body.contains("let bar = 1;"));
3722 assert!(body.contains("let x = bar + 2;"));
3723 let _ = a.undo_blocking();
3726 let restored = a.editor.document.snapshot().buffer.as_string();
3727 assert!(restored.contains("let foo = 1;"));
3728 let _ = std::fs::remove_file(path);
3729 }
3730
3731 #[test]
3732 fn drain_pending_insert_completion_lsp_no_servers_keeps_popup_open_if_sync_had_results() {
3733 let mut a = app_with("alpha alphabet alligator\nal", 10);
3736 a.editor.modal = ModalState::Insert;
3737 a.editor.cursor = Position::new(1, 2);
3738 a.do_completion_trigger();
3739 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::AsyncCompletionOutcome>();
3744 a.editor.pending_insert_completion_async_rx = Some(rx);
3745 a.editor.pending_insert_completion_async_token =
3746 Some(lattice_protocol::CancellationToken::new());
3747 tx.send(super::AsyncCompletionOutcome::Nothing).unwrap();
3748 a.drain_pending_insert_completion_lsp();
3749 assert!(a.editor.insert_completion.is_some());
3751 }
3752
3753 #[test]
3754 fn drain_pending_insert_completion_lsp_items_merge_into_popup() {
3755 let mut a = app_with("\nfo", 10);
3756 a.editor.modal = ModalState::Insert;
3757 a.editor.cursor = Position::new(1, 2);
3758 a.editor.insert_completion = Some(lattice_completion::InsertCompletionState::open(
3763 lattice_completion::CompletionTrigger::Manual,
3764 Position::new(1, 0),
3765 Position::new(1, 2),
3766 "fo".to_string(),
3767 ));
3768 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::AsyncCompletionOutcome>();
3769 a.editor.pending_insert_completion_async_rx = Some(rx);
3770 a.editor.pending_insert_completion_async_token =
3771 Some(lattice_protocol::CancellationToken::new());
3772 tx.send(super::AsyncCompletionOutcome::Items {
3773 sources: vec![lattice_completion::SourceId::new(
3774 lattice_completion::LSP_COMPLETION_SOURCE_ID,
3775 )],
3776 candidates: vec![
3777 lsp_meta_candidate(super::LspCompletionMeta {
3778 label: "foo".into(),
3779 insert_text: "foo".into(),
3780 filter_text: None,
3781 sort_text: None,
3782 detail: Some("fn() -> i32".into()),
3783 documentation: None,
3784 kind: Some(lattice_lsp::lsp_types::CompletionItemKind::FUNCTION),
3785 deprecated: false,
3786 preselect: false,
3787 commit_characters: Vec::new(),
3788 additional_text_edits: Vec::new(),
3789 command: None,
3790 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT,
3791 replace_range: None,
3792 server_id: "test-server".to_string(),
3793 original_item: lattice_lsp::lsp_types::CompletionItem::default(),
3794 resolved: false,
3795 }),
3796 lsp_meta_candidate(super::LspCompletionMeta {
3797 label: "foobar".into(),
3798 insert_text: "foobar".into(),
3799 filter_text: None,
3800 sort_text: None,
3801 detail: None,
3802 documentation: None,
3803 kind: Some(lattice_lsp::lsp_types::CompletionItemKind::VARIABLE),
3804 deprecated: false,
3805 preselect: false,
3806 commit_characters: Vec::new(),
3807 additional_text_edits: Vec::new(),
3808 command: None,
3809 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT,
3810 replace_range: None,
3811 server_id: "test-server".to_string(),
3812 original_item: lattice_lsp::lsp_types::CompletionItem::default(),
3813 resolved: false,
3814 }),
3815 ],
3816 is_incomplete: false,
3817 })
3818 .unwrap();
3819 a.drain_pending_insert_completion_lsp();
3820 let state = a.editor.insert_completion.as_ref().expect("popup open");
3821 let labels: Vec<String> = state
3823 .rendered
3824 .iter()
3825 .map(|c| c.raw.display.clone())
3826 .collect();
3827 assert!(labels.iter().any(|l| l.starts_with("foo")));
3828 assert!(labels.iter().any(|l| l.starts_with("foobar")));
3829 let state = a.editor.insert_completion.as_ref().expect("popup");
3833 let lsp_rows = state
3834 .raw
3835 .iter()
3836 .filter(|r| {
3837 matches!(
3838 r.data,
3839 lattice_completion::CandidateData::Extension {
3840 kind_id: LSP_COMPLETION_KIND_ID,
3841 ..
3842 }
3843 )
3844 })
3845 .count();
3846 assert_eq!(lsp_rows, 2);
3847 }
3848
3849 #[test]
3850 fn drain_pending_insert_completion_lsp_drops_prior_lsp_rows_on_refresh() {
3851 let mut a = app_with("xx", 10);
3854 a.editor.modal = ModalState::Insert;
3855 a.editor.cursor = Position::ZERO;
3856 a.editor.insert_completion = Some(lattice_completion::InsertCompletionState::open(
3857 lattice_completion::CompletionTrigger::Manual,
3858 Position::ZERO,
3859 Position::ZERO,
3860 String::new(),
3861 ));
3862 let mk_item = |label: &str| super::LspCompletionMeta {
3863 label: label.into(),
3864 insert_text: label.into(),
3865 filter_text: None,
3866 sort_text: None,
3867 detail: None,
3868 documentation: None,
3869 kind: None,
3870 deprecated: false,
3871 preselect: false,
3872 commit_characters: Vec::new(),
3873 additional_text_edits: Vec::new(),
3874 command: None,
3875 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT,
3876 replace_range: None,
3877 server_id: "test-server".to_string(),
3878 original_item: lattice_lsp::lsp_types::CompletionItem::default(),
3879 resolved: false,
3880 };
3881 let (tx1, rx1) = tokio::sync::mpsc::unbounded_channel::<super::AsyncCompletionOutcome>();
3883 a.editor.pending_insert_completion_async_rx = Some(rx1);
3884 a.editor.pending_insert_completion_async_token =
3885 Some(lattice_protocol::CancellationToken::new());
3886 tx1.send(super::AsyncCompletionOutcome::Items {
3887 sources: vec![lattice_completion::SourceId::new(
3888 lattice_completion::LSP_COMPLETION_SOURCE_ID,
3889 )],
3890 candidates: vec![
3891 lsp_meta_candidate(mk_item("alpha")),
3892 lsp_meta_candidate(mk_item("alphabet")),
3893 ],
3894 is_incomplete: false,
3895 })
3896 .unwrap();
3897 a.drain_pending_insert_completion_lsp();
3898 let pre = a
3899 .editor
3900 .insert_completion
3901 .as_ref()
3902 .map(|s| s.raw.len())
3903 .unwrap_or(0);
3904 assert_eq!(pre, 2);
3905 let (tx2, rx2) = tokio::sync::mpsc::unbounded_channel::<super::AsyncCompletionOutcome>();
3908 a.editor.pending_insert_completion_async_rx = Some(rx2);
3909 a.editor.pending_insert_completion_async_token =
3910 Some(lattice_protocol::CancellationToken::new());
3911 tx2.send(super::AsyncCompletionOutcome::Items {
3912 sources: vec![lattice_completion::SourceId::new(
3913 lattice_completion::LSP_COMPLETION_SOURCE_ID,
3914 )],
3915 candidates: vec![lsp_meta_candidate(mk_item("beta"))],
3916 is_incomplete: false,
3917 })
3918 .unwrap();
3919 a.drain_pending_insert_completion_lsp();
3920 let state = a.editor.insert_completion.as_ref().expect("popup");
3921 let lsp_rows: Vec<_> = state
3922 .raw
3923 .iter()
3924 .filter_map(|r| match &r.data {
3925 lattice_completion::CandidateData::Extension {
3926 kind_id: LSP_COMPLETION_KIND_ID,
3927 payload,
3928 } => lattice_lsp::completion::decode_meta(payload),
3929 _ => None,
3930 })
3931 .collect();
3932 assert_eq!(lsp_rows.len(), 1);
3933 assert_eq!(lsp_rows[0].label, "beta");
3934 }
3935
3936 #[test]
3937 fn lsp_completion_meta_for_returns_none_for_sync_sourced_candidates() {
3938 let a = app_with("xx", 10);
3939 let raw = lattice_completion::RawCandidate::plain(
3940 "foo",
3941 lattice_completion::CandidateKind::Plain,
3942 );
3943 let scored = lattice_completion::ScoredCandidate {
3944 raw,
3945 score: lattice_completion::MatchScore(100),
3946 match_ranges: Vec::new(),
3947 };
3948 let rendered = lattice_completion::RenderedCandidate::from_scored(scored);
3949 assert!(a.lsp_completion_meta_for(&rendered).is_none());
3950 }
3951
3952 #[test]
3953 fn drain_pending_completion_resolve_fills_metadata_and_body() {
3954 let mut a = app_with("xx", 10);
3955 a.editor.modal = ModalState::Insert;
3956 a.editor.cursor = Position::ZERO;
3957 let mut state = lattice_completion::InsertCompletionState::open(
3959 lattice_completion::CompletionTrigger::Manual,
3960 Position::ZERO,
3961 Position::ZERO,
3962 String::new(),
3963 );
3964 let meta = super::LspCompletionMeta {
3965 label: "foo".into(),
3966 insert_text: "foo".into(),
3967 filter_text: None,
3968 sort_text: None,
3969 detail: None,
3970 documentation: None,
3971 kind: None,
3972 deprecated: false,
3973 preselect: false,
3974 commit_characters: Vec::new(),
3975 additional_text_edits: Vec::new(),
3976 command: None,
3977 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT,
3978 replace_range: None,
3979 server_id: "test-server".to_string(),
3980 original_item: lattice_lsp::lsp_types::CompletionItem::default(),
3981 resolved: false,
3982 };
3983 let mut raw = lattice_completion::RawCandidate::plain(
3984 "foo",
3985 lattice_completion::CandidateKind::Plain,
3986 );
3987 raw.data = lattice_completion::CandidateData::Extension {
3988 kind_id: super::LSP_COMPLETION_KIND_ID,
3989 payload: lattice_lsp::completion::encode_meta(&meta),
3990 };
3991 state.raw.push(raw.clone());
3992 state
3993 .rendered
3994 .push(lattice_completion::RenderedCandidate::from_scored(
3995 lattice_completion::ScoredCandidate {
3996 raw,
3997 score: lattice_completion::MatchScore(100),
3998 match_ranges: Vec::new(),
3999 },
4000 ));
4001 state.doc_popup = Some(lattice_completion::DocPopupState {
4004 for_index: 0,
4005 body: None,
4006 scroll: 5, });
4008 let _ = meta;
4010 a.editor.insert_completion = Some(state);
4011 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CompletionResolveOutcome>();
4013 a.editor.pending_completion_resolve_rx = Some(rx);
4014 a.editor.pending_completion_resolve_token =
4015 Some(lattice_protocol::CancellationToken::new());
4016 let mut resolved = lattice_lsp::lsp_types::CompletionItem {
4017 label: "foo".into(),
4018 detail: Some("fn foo() -> i32".into()),
4019 ..Default::default()
4020 };
4021 resolved.documentation = Some(lattice_lsp::lsp_types::Documentation::String(
4022 "Returns 42.".into(),
4023 ));
4024 tx.send(super::CompletionResolveOutcome {
4025 meta_index: 0,
4026 resolved,
4027 })
4028 .unwrap();
4029 a.drain_pending_completion_resolve();
4030 let state = a.editor.insert_completion.as_ref().expect("popup");
4033 let payload = match &state.raw[0].data {
4034 lattice_completion::CandidateData::Extension { payload, .. } => payload.clone(),
4035 _ => panic!("expected Extension payload"),
4036 };
4037 let updated = lattice_lsp::completion::decode_meta(&payload).expect("decode");
4038 assert!(updated.resolved);
4039 assert_eq!(updated.detail.as_deref(), Some("fn foo() -> i32"));
4040 assert_eq!(updated.documentation.as_deref(), Some("Returns 42."));
4041 let popup = state.doc_popup.as_ref().expect("popup");
4043 assert_eq!(popup.scroll, 0);
4044 let body = popup.body.as_deref().unwrap_or("");
4045 assert!(body.contains("fn foo() -> i32"));
4046 assert!(body.contains("Returns 42."));
4047 }
4048
4049 #[test]
4050 fn drain_pending_completion_resolve_drops_stale_index_after_selection_moved() {
4051 let mut a = app_with("xx", 10);
4056 let mut state = lattice_completion::InsertCompletionState::open(
4057 lattice_completion::CompletionTrigger::Manual,
4058 Position::ZERO,
4059 Position::ZERO,
4060 String::new(),
4061 );
4062 let mk_meta = |label: &str| super::LspCompletionMeta {
4063 label: label.into(),
4064 insert_text: label.into(),
4065 filter_text: None,
4066 sort_text: None,
4067 detail: None,
4068 documentation: None,
4069 kind: None,
4070 deprecated: false,
4071 preselect: false,
4072 commit_characters: Vec::new(),
4073 additional_text_edits: Vec::new(),
4074 command: None,
4075 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT,
4076 replace_range: None,
4077 server_id: "test-server".to_string(),
4078 original_item: {
4079 let ci = lattice_lsp::lsp_types::CompletionItem {
4080 label: label.into(),
4081 ..Default::default()
4082 };
4083 ci
4084 },
4085 resolved: false,
4086 };
4087 for label in ["c0", "c1"] {
4088 let meta = mk_meta(label);
4089 let mut raw = lattice_completion::RawCandidate::plain(
4090 label,
4091 lattice_completion::CandidateKind::Plain,
4092 );
4093 raw.data = lattice_completion::CandidateData::Extension {
4094 kind_id: super::LSP_COMPLETION_KIND_ID,
4095 payload: lattice_lsp::completion::encode_meta(&meta),
4096 };
4097 state.raw.push(raw.clone());
4098 state
4099 .rendered
4100 .push(lattice_completion::RenderedCandidate::from_scored(
4101 lattice_completion::ScoredCandidate {
4102 raw,
4103 score: lattice_completion::MatchScore(100),
4104 match_ranges: Vec::new(),
4105 },
4106 ));
4107 }
4108 state.selected = 1; state.doc_popup = Some(lattice_completion::DocPopupState {
4110 for_index: 1,
4111 body: Some("for c1".into()),
4112 scroll: 0,
4113 });
4114 a.editor.insert_completion = Some(state);
4115 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CompletionResolveOutcome>();
4116 a.editor.pending_completion_resolve_rx = Some(rx);
4117 a.editor.pending_completion_resolve_token =
4118 Some(lattice_protocol::CancellationToken::new());
4119 let mut resolved = lattice_lsp::lsp_types::CompletionItem {
4120 label: "c0".into(),
4121 ..Default::default()
4122 };
4123 resolved.documentation = Some(lattice_lsp::lsp_types::Documentation::String(
4124 "stale".into(),
4125 ));
4126 tx.send(super::CompletionResolveOutcome {
4127 meta_index: 0,
4128 resolved,
4129 })
4130 .unwrap();
4131 a.drain_pending_completion_resolve();
4132 let state = a.editor.insert_completion.as_ref().expect("popup");
4134 let c0_payload = match &state.raw[0].data {
4135 lattice_completion::CandidateData::Extension { payload, .. } => payload.clone(),
4136 _ => panic!("expected Extension"),
4137 };
4138 let c0_meta = lattice_lsp::completion::decode_meta(&c0_payload).expect("decode");
4139 assert!(c0_meta.resolved);
4140 assert_eq!(c0_meta.documentation.as_deref(), Some("stale"));
4141 let body = state.doc_popup.as_ref().and_then(|d| d.body.clone());
4143 assert_eq!(body.as_deref(), Some("for c1"));
4144 }
4145
4146 #[test]
4147 fn lsp_completion_meta_for_decodes_payload() {
4148 let a = app_with("xx", 10);
4152 let meta = super::LspCompletionMeta {
4153 label: "second".into(),
4154 insert_text: "second".into(),
4155 filter_text: None,
4156 sort_text: None,
4157 detail: None,
4158 documentation: None,
4159 kind: None,
4160 deprecated: false,
4161 preselect: false,
4162 commit_characters: Vec::new(),
4163 additional_text_edits: Vec::new(),
4164 command: None,
4165 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT,
4166 replace_range: None,
4167 server_id: "test-server".to_string(),
4168 original_item: lattice_lsp::lsp_types::CompletionItem::default(),
4169 resolved: false,
4170 };
4171 let mut raw = lattice_completion::RawCandidate::plain(
4172 "second",
4173 lattice_completion::CandidateKind::Plain,
4174 );
4175 raw.data = lattice_completion::CandidateData::Extension {
4176 kind_id: super::LSP_COMPLETION_KIND_ID,
4177 payload: lattice_lsp::completion::encode_meta(&meta),
4178 };
4179 let scored = lattice_completion::ScoredCandidate {
4180 raw,
4181 score: lattice_completion::MatchScore(100),
4182 match_ranges: Vec::new(),
4183 };
4184 let rendered = lattice_completion::RenderedCandidate::from_scored(scored);
4185 let decoded = a.lsp_completion_meta_for(&rendered).expect("meta resolves");
4186 assert_eq!(decoded.label, "second");
4187 }
4188
4189 #[test]
4190 fn drain_pending_completion_no_servers_echoes() {
4191 let mut a = app_with("xx", 10);
4192 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CompletionOutcome>();
4193 a.editor.pending_completion_rx = Some(rx);
4194 a.editor.pending_completion_token = Some(lattice_protocol::CancellationToken::new());
4195 tx.send(super::CompletionOutcome::NoServers).unwrap();
4196 a.drain_pending_completion();
4197 let msg = a.editor.last_message.as_ref().expect("echo");
4198 assert!(msg.text.contains("no LSP server"));
4199 }
4200
4201 #[test]
4202 fn drain_pending_completion_items_open_picker_with_indexed_text() {
4203 let mut a = app_with("foo\n", 10);
4204 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CompletionOutcome>();
4205 a.editor.pending_completion_rx = Some(rx);
4206 a.editor.pending_completion_token = Some(lattice_protocol::CancellationToken::new());
4207 tx.send(super::CompletionOutcome::Items(vec![
4208 super::CompletionItemRow {
4209 label: "foo_bar".into(),
4210 kind_glyph: "ƒ",
4211 detail: Some("fn foo_bar()".into()),
4212 insert_text: "foo_bar()".into(),
4213 replace_range: (0, 3),
4214 line: 0,
4215 },
4216 ]))
4217 .unwrap();
4218 a.drain_pending_completion();
4219 let picker = a.editor.picker.as_ref().expect("picker");
4220 assert!(picker.title.starts_with("complete"));
4221 assert!(matches!(
4222 picker.on_accept,
4223 lattice_picker::PickerAction::AcceptLspCompletion
4224 ));
4225 assert_eq!(picker.candidates.len(), 1);
4226 let display = &picker.candidates[0].raw.display;
4228 assert!(display.contains("ƒ foo_bar"));
4229 assert!(display.contains("fn foo_bar()"));
4230 let routing = picker
4234 .routing_for(&picker.candidates[0])
4235 .expect("routing payload set");
4236 match routing {
4237 lattice_picker::RoutingPayload::LspCompletion { index } => {
4238 assert_eq!(*index, 0);
4239 }
4240 other => panic!("expected LspCompletion routing, got {other:?}"),
4241 }
4242 assert!(a.editor.pending_completion_items.is_some());
4244 }
4245
4246 #[test]
4247 fn drain_pending_completion_empty_echoes_no_completions() {
4248 let mut a = app_with("xx", 10);
4249 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::CompletionOutcome>();
4250 a.editor.pending_completion_rx = Some(rx);
4251 a.editor.pending_completion_token = Some(lattice_protocol::CancellationToken::new());
4252 tx.send(super::CompletionOutcome::Items(Vec::new()))
4253 .unwrap();
4254 a.drain_pending_completion();
4255 assert!(a.editor.picker.is_none());
4256 let msg = a.editor.last_message.as_ref().expect("echo");
4257 assert!(msg.text.contains("no completions"));
4258 }
4259
4260 #[test]
4261 fn signature_help_to_markdown_renders_active_signature() {
4262 let sh = lattice_lsp::lsp_types::SignatureHelp {
4263 signatures: vec![lattice_lsp::lsp_types::SignatureInformation {
4264 label: "fn foo(a: i32, b: &str) -> i32".into(),
4265 documentation: Some(lattice_lsp::lsp_types::Documentation::String(
4266 "Adds.".into(),
4267 )),
4268 parameters: Some(vec![
4269 lattice_lsp::lsp_types::ParameterInformation {
4270 label: lattice_lsp::lsp_types::ParameterLabel::Simple("a: i32".into()),
4271 documentation: Some(lattice_lsp::lsp_types::Documentation::String(
4272 "the first.".into(),
4273 )),
4274 },
4275 lattice_lsp::lsp_types::ParameterInformation {
4276 label: lattice_lsp::lsp_types::ParameterLabel::Simple("b: &str".into()),
4277 documentation: None,
4278 },
4279 ]),
4280 active_parameter: Some(0),
4281 }],
4282 active_signature: Some(0),
4283 active_parameter: None,
4284 };
4285 let body = super::signature_help_to_markdown(&sh);
4286 assert!(body.contains("fn foo(a: i32"));
4287 assert!(body.contains("**param:** `a: i32`"));
4288 assert!(body.contains("the first."));
4289 assert!(body.contains("Adds."));
4290 }
4291
4292 #[test]
4293 fn signature_help_to_markdown_empty_when_no_signatures() {
4294 let sh = lattice_lsp::lsp_types::SignatureHelp {
4295 signatures: vec![],
4296 active_signature: None,
4297 active_parameter: None,
4298 };
4299 assert_eq!(super::signature_help_to_markdown(&sh), "");
4300 }
4301
4302 #[test]
4303 fn drain_pending_signature_help_body_opens_popup() {
4304 let mut a = app_with("xx", 10);
4305 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::SignatureHelpOutcome>();
4306 a.editor.pending_signature_help_rx = Some(rx);
4307 a.editor.pending_signature_help_token = Some(lattice_protocol::CancellationToken::new());
4308 tx.send(super::SignatureHelpOutcome::Body(
4309 "```text\nfn x()\n```\n".into(),
4310 ))
4311 .unwrap();
4312 a.drain_pending_signature_help();
4313 let h = a.popup_help().expect("popup");
4314 assert_eq!(h.title, "hover");
4315 assert!(a.editor.pending_signature_help_token.is_none());
4316 }
4317
4318 #[test]
4319 fn drain_pending_signature_help_empty_body_echoes_no_signature_info() {
4320 let mut a = app_with("xx", 10);
4321 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::SignatureHelpOutcome>();
4322 a.editor.pending_signature_help_rx = Some(rx);
4323 a.editor.pending_signature_help_token = Some(lattice_protocol::CancellationToken::new());
4324 tx.send(super::SignatureHelpOutcome::Body(String::new()))
4325 .unwrap();
4326 a.drain_pending_signature_help();
4327 let msg = a.editor.last_message.as_ref().expect("echo");
4328 assert!(msg.text.contains("no signature info"));
4329 assert!(a.editor.popup_buffer.is_none());
4330 }
4331
4332 #[test]
4333 fn nav_request_captures_tag_origin_for_picker_consumption() {
4334 let mut a = app_with("foo bar\nbaz\n", 10);
4354 a.toggle_mode_by_name("lsp-mode");
4357 a.editor.cursor = Position::new(0, 1);
4358 use std::str::FromStr;
4361 a.editor.buffer_uris.insert(
4362 a.editor.document_buffer_id,
4363 lattice_lsp::Uri::from_str("file:///tmp/x.rs").unwrap(),
4364 );
4365 a.editor
4366 .lsp_nav_request(lattice_lsp::cache::LspNavKind::Definition);
4367 let origin = a.editor.pending_tag_origin.as_ref().expect("origin set");
4368 assert_eq!(origin.position, Position::new(0, 1));
4369 assert_eq!(origin.label, "foo");
4370 }
4371
4372 #[test]
4373 fn lsp_nav_request_pre_cancels_prior_token_regardless_of_kind() {
4374 let mut a = app_with("xx", 10);
4377 let stale = lattice_protocol::CancellationToken::new();
4378 a.editor.pending_definition_token = Some(stale.clone());
4379 a.apply_effect(lattice_grammar::Effect::Lsp(
4380 lattice_grammar::LspRequest::Implementation,
4381 ));
4382 assert!(stale.is_cancelled());
4383 }
4384
4385 #[test]
4386 fn lsp_definition_request_pre_cancels_in_flight_token() {
4387 let mut a = app_with("xx", 10);
4388 let stale = lattice_protocol::CancellationToken::new();
4389 a.editor.pending_definition_token = Some(stale.clone());
4390 a.apply_effect(lattice_grammar::Effect::Lsp(
4391 lattice_grammar::LspRequest::Definition,
4392 ));
4393 assert!(stale.is_cancelled());
4394 }
4395
4396 #[test]
4397 fn drain_pending_definitions_with_no_results_echoes_not_found() {
4398 let mut a = app_with("xx", 10);
4399 let (tx, rx) =
4400 tokio::sync::mpsc::unbounded_channel::<Vec<lattice_lsp::lsp_types::Location>>();
4401 a.editor.pending_definition_rx = Some(rx);
4402 a.editor.pending_definition_token = Some(lattice_protocol::CancellationToken::new());
4403 tx.send(Vec::new()).unwrap();
4404 a.drain_pending_definitions();
4405 let msg = a.editor.last_message.as_ref().expect("echo");
4406 assert!(msg.text.contains("no definitions"));
4407 assert!(a.editor.pending_definition_token.is_none());
4408 }
4409
4410 #[test]
4411 fn drain_pending_definitions_with_single_same_buffer_jumps_in_place() {
4412 let path = std::env::temp_dir().join(format!("lattice-defjump-{}.rs", std::process::id()));
4415 std::fs::write(&path, "first line\nsecond line\nthird line\n").unwrap();
4416 let doc = Document::open(&path).unwrap();
4417 let mut a = App::new(doc);
4418 a.set_viewport_height(10);
4419 let (tx, rx) =
4423 tokio::sync::mpsc::unbounded_channel::<Vec<lattice_lsp::lsp_types::Location>>();
4424 a.editor.pending_definition_rx = Some(rx);
4425 a.editor.pending_definition_token = Some(lattice_protocol::CancellationToken::new());
4426 let target = lattice_lsp::lsp_types::Location {
4427 uri: super::tests::fake_uri(path.to_str().unwrap()),
4428 range: lattice_lsp::lsp_types::Range {
4429 start: lattice_lsp::lsp_types::Position {
4430 line: 2,
4431 character: 5,
4432 },
4433 end: lattice_lsp::lsp_types::Position {
4434 line: 2,
4435 character: 6,
4436 },
4437 },
4438 };
4439 tx.send(vec![target]).unwrap();
4440 a.drain_pending_definitions();
4441 assert_eq!(a.editor.cursor.line, 2);
4443 assert_eq!(a.editor.cursor.byte, 5);
4444 let pushed = a
4446 .editor
4447 .position_history
4448 .iter()
4449 .any(|e| e.source == PositionSource::PluginPush && e.position == Position::ZERO);
4450 assert!(pushed, "expected PluginPush entry for pre-jump cursor");
4451 let _ = std::fs::remove_file(path);
4452 }
4453
4454 #[test]
4455 fn drain_pending_definitions_with_multiple_opens_picker() {
4456 let path = std::env::temp_dir().join(format!("lattice-defmulti-{}.rs", std::process::id()));
4461 std::fs::write(&path, "alpha\nbeta\ngamma\n").unwrap();
4462 let doc = Document::open(&path).unwrap();
4463 let mut a = App::new(doc);
4464 a.set_viewport_height(10);
4465 let (tx, rx) =
4466 tokio::sync::mpsc::unbounded_channel::<Vec<lattice_lsp::lsp_types::Location>>();
4467 a.editor.pending_definition_rx = Some(rx);
4468 a.editor.pending_definition_token = Some(lattice_protocol::CancellationToken::new());
4469 a.editor.pending_nav_kind = Some(super::LspNavKind::Definition);
4470 let target_path = path.to_str().unwrap();
4471 tx.send(vec![
4472 super::tests::loc(target_path, 1, 0),
4473 super::tests::loc(target_path, 2, 0),
4474 ])
4475 .unwrap();
4476 a.drain_pending_definitions();
4477 let picker = a.editor.picker.as_ref().expect("multi-result opens picker");
4478 assert_eq!(picker.title, "lsp:definitions");
4479 assert_eq!(picker.candidates.len(), 2);
4480 assert!(matches!(
4481 picker.on_accept,
4482 lattice_picker::PickerAction::JumpToLspLocation
4483 ));
4484 assert_eq!(a.editor.cursor.line, 0);
4486 let _ = std::fs::remove_file(path);
4487 }
4488
4489 #[test]
4490 fn lsp_supervisor_constructed_with_builtin_configs() {
4491 let app = App::new(Document::from_text(""));
4492 assert!(
4495 app.editor.lsp.configs().len() >= 6,
4496 "expected at least 6 builtin server configs"
4497 );
4498 assert_eq!(app.editor.lsp.running_actor_count(), 0);
4500 assert_eq!(app.editor.lsp.attached_buffer_count(), 0);
4501 assert!(app.editor.buffer_uris.is_empty());
4502 }
4503
4504 #[test]
4505 fn lsp_close_buffer_removes_uri_mapping_for_unattached_buffer() {
4506 let mut app = App::new(Document::from_text(""));
4507 let fake_uri =
4510 <lattice_lsp::Uri as std::str::FromStr>::from_str("file:///tmp/x.rs").unwrap();
4511 app.editor
4512 .buffer_uris
4513 .insert(app.editor.document_buffer_id, fake_uri);
4514 app.editor.publish_render_state();
4517 assert!(app.buffer_uri(app.editor.document_buffer_id).is_some());
4518
4519 app.lsp_close_buffer(app.editor.document_buffer_id);
4520 app.editor.publish_render_state();
4524 assert!(app.buffer_uri(app.editor.document_buffer_id).is_none());
4525 }
4526
4527 #[test]
4528 fn lsp_close_buffer_is_noop_for_unmapped_id() {
4529 let mut app = App::new(Document::from_text(""));
4530 app.lsp_close_buffer(app.editor.document_buffer_id);
4532 assert!(app.editor.buffer_uris.is_empty());
4533 }
4534
4535 #[test]
4536 fn lsp_log_no_arg_activates_subsystem_buffer_even_with_no_running_servers() {
4537 let mut app = app_with("hi\n", 5);
4548 let lsp_buf = app.editor.buffers.by_name("*lsp*").expect("*lsp* at boot");
4549 let initial = app.active_pane_buffer_id();
4550 assert_ne!(initial, lsp_buf);
4551 app.do_open_lsp_log(None);
4552 assert!(app.editor.picker.is_none(), "no picker on no-arg :lsp-log");
4553 assert_eq!(
4554 app.active_pane_buffer_id(),
4555 lsp_buf,
4556 "active pane must switch to *lsp*"
4557 );
4558 }
4559
4560 #[test]
4561 fn lsp_log_with_arg_no_match_echoes_message() {
4562 let mut app = app_with("hi\n", 5);
4563 app.do_open_lsp_log(Some("rust"));
4564 let msg = app.editor.last_message.as_ref().unwrap();
4565 assert!(msg.text.contains("no LSP server"));
4566 }
4567
4568 #[tokio::test(flavor = "multi_thread")]
4569 async fn lsp_log_buffer_refreshes_live_when_record_appended() {
4570 let mut app = app_with("hi\n", 5);
4574 let instance = lattice_lsp::InstanceKey::new(
4575 std::sync::Arc::<str>::from("rust"),
4576 std::sync::Arc::<std::path::Path>::from(
4577 std::env::current_dir()
4578 .unwrap_or_else(|_| std::path::PathBuf::from("/"))
4579 .as_path(),
4580 ),
4581 );
4582 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4583 app.open_lsp_log_in_pane("rust");
4584 let log_id = app
4585 .editor
4586 .buffers
4587 .by_name(&lattice_lsp::lsp_server_log_name(&instance))
4588 .expect("per-instance log buffer registered");
4589 let body_before = app.editor.buffers.document_handle(log_id).unwrap().text();
4590 assert!(!body_before.contains("fresh-after-open"));
4591 app.editor.lsp_logger.log(
4592 Some(&instance),
4593 lattice_lsp::LogLevel::Info,
4594 lattice_lsp::LogSource::Client,
4595 "fresh-after-open",
4596 );
4597 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4599 let body_after = app.editor.buffers.document_handle(log_id).unwrap().text();
4600 assert!(
4601 body_after.contains("fresh-after-open"),
4602 "expected new record visible after drain, got body:\n{body_after}"
4603 );
4604 }
4605
4606 #[test]
4612 fn lsp_log_drain_surfaces_show_message_to_minibuffer() {
4613 let mut app = app_with("hi\n", 5);
4614 let instance = lattice_lsp::InstanceKey::new(
4615 std::sync::Arc::<str>::from("rust"),
4616 std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp/test-ws")),
4617 );
4618 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4619 app.editor.lsp_logger.log(
4620 Some(&instance),
4621 lattice_lsp::LogLevel::Warn,
4622 lattice_lsp::LogSource::LspShowMessage,
4623 "indexing complete",
4624 );
4625 app.drain_lsp_log_events();
4626 let msg = app.editor.last_message.as_ref().expect("set_message fired");
4627 assert_eq!(msg.level, EchoLevel::Warn);
4628 assert!(msg.text.contains("indexing complete"), "got `{}`", msg.text);
4629 assert!(msg.text.contains("[rust]"), "got `{}`", msg.text);
4632 }
4633
4634 #[test]
4639 fn lsp_log_drain_does_not_surface_log_message_to_minibuffer() {
4640 let mut app = app_with("hi\n", 5);
4641 let instance = lattice_lsp::InstanceKey::new(
4642 std::sync::Arc::<str>::from("rust"),
4643 std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp/test-ws")),
4644 );
4645 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4646 let before = app.editor.last_message.clone();
4648 app.editor.lsp_logger.log(
4649 Some(&instance),
4650 lattice_lsp::LogLevel::Info,
4651 lattice_lsp::LogSource::LspMessage,
4652 "internal log thing",
4653 );
4654 app.drain_lsp_log_events();
4655 assert_eq!(
4656 app.editor.last_message, before,
4657 "logMessage should NOT touch the echo area"
4658 );
4659 }
4660
4661 #[test]
4665 fn lsp_log_level_typed_option_seeds_boot_level() {
4666 let app = app_with("hi\n", 5);
4667 let instance = lattice_lsp::InstanceKey::new(
4670 std::sync::Arc::<str>::from("rust"),
4671 std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp/test-ws")),
4672 );
4673 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4674 app.editor.lsp_logger.log(
4675 Some(&instance),
4676 lattice_lsp::LogLevel::Debug,
4677 lattice_lsp::LogSource::Client,
4678 "should-be-filtered",
4679 );
4680 let records = app.editor.lsp_logger.snapshot_instance(&instance);
4681 assert!(
4682 !records
4683 .iter()
4684 .any(|r| r.message.contains("should-be-filtered")),
4685 "Debug record should be filtered by the default Info level seed"
4686 );
4687 app.editor
4690 .config
4691 .parse_and_set_command("lsp.log_level=debug")
4692 .unwrap();
4693 app.editor
4695 .lsp_logger
4696 .set_default_level(lattice_lsp::LogLevel::Debug);
4697 app.editor.lsp_logger.log(
4698 Some(&instance),
4699 lattice_lsp::LogLevel::Debug,
4700 lattice_lsp::LogSource::Client,
4701 "should-pass-now",
4702 );
4703 let records = app.editor.lsp_logger.snapshot_instance(&instance);
4704 assert!(
4705 records
4706 .iter()
4707 .any(|r| r.message.contains("should-pass-now")),
4708 "Debug record should land after raising the level"
4709 );
4710 }
4711
4712 #[test]
4717 fn lsp_log_drain_telemetry_uses_distinct_source_tag() {
4718 assert_eq!(lattice_lsp::LogSource::Telemetry.tag(), "telemetry");
4723 }
4724
4725 #[test]
4726 fn lsp_log_drain_is_noop_when_no_log_buffer_open() {
4727 let mut app = app_with("hi\n", 5);
4731 let instance = lattice_lsp::InstanceKey::new(
4732 std::sync::Arc::<str>::from("rust"),
4733 std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp/test-ws")),
4734 );
4735 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4736 app.editor.lsp_logger.log(
4737 Some(&instance),
4738 lattice_lsp::LogLevel::Info,
4739 lattice_lsp::LogSource::Client,
4740 "no-target",
4741 );
4742 app.drain_lsp_log_events();
4743 assert!(app.editor.buffers.help_with_title("lsp:rust").is_none());
4745 assert!(app.editor.buffers.help_with_title("lsp").is_none());
4746 }
4747
4748 #[tokio::test(flavor = "multi_thread")]
4749 async fn lsp_trace_buffer_refreshes_live_when_trace_record_appended() {
4750 let mut app = app_with("hi\n", 5);
4754 let instance = lattice_lsp::InstanceKey::new(
4755 std::sync::Arc::<str>::from("rust"),
4756 std::sync::Arc::<std::path::Path>::from(
4757 std::env::current_dir()
4758 .unwrap_or_else(|_| std::path::PathBuf::from("/"))
4759 .as_path(),
4760 ),
4761 );
4762 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4763 app.editor.lsp_logger.enable_trace(instance.clone());
4764 app.open_lsp_trace_log_in_pane("rust");
4765 let trace_id = app
4766 .editor
4767 .buffers
4768 .by_name(&lattice_lsp::lsp_server_trace_log_name(&instance))
4769 .expect("trace buffer registered");
4770 let before = app.editor.buffers.document_handle(trace_id).unwrap().text();
4771 assert!(!before.contains("→ NEW"));
4772 app.editor.lsp_logger.log(
4773 Some(&instance),
4774 lattice_lsp::LogLevel::Trace,
4775 lattice_lsp::LogSource::Trace,
4776 "→ NEW request id=42",
4777 );
4778 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4779 let after = app.editor.buffers.document_handle(trace_id).unwrap().text();
4780 assert!(after.contains("→ NEW"));
4781 }
4782
4783 #[tokio::test(flavor = "multi_thread")]
4784 async fn lsp_log_burst_coalesces_into_one_refresh() {
4785 let mut app = app_with("hi\n", 5);
4788 let instance = lattice_lsp::InstanceKey::new(
4789 std::sync::Arc::<str>::from("rust"),
4790 std::sync::Arc::<std::path::Path>::from(
4791 std::env::current_dir()
4792 .unwrap_or_else(|_| std::path::PathBuf::from("/"))
4793 .as_path(),
4794 ),
4795 );
4796 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4797 app.open_lsp_log_in_pane("rust");
4798 for i in 0..50 {
4799 app.editor.lsp_logger.log(
4800 Some(&instance),
4801 lattice_lsp::LogLevel::Info,
4802 lattice_lsp::LogSource::Client,
4803 format!("msg-{i}"),
4804 );
4805 }
4806 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
4807 let log_id = app
4808 .editor
4809 .buffers
4810 .by_name(&lattice_lsp::lsp_server_log_name(&instance))
4811 .expect("per-instance log buffer registered");
4812 let body = app.editor.buffers.document_handle(log_id).unwrap().text();
4813 assert!(body.contains("msg-0"));
4815 assert!(body.contains("msg-49"));
4816 }
4817
4818 #[test]
4819 fn lsp_trace_toggle_flips_state_without_opening_buffer() {
4820 let mut app = app_with("hi\n", 5);
4821 let instance = lattice_lsp::InstanceKey::new(
4824 std::sync::Arc::<str>::from("rust"),
4825 std::sync::Arc::<std::path::Path>::from(
4826 std::env::current_dir()
4827 .unwrap_or_else(|_| std::path::PathBuf::from("/"))
4828 .as_path(),
4829 ),
4830 );
4831 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4832 app.do_toggle_lsp_trace("rust");
4834 assert!(app.editor.lsp_logger.is_tracing(&instance));
4835 assert!(app.editor.popup_buffer.is_none());
4838 let msg = app.editor.last_message.as_ref().unwrap();
4839 assert!(msg.text.contains("on"));
4840 assert!(msg.text.contains(":lsp-trace-log"));
4841 app.do_toggle_lsp_trace("rust");
4843 assert!(!app.editor.lsp_logger.is_tracing(&instance));
4844 assert!(app.editor.popup_buffer.is_none());
4845 }
4846
4847 #[test]
4848 fn lsp_trace_resolves_binary_name_to_canonical_id() {
4849 let mut app = app_with("hi\n", 5);
4854 let ws: std::sync::Arc<std::path::Path> =
4855 std::sync::Arc::from(std::path::Path::new("/tmp/test-ws"));
4856 let canonical = lattice_lsp::InstanceKey::new(
4857 std::sync::Arc::<str>::from("rust"),
4858 std::sync::Arc::clone(&ws),
4859 );
4860 let phantom = lattice_lsp::InstanceKey::new(
4861 std::sync::Arc::<str>::from("rust-analyzer"),
4862 std::sync::Arc::clone(&ws),
4863 );
4864 app.do_toggle_lsp_trace("rust-analyzer");
4865 assert!(!app.editor.lsp_logger.is_tracing(&canonical));
4870 assert!(!app.editor.lsp_logger.is_tracing(&phantom));
4871 let msg = app.editor.last_message.as_ref().unwrap();
4872 assert!(msg.text.contains("resolved"));
4873 }
4874
4875 #[test]
4876 fn lsp_trace_unknown_name_echoes_error_with_running_servers() {
4877 let mut app = app_with("hi\n", 5);
4878 app.do_toggle_lsp_trace("totally-fake-server-name");
4879 let msg = app.editor.last_message.as_ref().unwrap();
4880 assert!(matches!(msg.level, EchoLevel::Error));
4881 assert!(msg.text.contains("totally-fake-server-name"));
4882 }
4883
4884 #[test]
4889 fn lsp_status_with_no_servers_renders_placeholder() {
4890 let mut app = app_with("hi\n", 5);
4891 app.do_lsp_status();
4892 let body = app.popup_help().unwrap().content.as_string();
4893 assert!(body.contains("0 server"));
4894 assert!(body.contains("no LSP servers running"));
4895 }
4896
4897 #[test]
4898 fn lsp_log_level_subsystem_wide_accepts_known_levels() {
4899 let mut app = app_with("hi\n", 5);
4900 for lvl in ["error", "warn", "info", "debug", "trace"] {
4901 app.do_set_lsp_log_level(None, lvl);
4902 let msg = app.editor.last_message.as_ref().unwrap();
4903 assert!(
4904 msg.text.contains(lvl),
4905 "echo should mention {lvl}, got {}",
4906 msg.text
4907 );
4908 }
4909 }
4910
4911 #[test]
4912 fn lsp_log_level_rejects_unknown_level() {
4913 let mut app = app_with("hi\n", 5);
4914 app.do_set_lsp_log_level(None, "babble");
4915 let msg = app.editor.last_message.as_ref().unwrap();
4916 assert!(msg.text.contains("unknown log level"));
4917 }
4918
4919 #[test]
4920 fn lsp_log_level_per_server_override() {
4921 let mut app = app_with("hi\n", 5);
4922 app.do_set_lsp_log_level(Some("rust"), "debug");
4923 let instance = lattice_lsp::InstanceKey::new(
4928 std::sync::Arc::<str>::from("rust"),
4929 std::sync::Arc::<std::path::Path>::from(
4930 std::env::current_dir()
4931 .unwrap_or_else(|_| std::path::PathBuf::from("/"))
4932 .as_path(),
4933 ),
4934 );
4935 app.editor.lsp_logger.log(
4936 Some(&instance),
4937 lattice_lsp::LogLevel::Debug,
4938 lattice_lsp::LogSource::Client,
4939 "debug event",
4940 );
4941 let recs = app.editor.lsp_logger.snapshot_instance(&instance);
4942 assert!(recs.iter().any(|r| r.message == "debug event"));
4943 }
4944
4945 #[test]
4946 fn lsp_log_clear_drops_global_records() {
4947 let mut app = app_with("hi\n", 5);
4948 app.editor.lsp_logger.log(
4949 None,
4950 lattice_lsp::LogLevel::Info,
4951 lattice_lsp::LogSource::Client,
4952 "x",
4953 );
4954 assert_eq!(app.editor.lsp_logger.snapshot_global().len(), 1);
4955 app.do_lsp_log_clear(None);
4956 assert_eq!(app.editor.lsp_logger.snapshot_global().len(), 0);
4957 }
4958
4959 #[test]
4960 fn lsp_log_clear_drops_per_server_records() {
4961 let mut app = app_with("hi\n", 5);
4962 let instance = lattice_lsp::InstanceKey::new(
4963 std::sync::Arc::<str>::from("rust"),
4964 std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp/test-ws")),
4965 );
4966 let _id: std::sync::Arc<str> = std::sync::Arc::clone(&instance.server_id);
4967 app.editor.lsp_logger.log(
4968 Some(&instance),
4969 lattice_lsp::LogLevel::Info,
4970 lattice_lsp::LogSource::Client,
4971 "x",
4972 );
4973 assert_eq!(app.editor.lsp_logger.snapshot_instance(&instance).len(), 1);
4974 app.do_lsp_log_clear(Some("rust"));
4975 assert_eq!(app.editor.lsp_logger.snapshot_instance(&instance).len(), 0);
4976 }
4977
4978 fn inject_show_message_request(app: &mut App, req: lattice_lsp::InboundShowMessageRequest) {
4990 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
4991 tx.send(req).unwrap();
4992 app.editor.pending_show_message_request_rx = Some(rx);
4993 app.drain_inbound_show_message_requests();
4994 }
4995
4996 fn make_smr(
4997 server_id: &std::sync::Arc<str>,
4998 message: &str,
4999 actions: Vec<&str>,
5000 ) -> (
5001 lattice_lsp::InboundShowMessageRequest,
5002 tokio::sync::oneshot::Receiver<lattice_lsp::ShowMessageRequestOutcome>,
5003 ) {
5004 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
5005 let req = lattice_lsp::InboundShowMessageRequest {
5006 server_id: server_id.clone(),
5007 workspace: std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp")),
5008 level: lattice_lsp::lsp_types::MessageType::INFO,
5009 message: message.into(),
5010 actions: actions
5011 .into_iter()
5012 .map(|t| lattice_lsp::lsp_types::MessageActionItem {
5013 title: t.into(),
5014 properties: Default::default(),
5015 })
5016 .collect(),
5017 response: response_tx,
5018 };
5019 (req, response_rx)
5020 }
5021
5022 #[test]
5026 fn show_message_request_actionless_auto_dismisses() {
5027 let mut app = app_with("hi\n", 5);
5028 let server_id: std::sync::Arc<str> = std::sync::Arc::from("rust");
5029 let (req, mut response_rx) = make_smr(&server_id, "Heads up!", Vec::new());
5030 inject_show_message_request(&mut app, req);
5031 let outcome = response_rx.try_recv().expect("reply landed");
5032 assert!(
5033 outcome.selected.is_none(),
5034 "actionless prompt should auto-dismiss",
5035 );
5036 assert!(
5037 app.editor.picker.is_none(),
5038 "no picker for actionless prompt"
5039 );
5040 let msg = app.editor.last_message.as_ref().expect("minibuffer set");
5041 assert!(msg.text.contains("Heads up!"));
5042 let records = app
5043 .editor
5044 .lsp_logger
5045 .snapshot_instance(&lattice_lsp::InstanceKey::new(
5046 std::sync::Arc::clone(&server_id),
5047 std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp")),
5048 ));
5049 assert!(
5050 records
5051 .iter()
5052 .any(|r| r.message.contains("showMessageRequest"))
5053 );
5054 }
5055
5056 #[test]
5059 fn show_message_request_accept_replies_with_selected_action() {
5060 let mut app = app_with("hi\n", 5);
5061 let server_id: std::sync::Arc<str> = std::sync::Arc::from("rust");
5062 let (req, mut response_rx) = make_smr(&server_id, "Reload workspace?", vec!["Yes", "No"]);
5063 inject_show_message_request(&mut app, req);
5064 assert!(
5066 app.editor.picker.is_some(),
5067 "picker should open for actionful prompt"
5068 );
5069 assert_eq!(app.editor.lsp_pending_show_message_requests.len(), 1);
5070 app.apply(crate::Action::PickerSelectNext);
5074 app.apply(crate::Action::PickerAccept);
5075 let outcome = response_rx.try_recv().expect("reply landed");
5076 let selected = outcome.selected.expect("an action was selected");
5077 assert_eq!(selected.title, "No");
5078 assert!(app.editor.picker.is_none(), "picker closed after accept");
5079 assert!(app.editor.lsp_pending_show_message_requests.is_empty());
5080 }
5081
5082 #[test]
5085 fn show_message_request_dismiss_replies_null() {
5086 let mut app = app_with("hi\n", 5);
5087 let server_id: std::sync::Arc<str> = std::sync::Arc::from("rust");
5088 let (req, mut response_rx) = make_smr(&server_id, "Reload workspace?", vec!["Yes", "No"]);
5089 inject_show_message_request(&mut app, req);
5090 assert!(app.editor.picker.is_some());
5091 app.apply(crate::Action::PickerDismiss);
5092 let outcome = response_rx.try_recv().expect("reply landed");
5093 assert!(outcome.selected.is_none(), "dismiss should reply null",);
5094 assert!(app.editor.picker.is_none());
5095 assert!(app.editor.lsp_pending_show_message_requests.is_empty());
5096 }
5097
5098 #[test]
5102 fn show_message_request_queues_when_picker_already_open() {
5103 let mut app = app_with("hi\n", 5);
5104 let server_id: std::sync::Arc<str> = std::sync::Arc::from("rust");
5105 let (req1, mut rx1) = make_smr(&server_id, "First?", vec!["A", "B"]);
5106 let (req2, mut rx2) = make_smr(&server_id, "Second?", vec!["X", "Y"]);
5107 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
5108 tx.send(req1).unwrap();
5109 tx.send(req2).unwrap();
5110 app.editor.pending_show_message_request_rx = Some(rx);
5111 app.drain_inbound_show_message_requests();
5112 assert!(app.editor.picker.is_some());
5114 assert_eq!(app.editor.lsp_show_message_request_queue.len(), 1);
5115 assert_eq!(app.editor.lsp_pending_show_message_requests.len(), 2);
5116 app.apply(crate::Action::PickerDismiss);
5120 let outcome1 = rx1.try_recv().expect("first reply landed");
5121 assert!(outcome1.selected.is_none());
5122 assert!(
5123 app.editor.picker.is_some(),
5124 "queued picker should auto-open after dismiss",
5125 );
5126 let title = app.editor.picker.as_ref().unwrap().title.clone();
5127 assert!(title.contains("Second?"));
5128 app.apply(crate::Action::PickerAccept);
5130 let outcome2 = rx2.try_recv().expect("second reply landed");
5131 let selected = outcome2.selected.expect("action picked");
5132 assert_eq!(selected.title, "X");
5133 assert!(app.editor.lsp_pending_show_message_requests.is_empty());
5134 assert!(app.editor.lsp_show_message_request_queue.is_empty());
5135 }
5136
5137 #[test]
5138 fn lsp_restart_queues_via_supervisor_mailbox() {
5139 let mut app = app_with("hi\n", 5);
5147 app.do_lsp_restart("rust");
5148 let msg = app.editor.last_message.as_ref().unwrap();
5149 assert!(
5150 msg.text.contains("queued"),
5151 "expected immediate `queued` echo; got `{}`",
5152 msg.text,
5153 );
5154 assert!(matches!(msg.level, EchoLevel::Info));
5155 }
5156
5157 fn app_with_path(text: &str, viewport: u32, path: std::path::PathBuf) -> App {
5158 let doc = lattice_core::DocumentBuilder::default()
5159 .with_text(text)
5160 .with_path(path)
5161 .build();
5162 let mut a = App::new(doc);
5163 a.set_viewport_height(viewport);
5164 a
5165 }
5166
5167 fn inject_inbound_apply_edit(a: &mut App, inbound: lattice_lsp::InboundApplyEdit) {
5168 let (tx, new_rx) = tokio::sync::mpsc::unbounded_channel();
5172 tx.send(inbound).expect("send to fresh receiver");
5173 a.editor.pending_apply_edit_rx = Some(new_rx);
5174 }
5175
5176 #[test]
5177 fn drain_inbound_apply_edits_applies_active_buffer_edit() {
5178 let dir =
5182 std::env::temp_dir().join(format!("lattice-applyedit-test-{}", std::process::id(),));
5183 let _ = std::fs::remove_dir_all(&dir);
5184 std::fs::create_dir_all(&dir).unwrap();
5185 let path = dir.join("buffer.rs");
5186 std::fs::write(&path, "fn main() {}\n").unwrap();
5187 let mut a = app_with_path("fn main() {}\n", 5, path.clone());
5188 let uri: lattice_lsp::lsp_types::Uri = lattice_lsp::actor::uri_from_path(&path);
5189 let edit = lattice_lsp::lsp_types::TextEdit {
5191 range: lattice_lsp::lsp_types::Range {
5192 start: lattice_lsp::lsp_types::Position {
5193 line: 0,
5194 character: 3,
5195 },
5196 end: lattice_lsp::lsp_types::Position {
5197 line: 0,
5198 character: 7,
5199 },
5200 },
5201 new_text: "xyz".into(),
5202 };
5203 let mut changes = std::collections::HashMap::new();
5204 changes.insert(uri, vec![edit]);
5205 let workspace_edit = lattice_lsp::lsp_types::WorkspaceEdit {
5206 changes: Some(changes),
5207 document_changes: None,
5208 change_annotations: None,
5209 };
5210 let (resp_tx, mut resp_rx) = tokio::sync::oneshot::channel();
5211 inject_inbound_apply_edit(
5212 &mut a,
5213 lattice_lsp::InboundApplyEdit {
5214 server_id: std::sync::Arc::from("test-server"),
5215 workspace: std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp")),
5216 label: Some("rename main".into()),
5217 edit: workspace_edit,
5218 response: resp_tx,
5219 },
5220 );
5221 a.drain_inbound_apply_edits();
5222 let outcome = resp_rx.try_recv().expect("drain replied via oneshot");
5225 assert!(
5226 outcome.applied,
5227 "edit applied: {:?}",
5228 outcome.failure_reason,
5229 );
5230 let after = a.editor.document.snapshot().buffer.as_string();
5231 assert_eq!(after, "fn xyz() {}\n");
5232 }
5233
5234 #[test]
5235 fn drain_inbound_apply_edits_empty_workspace_edit_replies_applied_true() {
5236 let mut a = app_with("", 5);
5241 let workspace_edit = lattice_lsp::lsp_types::WorkspaceEdit::default();
5242 let (resp_tx, mut resp_rx) = tokio::sync::oneshot::channel();
5243 inject_inbound_apply_edit(
5244 &mut a,
5245 lattice_lsp::InboundApplyEdit {
5246 server_id: std::sync::Arc::from("test-server"),
5247 workspace: std::sync::Arc::<std::path::Path>::from(std::path::Path::new("/tmp")),
5248 label: None,
5249 edit: workspace_edit,
5250 response: resp_tx,
5251 },
5252 );
5253 a.drain_inbound_apply_edits();
5254 let outcome = resp_rx.try_recv().expect("drain replied");
5255 assert!(outcome.applied);
5256 assert_eq!(
5257 outcome.failure_reason.as_deref(),
5258 Some("empty workspace edit"),
5259 );
5260 }
5261
5262 #[test]
5271 fn drain_inbound_apply_edits_no_op_when_channel_empty() {
5272 let mut a = app_with("", 5);
5275 a.drain_inbound_apply_edits();
5276 assert!(a.editor.pending_apply_edit_rx.is_some());
5278 }
5279
5280 #[test]
5281 fn lsp_snippet_with_additional_edits_lands_as_one_undo_unit() {
5282 let mut a = app_with("\n\nfor", 10);
5287 a.editor.modal = ModalState::Insert;
5288 a.editor.cursor = Position::new(2, 3);
5289 let mut state = lattice_completion::InsertCompletionState::open(
5294 lattice_completion::CompletionTrigger::Manual,
5295 Position::new(2, 0),
5296 Position::new(2, 3),
5297 "for".into(),
5298 );
5299 let meta = LspCompletionMeta {
5300 label: "for-loop".into(),
5301 insert_text: "for ${1:i} in iter {}".into(),
5304 filter_text: None,
5305 sort_text: None,
5306 detail: None,
5307 documentation: None,
5308 kind: Some(lattice_lsp::lsp_types::CompletionItemKind::SNIPPET),
5309 deprecated: false,
5310 preselect: false,
5311 commit_characters: Vec::new(),
5312 additional_text_edits: vec![lattice_lsp::lsp_types::TextEdit {
5313 range: lattice_lsp::lsp_types::Range {
5314 start: lattice_lsp::lsp_types::Position {
5315 line: 0,
5316 character: 0,
5317 },
5318 end: lattice_lsp::lsp_types::Position {
5319 line: 0,
5320 character: 0,
5321 },
5322 },
5323 new_text: "use std::iter;\n".into(),
5324 }],
5325 command: None,
5326 insert_text_format: lattice_lsp::lsp_types::InsertTextFormat::SNIPPET,
5327 replace_range: None,
5328 server_id: "test-server".to_string(),
5329 original_item: lattice_lsp::lsp_types::CompletionItem::default(),
5330 resolved: true,
5331 };
5332 let mut raw = lattice_completion::RawCandidate::plain(
5333 "for",
5334 lattice_completion::CandidateKind::Plain,
5335 )
5336 .with_source(lattice_completion::SourceId::new(
5337 lattice_completion::LSP_COMPLETION_SOURCE_ID,
5338 ));
5339 raw.data = lattice_completion::CandidateData::Extension {
5340 kind_id: LSP_COMPLETION_KIND_ID,
5341 payload: lattice_lsp::completion::encode_meta(&meta),
5342 };
5343 state.raw.push(raw.clone());
5344 state
5345 .rendered
5346 .push(lattice_completion::RenderedCandidate::from_scored(
5347 lattice_completion::ScoredCandidate {
5348 raw,
5349 score: lattice_completion::MatchScore(100),
5350 match_ranges: Vec::new(),
5351 },
5352 ));
5353 let _ = meta;
5355 a.editor.insert_completion = Some(state);
5356 a.do_completion_accept();
5357 let after_accept = a.editor.document.snapshot().buffer.as_string();
5364 assert!(
5365 after_accept.contains("use std::iter;"),
5366 "auto-import applied: `{after_accept}`"
5367 );
5368 assert!(
5369 after_accept.contains("for i in iter {}"),
5370 "snippet expanded: `{after_accept}`"
5371 );
5372 assert!(
5374 a.editor
5375 .snippet_session
5376 .is_active(a.editor.document_buffer_id),
5377 "active snippet started"
5378 );
5379 a.undo_blocking().expect("undo");
5382 let after_undo = a.editor.document.snapshot().buffer.as_string();
5383 assert_eq!(
5384 after_undo, "\n\nfor",
5385 "single undo reverted both auto-import and snippet (`{after_undo}`)",
5386 );
5387 }
5388
5389 #[test]
5390 fn next_diagnostic_advances_cursor() {
5391 let mut app = app_with("a\nb\nc\nd\ne\n", 10);
5392 seed_diags_at_lines(&mut app, &[1, 3]);
5393 app.editor.cursor = Position::new(0, 0);
5394 app.do_next_diagnostic();
5395 assert_eq!(app.editor.cursor, Position::new(1, 0));
5396 app.do_next_diagnostic();
5397 assert_eq!(app.editor.cursor, Position::new(3, 0));
5398 app.do_next_diagnostic();
5400 assert_eq!(app.editor.cursor, Position::new(1, 0));
5401 }
5402
5403 #[test]
5404 fn prev_diagnostic_walks_backward() {
5405 let mut app = app_with("a\nb\nc\nd\ne\n", 10);
5406 seed_diags_at_lines(&mut app, &[1, 3]);
5407 app.editor.cursor = Position::new(4, 0);
5408 app.do_prev_diagnostic();
5409 assert_eq!(app.editor.cursor, Position::new(3, 0));
5410 app.do_prev_diagnostic();
5411 assert_eq!(app.editor.cursor, Position::new(1, 0));
5412 app.do_prev_diagnostic();
5414 assert_eq!(app.editor.cursor, Position::new(3, 0));
5415 }
5416
5417 #[test]
5420 fn next_diagnostic_echoes_landed_message() {
5421 let mut app = app_with("a\nb\nc\nd\ne\n", 10);
5422 seed_diags_at_lines(&mut app, &[1, 3]);
5423 app.editor.cursor = Position::new(0, 0);
5424 app.do_next_diagnostic();
5425 assert_eq!(app.editor.cursor, Position::new(1, 0));
5426 let msg = app
5427 .editor
5428 .last_message
5429 .as_ref()
5430 .expect("jump should echo the landed diagnostic");
5431 assert!(msg.text.contains("err on line 1"), "got: {}", msg.text);
5432 }
5433
5434 #[test]
5435 fn next_diagnostic_with_no_attachment_echoes_error() {
5436 let mut app = app_with("hi\n", 5);
5437 app.toggle_mode_by_name("lsp-mode");
5442 app.do_next_diagnostic();
5444 let msg = app.editor.last_message.as_ref().expect("expected echo");
5445 assert!(msg.text.contains("no LSP attachment"), "got: {}", msg.text);
5446 }
5447
5448 #[test]
5449 fn next_diagnostic_with_no_diagnostics_echoes_info() {
5450 let mut app = app_with("hi\n", 5);
5451 use std::str::FromStr;
5454 let uri = lattice_lsp::Uri::from_str("file:///tmp/empty.rs").unwrap();
5455 app.editor
5456 .buffer_uris
5457 .insert(app.editor.document_buffer_id, uri);
5458 app.toggle_mode_by_name("lsp-mode");
5459 app.do_next_diagnostic();
5460 let msg = app.editor.last_message.as_ref().expect("expected echo");
5461 assert!(msg.text.contains("no diagnostics"), "got: {}", msg.text);
5462 }
5463
5464 #[test]
5465 fn m6_end_to_end_independent_sub_modes_per_feature() {
5466 let mut a = app_with("xx", 10);
5473 a.toggle_mode_by_name("lsp-mode");
5474 assert!(a.lsp_format_mode_enabled_for(a.editor.document_buffer_id));
5475 assert!(a.lsp_hover_mode_enabled_for(a.editor.document_buffer_id));
5476
5477 a.toggle_mode_by_name("lsp-format-mode");
5479 assert!(!a.lsp_format_mode_enabled_for(a.editor.document_buffer_id));
5480 assert!(a.lsp_hover_mode_enabled_for(a.editor.document_buffer_id));
5482 assert!(a.lsp_completion_mode_enabled_for(a.editor.document_buffer_id));
5483 assert!(a.lsp_diagnostics_mode_enabled_for(a.editor.document_buffer_id));
5484
5485 a.do_lsp_format_request(false);
5487 assert!(
5488 a.editor
5489 .last_message
5490 .as_ref()
5491 .map(|m| m.text.contains("lsp-format-mode disabled"))
5492 .unwrap_or(false),
5493 "expected format-mode echo, got: {:?}",
5494 a.editor.last_message,
5495 );
5496
5497 a.editor.last_message = None;
5500 a.apply_effect(lattice_grammar::Effect::Lsp(
5501 lattice_grammar::LspRequest::Hover,
5502 ));
5503 if let Some(msg) = &a.editor.last_message {
5504 assert!(
5505 !msg.text.contains("lsp-hover-mode disabled"),
5506 "hover sub-mode unexpectedly gated: {}",
5507 msg.text,
5508 );
5509 }
5510
5511 a.toggle_mode_by_name("lsp-format-mode");
5513 assert!(a.lsp_format_mode_enabled_for(a.editor.document_buffer_id));
5514 }
5515
5516 #[test]
5517 fn next_diagnostic_with_lsp_diagnostics_mode_off_echoes_gate() {
5518 let mut app = app_with("hi\n", 5);
5522 seed_diags_at_lines(&mut app, &[0]);
5523 app.toggle_mode_by_name("lsp-diagnostics-mode");
5526 app.do_next_diagnostic();
5527 let msg = app.editor.last_message.as_ref().expect("gate echo");
5528 assert!(
5529 msg.text.contains("lsp-diagnostics-mode disabled"),
5530 "expected sub-mode gate echo, got: {}",
5531 msg.text,
5532 );
5533 }
5534
5535 #[test]
5536 fn list_diagnostics_opens_picker() {
5537 let mut app = app_with("hi\n", 5);
5538 seed_diags_at_lines(&mut app, &[0, 1]);
5539 app.do_list_diagnostics();
5540 let picker = app.editor.picker.as_ref().expect("picker should open");
5541 assert!(picker.title.starts_with("diagnostics"));
5542 assert!(matches!(
5543 picker.source,
5544 lattice_picker::PickerSource::LspLocations
5545 ));
5546 assert!(matches!(
5547 picker.on_accept,
5548 lattice_picker::PickerAction::JumpToLspLocation
5549 ));
5550 assert_eq!(picker.candidates.len(), 2);
5552 let display = &picker.candidates[0].raw.display;
5554 assert!(display.starts_with("[E]"), "got: {display}");
5555 assert!(app.editor.popup_buffer.is_none());
5557 }
5558
5559 #[test]
5560 fn list_diagnostics_with_empty_layer_echoes() {
5561 let mut app = app_with("hi\n", 5);
5562 app.do_list_diagnostics();
5564 assert!(app.editor.picker.is_none());
5566 let msg = app.editor.last_message.as_ref().expect("echo");
5567 assert!(msg.text.contains("no diagnostics"));
5568 }
5569
5570 #[derive(Debug)]
5585 struct FakeAsyncSource {
5586 id: &'static str,
5587 candidates: Vec<String>,
5588 seen: std::sync::Arc<std::sync::Mutex<Option<lattice_completion::InsertContextSnapshot>>>,
5589 }
5590
5591 impl lattice_completion::AsyncCompletionSource for FakeAsyncSource {
5592 fn produce_async(
5593 &self,
5594 ctx: lattice_completion::InsertContextSnapshot,
5595 sink: std::sync::Arc<dyn lattice_completion::CandidateSink>,
5596 _token: lattice_protocol::CancellationToken,
5597 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
5598 *self.seen.lock().unwrap() = Some(ctx);
5599 let id = lattice_completion::SourceId::new(self.id);
5600 let candidates = self.candidates.clone();
5601 Box::pin(async move {
5602 for text in candidates {
5603 sink.push(
5604 lattice_completion::RawCandidate::plain(
5605 text,
5606 lattice_completion::CandidateKind::Plain,
5607 )
5608 .with_source(id.clone()),
5609 );
5610 }
5611 })
5612 }
5613 }
5614
5615 fn seed_async_source(
5616 a: &mut App,
5617 id: &'static str,
5618 candidates: &[&str],
5619 ) -> std::sync::Arc<std::sync::Mutex<Option<lattice_completion::InsertContextSnapshot>>> {
5620 let seen = std::sync::Arc::new(std::sync::Mutex::new(None));
5621 let source = std::sync::Arc::new(FakeAsyncSource {
5622 id,
5623 candidates: candidates.iter().map(|s| s.to_string()).collect(),
5624 seen: seen.clone(),
5625 });
5626 let contribution = lattice_completion::CompletionSourceContribution {
5627 accepts_non_word_query: false,
5628 id: lattice_completion::SourceId::new(id),
5629 default_priority: 100,
5630 auto_trigger: true,
5631 trigger_chars: Vec::new(),
5632 popup_filter_chord: None,
5633 kind: lattice_completion::CompletionSourceKind::Async(source),
5634 };
5635 let buffer = a.editor.document_buffer_id;
5636 a.editor
5637 .buffer_locals
5638 .entry(buffer)
5639 .or_default()
5640 .insert(lattice_mode::ActiveCompletionSources(vec![contribution]));
5641 seen
5642 }
5643
5644 fn settle_async_completion(a: &mut App, want: usize) {
5649 for _ in 0..200 {
5650 a.drain_pending_insert_completion_lsp();
5651 let n = a
5652 .editor
5653 .insert_completion
5654 .as_ref()
5655 .map(|s| s.raw.len())
5656 .unwrap_or(0);
5657 if n >= want {
5658 return;
5659 }
5660 std::thread::sleep(std::time::Duration::from_millis(5));
5661 }
5662 }
5663
5664 #[test]
5665 fn async_fan_out_drives_a_non_lsp_source() {
5666 let mut a = app_with("\nal", 10);
5667 a.editor.modal = ModalState::Insert;
5668 a.editor.cursor = Position::new(1, 2);
5669 seed_async_source(&mut a, "gen:test-plugin", &["alpha", "alphabet"]);
5670 a.editor.insert_completion = Some(lattice_completion::InsertCompletionState::open(
5671 lattice_completion::CompletionTrigger::Manual,
5672 Position::new(1, 0),
5673 Position::new(1, 2),
5674 "al".to_string(),
5675 ));
5676
5677 a.mutate_editor(|e| e.do_async_insert_completion_requests());
5678 settle_async_completion(&mut a, 2);
5679
5680 let state = a.editor.insert_completion.as_ref().expect("popup open");
5681 let texts: Vec<&str> = state.raw.iter().map(|c| c.text.as_str()).collect();
5682 assert!(
5683 texts.contains(&"alpha") && texts.contains(&"alphabet"),
5684 "a non-LSP async source contributed nothing: {texts:?}"
5685 );
5686 }
5687
5688 #[test]
5689 fn a_plugin_source_runs_with_no_lsp_server_attached() {
5690 let mut a = app_with("\nal", 10);
5693 a.editor.modal = ModalState::Insert;
5694 a.editor.cursor = Position::new(1, 2);
5695 assert!(
5696 a.editor
5697 .buffer_uris
5698 .get(&a.editor.document_buffer_id)
5699 .is_none(),
5700 "precondition: no URI, so LSP sits the round out"
5701 );
5702 seed_async_source(&mut a, "gen:test-plugin", &["alpha"]);
5703 a.editor.insert_completion = Some(lattice_completion::InsertCompletionState::open(
5704 lattice_completion::CompletionTrigger::Manual,
5705 Position::new(1, 0),
5706 Position::new(1, 2),
5707 "al".to_string(),
5708 ));
5709
5710 a.mutate_editor(|e| e.do_async_insert_completion_requests());
5711 settle_async_completion(&mut a, 1);
5712
5713 let state = a.editor.insert_completion.as_ref().expect("popup open");
5714 assert_eq!(
5715 state
5716 .raw
5717 .iter()
5718 .map(|c| c.text.as_str())
5719 .collect::<Vec<_>>(),
5720 vec!["alpha"]
5721 );
5722 }
5723
5724 #[test]
5725 fn a_source_sees_the_line_before_the_cursor() {
5726 let mut a = app_with("\nsee [[Ti", 10);
5729 a.editor.modal = ModalState::Insert;
5730 a.editor.cursor = Position::new(1, 9);
5731 let seen = seed_async_source(&mut a, "gen:test-plugin", &["Title"]);
5732 a.editor.insert_completion = Some(lattice_completion::InsertCompletionState::open(
5733 lattice_completion::CompletionTrigger::Manual,
5734 Position::new(1, 7),
5735 Position::new(1, 9),
5736 "Ti".to_string(),
5737 ));
5738
5739 a.mutate_editor(|e| e.do_async_insert_completion_requests());
5740 settle_async_completion(&mut a, 1);
5741
5742 let ctx = seen.lock().unwrap().clone().expect("the source was called");
5743 assert_eq!(ctx.line_before_cursor, "see [[Ti");
5744 assert_eq!(ctx.query, "Ti");
5745 }
5746
5747 #[test]
5748 fn each_source_replaces_only_its_own_rows() {
5749 let mut a = app_with("\nal", 10);
5753 a.editor.modal = ModalState::Insert;
5754 a.editor.cursor = Position::new(1, 2);
5755 a.editor.insert_completion = Some(lattice_completion::InsertCompletionState::open(
5756 lattice_completion::CompletionTrigger::Manual,
5757 Position::new(1, 0),
5758 Position::new(1, 2),
5759 "al".to_string(),
5760 ));
5761 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<super::AsyncCompletionOutcome>();
5762 a.editor.pending_insert_completion_async_rx = Some(rx);
5763 let one = lattice_completion::SourceId::new("gen:one");
5764 let two = lattice_completion::SourceId::new("gen:two");
5765 let row = |text: &str, src: &lattice_completion::SourceId| {
5766 lattice_completion::RawCandidate::plain(
5767 text.to_string(),
5768 lattice_completion::CandidateKind::Plain,
5769 )
5770 .with_source(src.clone())
5771 };
5772 tx.send(super::AsyncCompletionOutcome::Items {
5773 candidates: vec![row("alpha", &one)],
5774 sources: vec![one.clone()],
5775 is_incomplete: false,
5776 })
5777 .unwrap();
5778 tx.send(super::AsyncCompletionOutcome::Items {
5779 candidates: vec![row("also", &two)],
5780 sources: vec![two.clone()],
5781 is_incomplete: false,
5782 })
5783 .unwrap();
5784 a.drain_pending_insert_completion_lsp();
5785
5786 let mut texts: Vec<String> = a
5787 .editor
5788 .insert_completion
5789 .as_ref()
5790 .expect("popup")
5791 .raw
5792 .iter()
5793 .map(|c| c.text.clone())
5794 .collect();
5795 texts.sort();
5796 assert_eq!(texts, vec!["alpha", "also"], "both senders' rows survived");
5797
5798 tx.send(super::AsyncCompletionOutcome::Items {
5801 candidates: vec![row("altered", &one)],
5802 sources: vec![one],
5803 is_incomplete: false,
5804 })
5805 .unwrap();
5806 a.drain_pending_insert_completion_lsp();
5807 let mut texts: Vec<String> = a
5808 .editor
5809 .insert_completion
5810 .as_ref()
5811 .expect("popup")
5812 .raw
5813 .iter()
5814 .map(|c| c.text.clone())
5815 .collect();
5816 texts.sort();
5817 assert_eq!(texts, vec!["also", "altered"]);
5818 }
5819
5820 #[test]
5821 fn a_candidate_may_insert_something_other_than_what_it_matched() {
5822 let mut a = app_with("\nTi", 10);
5828 a.editor.modal = ModalState::Insert;
5829 a.editor.cursor = Position::new(1, 2);
5830 let mut state = lattice_completion::InsertCompletionState::open(
5831 lattice_completion::CompletionTrigger::Manual,
5832 Position::new(1, 0),
5833 Position::new(1, 2),
5834 "Ti".to_string(),
5835 );
5836 let mut raw = lattice_completion::RawCandidate::plain(
5837 "Title".to_string(),
5838 lattice_completion::CandidateKind::Plain,
5839 );
5840 raw.insert_text = Some("[[id:abc][Title]]".to_string());
5841 state.raw = vec![raw];
5842 a.editor.insert_completion = Some(state);
5843 a.mutate_editor(|e| {
5844 let mut s = e.insert_completion.take().expect("popup");
5845 e.refilter_insert_completion(&mut s);
5846 e.insert_completion = Some(s);
5847 });
5848
5849 a.mutate_editor(|e| e.do_completion_accept());
5850
5851 let text = a.editor.document.snapshot().buffer.line(1).unwrap();
5852 assert_eq!(text, "[[id:abc][Title]]");
5853 }
5854}