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 {
402 false
403 }
404 fn last_truncation(&self) -> Option<Truncation> {
407 None
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn rename_types_construct_and_clone() {
417 let q = RenameQuery {
418 abs_path: "/proj/a.rs".into(),
419 rel_path: "a.rs".into(),
420 target_range: TextRange0Based {
421 start_line: 0,
422 start_char: 0,
423 end_line: 0,
424 end_char: 3,
425 },
426 new_name: "bar".into(),
427 search_comments: false,
428 search_text_occurrences: false,
429 };
430 let q2 = q.clone();
431 assert_eq!(q2.new_name, "bar");
432
433 let plan = RenamePlan {
434 usages: vec![UsageSite {
435 path: "a.rs".into(),
436 range: TextRange0Based {
437 start_line: 1,
438 start_char: 4,
439 end_line: 1,
440 end_char: 7,
441 },
442 context: Some("foo()".into()),
443 }],
444 conflicts: vec![Conflict {
445 path: "a.rs".into(),
446 range: None,
447 message: "name already exists".into(),
448 }],
449 };
450 assert_eq!(plan.usages.len(), 1);
451 assert_eq!(plan.conflicts[0].message, "name already exists");
452
453 let apply = RenameApply {
454 abs_path: "/proj/a.rs".into(),
455 rel_path: "a.rs".into(),
456 target_range: q.target_range,
457 new_name: "bar".into(),
458 force: true,
459 };
460 let res = RenameResult {
461 applied: true,
462 changed_paths: vec!["a.rs".into()],
463 };
464 assert!(apply.force);
465 assert!(res.applied);
466 }
467
468 #[test]
469 fn move_and_safe_delete_types_construct_and_clone() {
470 let mt = MoveTarget::Path {
471 abs_path: "/proj/app/moved".into(),
472 rel_path: "app/moved".into(),
473 };
474 let mq = MoveQuery {
475 abs_path: "/proj/Widget.kt".into(),
476 rel_path: "Widget.kt".into(),
477 src_range: TextRange0Based {
478 start_line: 2,
479 start_char: 0,
480 end_line: 2,
481 end_char: 12,
482 },
483 target: mt.clone(),
484 };
485 let ma = MoveApply {
486 query: mq.clone(),
487 force: true,
488 };
489 assert_eq!(ma.query.target, mt);
490
491 let parent = MoveTarget::Parent {
492 abs_path: "/proj/Other.kt".into(),
493 rel_path: "Other.kt".into(),
494 range: TextRange0Based {
495 start_line: 0,
496 start_char: 0,
497 end_line: 5,
498 end_char: 1,
499 },
500 };
501 assert_ne!(parent, mt);
502
503 let sq = SafeDeleteQuery {
504 abs_path: "/proj/Widget.kt".into(),
505 rel_path: "Widget.kt".into(),
506 src_range: TextRange0Based {
507 start_line: 2,
508 start_char: 0,
509 end_line: 2,
510 end_char: 12,
511 },
512 };
513 let sa = SafeDeleteApply {
514 query: sq.clone(),
515 force: true,
516 propagate: false,
517 };
518 assert_eq!(sa.query, sq);
519 assert!(sa.force);
520 assert!(!sa.propagate);
521 }
522
523 #[test]
524 fn inline_and_reformat_types_construct_and_clone() {
525 let range = TextRange0Based {
526 start_line: 1,
527 start_char: 0,
528 end_line: 1,
529 end_char: 9,
530 };
531 let iq = InlineQuery {
532 abs_path: "/p/Calc.kt".into(),
533 rel_path: "Calc.kt".into(),
534 src_range: range,
535 keep_definition: false,
536 };
537 assert_eq!(iq.clone(), iq);
538 let ia = InlineApply { query: iq.clone() };
539 assert!(!ia.clone().query.keep_definition);
540 let rq = ReformatQuery {
541 abs_path: "/p/M.kt".into(),
542 rel_path: "M.kt".into(),
543 scope: ReformatScope::File,
544 optimize_imports: true,
545 };
546 assert_eq!(rq.clone(), rq);
547 assert!(matches!(
548 ReformatQuery {
549 scope: ReformatScope::Region { range },
550 ..rq.clone()
551 }
552 .scope,
553 ReformatScope::Region { .. }
554 ));
555 let rr = ReformatResult {
556 applied: true,
557 changed_paths: vec!["M.kt".into()],
558 };
559 assert_eq!(rr.clone(), rr);
560 }
561
562 #[test]
563 fn headless_inline_and_reformat_default_is_backend_required() {
564 struct Bare2;
565 impl LspBackend for Bare2 {
567 fn open_file(&mut self, _u: &Uri, _l: &str, _t: &str) -> Result<(), String> {
568 Ok(())
569 }
570 fn references(
571 &mut self,
572 _u: &Uri,
573 _p: Position,
574 _s: &str,
575 ) -> Result<Vec<Location>, String> {
576 Ok(vec![])
577 }
578 fn definition(
579 &mut self,
580 _u: &Uri,
581 _p: Position,
582 ) -> Result<GotoDefinitionResponse, String> {
583 Ok(GotoDefinitionResponse::Array(vec![]))
584 }
585 fn implementations(
586 &mut self,
587 _u: &Uri,
588 _p: Position,
589 _s: &str,
590 ) -> Result<Vec<Location>, String> {
591 Ok(vec![])
592 }
593 fn rename(
594 &mut self,
595 _u: &Uri,
596 _p: Position,
597 _n: &str,
598 ) -> Result<Option<WorkspaceEdit>, String> {
599 Ok(None)
600 }
601 }
602 let q = InlineQuery {
603 abs_path: "/a".into(),
604 rel_path: "a".into(),
605 src_range: TextRange0Based {
606 start_line: 0,
607 start_char: 0,
608 end_line: 0,
609 end_char: 0,
610 },
611 keep_definition: false,
612 };
613 assert!(
614 Bare2
615 .inline_preview(&q)
616 .unwrap_err()
617 .contains("BACKEND_REQUIRED")
618 );
619 assert!(
620 Bare2
621 .inline_apply(&InlineApply { query: q.clone() })
622 .unwrap_err()
623 .contains("BACKEND_REQUIRED")
624 );
625 let rq = ReformatQuery {
626 abs_path: "/a".into(),
627 rel_path: "a".into(),
628 scope: ReformatScope::File,
629 optimize_imports: false,
630 };
631 assert!(
632 Bare2
633 .reformat(&rq)
634 .unwrap_err()
635 .contains("BACKEND_REQUIRED")
636 );
637 }
638
639 #[test]
640 fn headless_rename_default_is_backend_required() {
641 let mut be = crate::lsp::edit_apply::HeadlessBackend;
643 let q = RenameQuery {
644 abs_path: "/x".into(),
645 rel_path: "x".into(),
646 target_range: TextRange0Based {
647 start_line: 0,
648 start_char: 0,
649 end_line: 0,
650 end_char: 1,
651 },
652 new_name: "y".into(),
653 search_comments: false,
654 search_text_occurrences: false,
655 };
656 let err = be.rename_preview(&q).unwrap_err();
657 assert!(err.starts_with("BACKEND_REQUIRED"), "got: {err}");
658 let a = RenameApply {
659 abs_path: "/x".into(),
660 rel_path: "x".into(),
661 target_range: q.target_range,
662 new_name: "y".into(),
663 force: false,
664 };
665 assert!(
666 be.rename_apply(&a)
667 .unwrap_err()
668 .starts_with("BACKEND_REQUIRED")
669 );
670 }
671
672 #[test]
673 fn headless_move_and_safe_delete_default_is_backend_required() {
674 struct Bare;
677 impl LspBackend for Bare {
678 fn open_file(&mut self, _u: &lsp_types::Uri, _l: &str, _t: &str) -> Result<(), String> {
679 Ok(())
680 }
681 fn references(
682 &mut self,
683 _u: &lsp_types::Uri,
684 _p: lsp_types::Position,
685 _s: &str,
686 ) -> Result<Vec<lsp_types::Location>, String> {
687 Ok(vec![])
688 }
689 fn definition(
690 &mut self,
691 _u: &lsp_types::Uri,
692 _p: lsp_types::Position,
693 ) -> Result<lsp_types::GotoDefinitionResponse, String> {
694 Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
695 }
696 fn implementations(
697 &mut self,
698 _u: &lsp_types::Uri,
699 _p: lsp_types::Position,
700 _s: &str,
701 ) -> Result<Vec<lsp_types::Location>, String> {
702 Ok(vec![])
703 }
704 fn rename(
705 &mut self,
706 _u: &lsp_types::Uri,
707 _p: lsp_types::Position,
708 _n: &str,
709 ) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
710 Ok(None)
711 }
712 }
713 let mut b = Bare;
714 let mq = MoveQuery {
715 abs_path: "/p/a.kt".into(),
716 rel_path: "a.kt".into(),
717 src_range: TextRange0Based {
718 start_line: 0,
719 start_char: 0,
720 end_line: 0,
721 end_char: 1,
722 },
723 target: MoveTarget::Path {
724 abs_path: "/p/x".into(),
725 rel_path: "x".into(),
726 },
727 };
728 assert!(
729 b.move_preview(&mq)
730 .unwrap_err()
731 .starts_with("BACKEND_REQUIRED")
732 );
733 assert!(
734 b.move_apply(&MoveApply {
735 query: mq,
736 force: false
737 })
738 .unwrap_err()
739 .starts_with("BACKEND_REQUIRED")
740 );
741 let sq = SafeDeleteQuery {
742 abs_path: "/p/a.kt".into(),
743 rel_path: "a.kt".into(),
744 src_range: TextRange0Based {
745 start_line: 0,
746 start_char: 0,
747 end_line: 0,
748 end_char: 1,
749 },
750 };
751 assert!(
752 b.safe_delete_preview(&sq)
753 .unwrap_err()
754 .starts_with("BACKEND_REQUIRED")
755 );
756 assert!(
757 b.safe_delete_apply(&SafeDeleteApply {
758 query: sq,
759 force: false,
760 propagate: false
761 })
762 .unwrap_err()
763 .starts_with("BACKEND_REQUIRED")
764 );
765 }
766}