1use lsp_types::{GotoDefinitionResponse, Location, Position, TextEdit, Uri, WorkspaceEdit};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HierarchyDirection {
16 Subtypes,
17 Supertypes,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct TypeHierarchyNode {
23 pub name: String,
24 pub path: String,
26 pub line: u32,
28 pub children: Vec<TypeHierarchyNode>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SymbolOverviewItem {
34 pub name: String,
35 pub kind: String,
36 pub line: u32,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct InspectionDiag {
43 pub path: String,
45 pub line: u32,
47 pub severity: String,
48 pub message: String,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct InspectionInfo {
54 pub id: String,
56 pub name: String,
58 pub severity: String,
60}
61
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub struct Truncation {
65 pub truncated: bool,
66 pub total: u32,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct TextRange0Based {
73 pub start_line: u32,
74 pub start_char: u32,
75 pub end_line: u32,
76 pub end_char: u32,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct RangeEdit {
83 pub abs_path: String,
85 pub rel_path: String,
87 pub range: TextRange0Based,
89 pub text: String,
91 pub expected_hash: Option<String>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct EditResult {
98 pub applied: bool,
99 pub new_range: TextRange0Based,
101 pub edited_text: String,
103 pub diff: String,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct RenameQuery {
112 pub abs_path: String,
114 pub rel_path: String,
116 pub target_range: TextRange0Based,
118 pub new_name: String,
119 pub search_comments: bool,
121 pub search_text_occurrences: bool,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct UsageSite {
129 pub path: String,
131 pub range: TextRange0Based,
133 pub context: Option<String>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Conflict {
142 pub path: String,
143 pub range: Option<TextRange0Based>,
144 pub message: String,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct RenamePlan {
151 pub usages: Vec<UsageSite>,
152 pub conflicts: Vec<Conflict>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct RenameApply {
159 pub abs_path: String,
160 pub rel_path: String,
161 pub target_range: TextRange0Based,
162 pub new_name: String,
163 pub force: bool,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct RenameResult {
170 pub applied: bool,
171 pub changed_paths: Vec<String>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum MoveTarget {
180 Path { abs_path: String, rel_path: String },
182 Parent {
185 abs_path: String,
186 rel_path: String,
187 range: TextRange0Based,
188 },
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct MoveQuery {
195 pub abs_path: String,
196 pub rel_path: String,
197 pub src_range: TextRange0Based,
198 pub target: MoveTarget,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct MoveApply {
205 pub query: MoveQuery,
206 pub force: bool,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct SafeDeleteQuery {
213 pub abs_path: String,
214 pub rel_path: String,
215 pub src_range: TextRange0Based,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct SafeDeleteApply {
222 pub query: SafeDeleteQuery,
223 pub force: bool,
224 pub propagate: bool,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct InlineQuery {
232 pub abs_path: String,
233 pub rel_path: String,
234 pub src_range: TextRange0Based,
235 pub keep_definition: bool,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct InlineApply {
242 pub query: InlineQuery,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum ReformatScope {
249 File,
250 Region { range: TextRange0Based },
251 Symbol { range: TextRange0Based },
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct ReformatQuery {
257 pub abs_path: String,
258 pub rel_path: String,
259 pub scope: ReformatScope,
260 pub optimize_imports: bool,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct ReformatResult {
268 pub applied: bool,
269 pub changed_paths: Vec<String>,
270}
271
272pub trait LspBackend: Send {
275 fn open_file(&mut self, uri: &Uri, language_id: &str, text: &str) -> Result<(), String>;
277 fn references(
278 &mut self,
279 uri: &Uri,
280 position: Position,
281 scope: &str,
282 ) -> Result<Vec<Location>, String>;
283 fn definition(
284 &mut self,
285 uri: &Uri,
286 position: Position,
287 ) -> Result<GotoDefinitionResponse, String>;
288 fn implementations(
289 &mut self,
290 uri: &Uri,
291 position: Position,
292 scope: &str,
293 ) -> Result<Vec<Location>, String>;
294 fn rename(
295 &mut self,
296 uri: &Uri,
297 position: Position,
298 new_name: &str,
299 ) -> Result<Option<WorkspaceEdit>, String>;
300
301 fn declaration(&mut self, _uri: &Uri, _position: Position) -> Result<Vec<Location>, String> {
303 Err("declaration requires the JetBrains backend".to_string())
304 }
305 fn type_hierarchy(
306 &mut self,
307 _uri: &Uri,
308 _position: Position,
309 _direction: HierarchyDirection,
310 ) -> Result<TypeHierarchyNode, String> {
311 Err("type_hierarchy requires the JetBrains backend".to_string())
312 }
313 fn symbols_overview(&mut self, uri: &Uri) -> Result<Vec<SymbolOverviewItem>, String> {
314 let abs = crate::lsp::client::uri_to_file_path(uri)
317 .ok_or_else(|| "symbols_overview: bad uri".to_string())?;
318 Ok(crate::lsp::edit_apply::overview_from_index(&abs))
319 }
320 fn format(&mut self, _uri: &Uri) -> Result<Vec<TextEdit>, String> {
321 Err("format requires the JetBrains backend".to_string())
322 }
323 fn inspections(&mut self, _uri: &Uri) -> Result<Vec<InspectionDiag>, String> {
324 Err("inspections requires the JetBrains backend".to_string())
325 }
326 fn list_inspections(&mut self) -> Result<Vec<InspectionInfo>, String> {
327 Err("list_inspections requires the JetBrains backend".to_string())
328 }
329
330 fn replace_symbol_body(&mut self, edit: &RangeEdit) -> Result<EditResult, String> {
333 crate::lsp::edit_apply::local_range_write(edit)
334 }
335 fn insert_before_symbol(&mut self, edit: &RangeEdit) -> Result<EditResult, String> {
338 crate::lsp::edit_apply::local_range_write(edit)
339 }
340 fn insert_after_symbol(&mut self, edit: &RangeEdit) -> Result<EditResult, String> {
343 crate::lsp::edit_apply::local_range_write(edit)
344 }
345
346 fn rename_preview(&mut self, _req: &RenameQuery) -> Result<RenamePlan, String> {
350 Err("BACKEND_REQUIRED: rename requires a running JetBrains IDE".to_string())
351 }
352 fn rename_apply(&mut self, _req: &RenameApply) -> Result<RenameResult, String> {
355 Err("BACKEND_REQUIRED: rename requires a running JetBrains IDE".to_string())
356 }
357
358 fn move_preview(&mut self, _req: &MoveQuery) -> Result<RenamePlan, String> {
362 Err("BACKEND_REQUIRED: move requires a running JetBrains IDE".to_string())
363 }
364 fn move_apply(&mut self, _req: &MoveApply) -> Result<RenameResult, String> {
367 Err("BACKEND_REQUIRED: move requires a running JetBrains IDE".to_string())
368 }
369 fn safe_delete_preview(&mut self, _req: &SafeDeleteQuery) -> Result<RenamePlan, String> {
372 Err("BACKEND_REQUIRED: safe_delete requires a running JetBrains IDE".to_string())
373 }
374 fn safe_delete_apply(&mut self, _req: &SafeDeleteApply) -> Result<RenameResult, String> {
377 Err("BACKEND_REQUIRED: safe_delete requires a running JetBrains IDE".to_string())
378 }
379
380 fn inline_preview(&mut self, _req: &InlineQuery) -> Result<RenamePlan, String> {
383 Err("BACKEND_REQUIRED: inline requires a running JetBrains IDE".to_string())
384 }
385 fn inline_apply(&mut self, _req: &InlineApply) -> Result<RenameResult, String> {
389 Err("BACKEND_REQUIRED: inline requires a running JetBrains IDE".to_string())
390 }
391 fn reformat(&mut self, _req: &ReformatQuery) -> Result<ReformatResult, String> {
394 Err("BACKEND_REQUIRED: reformat requires a running JetBrains IDE".to_string())
395 }
396
397 fn is_stale(&self, _project_root: &str) -> bool {
403 false
404 }
405 fn is_dead_after_error(&self, project_root: &str) -> bool {
409 self.is_stale(project_root)
410 }
411 fn set_request_timeout(&mut self, _timeout: Option<std::time::Duration>) {}
415 fn backend_info(&self) -> crate::lsp::capabilities::SemanticBackendInfo {
418 use crate::lsp::capabilities::{
419 SemanticBackendInfo, SemanticBackendKind, SemanticCapabilities,
420 };
421 SemanticBackendInfo {
422 kind: SemanticBackendKind::Lsp,
423 server_name: None,
424 server_version: None,
425 capabilities: SemanticCapabilities {
426 definition: true,
427 references: true,
428 implementations: true,
429 rename: true,
430 ..SemanticCapabilities::default()
431 },
432 utf8_positions: false,
433 }
434 }
435 fn last_truncation(&self) -> Option<Truncation> {
438 None
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn rename_types_construct_and_clone() {
448 let q = RenameQuery {
449 abs_path: "/proj/a.rs".into(),
450 rel_path: "a.rs".into(),
451 target_range: TextRange0Based {
452 start_line: 0,
453 start_char: 0,
454 end_line: 0,
455 end_char: 3,
456 },
457 new_name: "bar".into(),
458 search_comments: false,
459 search_text_occurrences: false,
460 };
461 let q2 = q.clone();
462 assert_eq!(q2.new_name, "bar");
463
464 let plan = RenamePlan {
465 usages: vec![UsageSite {
466 path: "a.rs".into(),
467 range: TextRange0Based {
468 start_line: 1,
469 start_char: 4,
470 end_line: 1,
471 end_char: 7,
472 },
473 context: Some("foo()".into()),
474 }],
475 conflicts: vec![Conflict {
476 path: "a.rs".into(),
477 range: None,
478 message: "name already exists".into(),
479 }],
480 };
481 assert_eq!(plan.usages.len(), 1);
482 assert_eq!(plan.conflicts[0].message, "name already exists");
483
484 let apply = RenameApply {
485 abs_path: "/proj/a.rs".into(),
486 rel_path: "a.rs".into(),
487 target_range: q.target_range,
488 new_name: "bar".into(),
489 force: true,
490 };
491 let res = RenameResult {
492 applied: true,
493 changed_paths: vec!["a.rs".into()],
494 };
495 assert!(apply.force);
496 assert!(res.applied);
497 }
498
499 #[test]
500 fn move_and_safe_delete_types_construct_and_clone() {
501 let mt = MoveTarget::Path {
502 abs_path: "/proj/app/moved".into(),
503 rel_path: "app/moved".into(),
504 };
505 let mq = MoveQuery {
506 abs_path: "/proj/Widget.kt".into(),
507 rel_path: "Widget.kt".into(),
508 src_range: TextRange0Based {
509 start_line: 2,
510 start_char: 0,
511 end_line: 2,
512 end_char: 12,
513 },
514 target: mt.clone(),
515 };
516 let ma = MoveApply {
517 query: mq.clone(),
518 force: true,
519 };
520 assert_eq!(ma.query.target, mt);
521
522 let parent = MoveTarget::Parent {
523 abs_path: "/proj/Other.kt".into(),
524 rel_path: "Other.kt".into(),
525 range: TextRange0Based {
526 start_line: 0,
527 start_char: 0,
528 end_line: 5,
529 end_char: 1,
530 },
531 };
532 assert_ne!(parent, mt);
533
534 let sq = SafeDeleteQuery {
535 abs_path: "/proj/Widget.kt".into(),
536 rel_path: "Widget.kt".into(),
537 src_range: TextRange0Based {
538 start_line: 2,
539 start_char: 0,
540 end_line: 2,
541 end_char: 12,
542 },
543 };
544 let sa = SafeDeleteApply {
545 query: sq.clone(),
546 force: true,
547 propagate: false,
548 };
549 assert_eq!(sa.query, sq);
550 assert!(sa.force);
551 assert!(!sa.propagate);
552 }
553
554 #[test]
555 fn inline_and_reformat_types_construct_and_clone() {
556 let range = TextRange0Based {
557 start_line: 1,
558 start_char: 0,
559 end_line: 1,
560 end_char: 9,
561 };
562 let iq = InlineQuery {
563 abs_path: "/p/Calc.kt".into(),
564 rel_path: "Calc.kt".into(),
565 src_range: range,
566 keep_definition: false,
567 };
568 assert_eq!(iq.clone(), iq);
569 let ia = InlineApply { query: iq.clone() };
570 assert!(!ia.clone().query.keep_definition);
571 let rq = ReformatQuery {
572 abs_path: "/p/M.kt".into(),
573 rel_path: "M.kt".into(),
574 scope: ReformatScope::File,
575 optimize_imports: true,
576 };
577 assert_eq!(rq.clone(), rq);
578 assert!(matches!(
579 ReformatQuery {
580 scope: ReformatScope::Region { range },
581 ..rq.clone()
582 }
583 .scope,
584 ReformatScope::Region { .. }
585 ));
586 let rr = ReformatResult {
587 applied: true,
588 changed_paths: vec!["M.kt".into()],
589 };
590 assert_eq!(rr.clone(), rr);
591 }
592
593 #[test]
594 fn headless_inline_and_reformat_default_is_backend_required() {
595 struct Bare2;
596 impl LspBackend for Bare2 {
598 fn open_file(&mut self, _u: &Uri, _l: &str, _t: &str) -> Result<(), String> {
599 Ok(())
600 }
601 fn references(
602 &mut self,
603 _u: &Uri,
604 _p: Position,
605 _s: &str,
606 ) -> Result<Vec<Location>, String> {
607 Ok(vec![])
608 }
609 fn definition(
610 &mut self,
611 _u: &Uri,
612 _p: Position,
613 ) -> Result<GotoDefinitionResponse, String> {
614 Ok(GotoDefinitionResponse::Array(vec![]))
615 }
616 fn implementations(
617 &mut self,
618 _u: &Uri,
619 _p: Position,
620 _s: &str,
621 ) -> Result<Vec<Location>, String> {
622 Ok(vec![])
623 }
624 fn rename(
625 &mut self,
626 _u: &Uri,
627 _p: Position,
628 _n: &str,
629 ) -> Result<Option<WorkspaceEdit>, String> {
630 Ok(None)
631 }
632 }
633 let q = InlineQuery {
634 abs_path: "/a".into(),
635 rel_path: "a".into(),
636 src_range: TextRange0Based {
637 start_line: 0,
638 start_char: 0,
639 end_line: 0,
640 end_char: 0,
641 },
642 keep_definition: false,
643 };
644 assert!(
645 Bare2
646 .inline_preview(&q)
647 .unwrap_err()
648 .contains("BACKEND_REQUIRED")
649 );
650 assert!(
651 Bare2
652 .inline_apply(&InlineApply { query: q.clone() })
653 .unwrap_err()
654 .contains("BACKEND_REQUIRED")
655 );
656 let rq = ReformatQuery {
657 abs_path: "/a".into(),
658 rel_path: "a".into(),
659 scope: ReformatScope::File,
660 optimize_imports: false,
661 };
662 assert!(
663 Bare2
664 .reformat(&rq)
665 .unwrap_err()
666 .contains("BACKEND_REQUIRED")
667 );
668 }
669
670 #[test]
671 fn headless_rename_default_is_backend_required() {
672 let mut be = crate::lsp::edit_apply::HeadlessBackend;
674 let q = RenameQuery {
675 abs_path: "/x".into(),
676 rel_path: "x".into(),
677 target_range: TextRange0Based {
678 start_line: 0,
679 start_char: 0,
680 end_line: 0,
681 end_char: 1,
682 },
683 new_name: "y".into(),
684 search_comments: false,
685 search_text_occurrences: false,
686 };
687 let err = be.rename_preview(&q).unwrap_err();
688 assert!(err.starts_with("BACKEND_REQUIRED"), "got: {err}");
689 let a = RenameApply {
690 abs_path: "/x".into(),
691 rel_path: "x".into(),
692 target_range: q.target_range,
693 new_name: "y".into(),
694 force: false,
695 };
696 assert!(
697 be.rename_apply(&a)
698 .unwrap_err()
699 .starts_with("BACKEND_REQUIRED")
700 );
701 }
702
703 #[test]
704 fn headless_move_and_safe_delete_default_is_backend_required() {
705 struct Bare;
708 impl LspBackend for Bare {
709 fn open_file(&mut self, _u: &lsp_types::Uri, _l: &str, _t: &str) -> Result<(), String> {
710 Ok(())
711 }
712 fn references(
713 &mut self,
714 _u: &lsp_types::Uri,
715 _p: lsp_types::Position,
716 _s: &str,
717 ) -> Result<Vec<lsp_types::Location>, String> {
718 Ok(vec![])
719 }
720 fn definition(
721 &mut self,
722 _u: &lsp_types::Uri,
723 _p: lsp_types::Position,
724 ) -> Result<lsp_types::GotoDefinitionResponse, String> {
725 Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
726 }
727 fn implementations(
728 &mut self,
729 _u: &lsp_types::Uri,
730 _p: lsp_types::Position,
731 _s: &str,
732 ) -> Result<Vec<lsp_types::Location>, String> {
733 Ok(vec![])
734 }
735 fn rename(
736 &mut self,
737 _u: &lsp_types::Uri,
738 _p: lsp_types::Position,
739 _n: &str,
740 ) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
741 Ok(None)
742 }
743 }
744 let mut b = Bare;
745 let mq = MoveQuery {
746 abs_path: "/p/a.kt".into(),
747 rel_path: "a.kt".into(),
748 src_range: TextRange0Based {
749 start_line: 0,
750 start_char: 0,
751 end_line: 0,
752 end_char: 1,
753 },
754 target: MoveTarget::Path {
755 abs_path: "/p/x".into(),
756 rel_path: "x".into(),
757 },
758 };
759 assert!(
760 b.move_preview(&mq)
761 .unwrap_err()
762 .starts_with("BACKEND_REQUIRED")
763 );
764 assert!(
765 b.move_apply(&MoveApply {
766 query: mq,
767 force: false
768 })
769 .unwrap_err()
770 .starts_with("BACKEND_REQUIRED")
771 );
772 let sq = SafeDeleteQuery {
773 abs_path: "/p/a.kt".into(),
774 rel_path: "a.kt".into(),
775 src_range: TextRange0Based {
776 start_line: 0,
777 start_char: 0,
778 end_line: 0,
779 end_char: 1,
780 },
781 };
782 assert!(
783 b.safe_delete_preview(&sq)
784 .unwrap_err()
785 .starts_with("BACKEND_REQUIRED")
786 );
787 assert!(
788 b.safe_delete_apply(&SafeDeleteApply {
789 query: sq,
790 force: false,
791 propagate: false
792 })
793 .unwrap_err()
794 .starts_with("BACKEND_REQUIRED")
795 );
796 }
797}