1use std::collections::HashMap;
4use std::path::Path;
5
6use ignore::WalkBuilder;
7use serde::{Deserialize, Serialize};
8
9use super::routing::{ServerId, ToolKind};
10
11pub const DEFAULT_HEURISTICS_MAX_DEPTH: usize = 10;
13
14const EXCLUDED_DIRECTORIES: &[&str] = &[
17 "node_modules",
18 "target",
19 ".git",
20 "__pycache__",
21 ".venv",
22 "venv",
23 ".tox",
24 ".mypy_cache",
25 ".pytest_cache",
26 "build",
27 "dist",
28 ".cargo",
29 ".rustup",
30 "vendor",
31 "coverage",
32 ".next",
33 ".nuxt",
34];
35
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41#[serde(deny_unknown_fields)]
42pub struct ServerHeuristics {
43 #[serde(default)]
51 pub project_markers: Vec<String>,
52}
53
54impl ServerHeuristics {
55 #[must_use]
57 pub fn with_markers<I, S>(markers: I) -> Self
58 where
59 I: IntoIterator<Item = S>,
60 S: Into<String>,
61 {
62 Self {
63 project_markers: markers.into_iter().map(Into::into).collect(),
64 }
65 }
66
67 #[must_use]
73 pub fn is_applicable(&self, workspace_root: &Path) -> bool {
74 if self.project_markers.is_empty() {
75 return true;
76 }
77 self.project_markers
78 .iter()
79 .any(|marker| workspace_root.join(marker).exists())
80 }
81
82 #[must_use]
96 pub fn is_applicable_recursive(&self, workspace_root: &Path, max_depth: Option<usize>) -> bool {
97 if self.project_markers.is_empty() {
98 return true;
99 }
100
101 if self.is_applicable(workspace_root) {
103 return true;
104 }
105
106 let depth = max_depth.unwrap_or(DEFAULT_HEURISTICS_MAX_DEPTH);
107 self.find_any_marker_recursive(workspace_root, depth)
108 }
109
110 fn find_any_marker_recursive(&self, workspace_root: &Path, max_depth: usize) -> bool {
112 let mut builder = WalkBuilder::new(workspace_root);
113 builder
114 .max_depth(Some(max_depth))
115 .hidden(false)
116 .git_ignore(true)
117 .git_global(false)
118 .git_exclude(false)
119 .follow_links(false)
120 .standard_filters(false)
121 .filter_entry(|entry| {
122 if entry.file_type().is_some_and(|ft| ft.is_dir())
124 && let Some(name) = entry.file_name().to_str()
125 && EXCLUDED_DIRECTORIES.contains(&name)
126 {
127 return false;
128 }
129 true
130 });
131
132 for entry in builder.build().flatten() {
133 let path = entry.path();
134
135 if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
137 && self.project_markers.iter().any(|m| m == file_name)
138 {
139 return true;
140 }
141 }
142
143 false
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct LspServerConfig {
151 pub language_id: String,
153
154 pub command: String,
156
157 #[serde(default)]
159 pub args: Vec<String>,
160
161 #[serde(default)]
163 pub env: HashMap<String, String>,
164
165 #[serde(default)]
167 pub file_patterns: Vec<String>,
168
169 #[serde(default)]
171 pub initialization_options: Option<serde_json::Value>,
172
173 #[serde(default = "default_timeout")]
175 pub timeout_seconds: u64,
176
177 #[serde(default)]
180 pub heuristics: Option<ServerHeuristics>,
181
182 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub name: Option<String>,
191
192 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub handles: Option<Vec<ToolKind>>,
199}
200
201const fn default_timeout() -> u64 {
202 30
203}
204
205impl LspServerConfig {
206 #[must_use]
215 pub fn should_spawn(&self, workspace_root: &Path, max_depth: Option<usize>) -> bool {
216 self.heuristics
217 .as_ref()
218 .is_none_or(|h| h.is_applicable_recursive(workspace_root, max_depth))
219 }
220
221 #[must_use]
227 pub fn id(&self) -> ServerId {
228 self.name
229 .clone()
230 .map_or_else(|| ServerId::from(self.language_id.clone()), ServerId::from)
231 }
232
233 #[must_use]
235 pub fn rust_analyzer() -> Self {
236 Self {
237 language_id: "rust".to_string(),
238 command: "rust-analyzer".to_string(),
239 args: vec![],
240 env: HashMap::new(),
241 file_patterns: vec!["**/*.rs".to_string()],
242 initialization_options: None,
243 timeout_seconds: default_timeout(),
244 heuristics: Some(ServerHeuristics::with_markers([
245 "Cargo.toml",
246 "rust-toolchain.toml",
247 ])),
248 name: None,
249 handles: None,
250 }
251 }
252
253 #[must_use]
255 pub fn pyright() -> Self {
256 Self {
257 language_id: "python".to_string(),
258 command: "pyright-langserver".to_string(),
259 args: vec!["--stdio".to_string()],
260 env: HashMap::new(),
261 file_patterns: vec!["**/*.py".to_string()],
262 initialization_options: None,
263 timeout_seconds: default_timeout(),
264 heuristics: Some(ServerHeuristics::with_markers([
265 "pyproject.toml",
266 "setup.py",
267 "requirements.txt",
268 "pyrightconfig.json",
269 ])),
270 name: None,
271 handles: None,
272 }
273 }
274
275 #[must_use]
277 pub fn typescript() -> Self {
278 Self {
279 language_id: "typescript".to_string(),
280 command: "typescript-language-server".to_string(),
281 args: vec!["--stdio".to_string()],
282 env: HashMap::new(),
283 file_patterns: vec!["**/*.ts".to_string(), "**/*.tsx".to_string()],
284 initialization_options: None,
285 timeout_seconds: default_timeout(),
286 heuristics: Some(ServerHeuristics::with_markers([
287 "package.json",
288 "tsconfig.json",
289 "jsconfig.json",
290 ])),
291 name: None,
292 handles: None,
293 }
294 }
295
296 #[must_use]
298 pub fn gopls() -> Self {
299 Self {
300 language_id: "go".to_string(),
301 command: "gopls".to_string(),
302 args: vec!["serve".to_string()],
303 env: HashMap::new(),
304 file_patterns: vec!["**/*.go".to_string()],
305 initialization_options: None,
306 timeout_seconds: default_timeout(),
307 heuristics: Some(ServerHeuristics::with_markers(["go.mod", "go.sum"])),
308 name: None,
309 handles: None,
310 }
311 }
312
313 #[must_use]
315 pub fn clangd() -> Self {
316 Self {
317 language_id: "cpp".to_string(),
318 command: "clangd".to_string(),
319 args: vec![],
320 env: HashMap::new(),
321 file_patterns: vec![
322 "**/*.c".to_string(),
323 "**/*.cpp".to_string(),
324 "**/*.h".to_string(),
325 "**/*.hpp".to_string(),
326 ],
327 initialization_options: None,
328 timeout_seconds: default_timeout(),
329 heuristics: Some(ServerHeuristics::with_markers([
330 "CMakeLists.txt",
331 "compile_commands.json",
332 "Makefile",
333 ".clangd",
334 ])),
335 name: None,
336 handles: None,
337 }
338 }
339
340 #[must_use]
342 pub fn zls() -> Self {
343 Self {
344 language_id: "zig".to_string(),
345 command: "zls".to_string(),
346 args: vec![],
347 env: HashMap::new(),
348 file_patterns: vec!["**/*.zig".to_string()],
349 initialization_options: None,
350 timeout_seconds: default_timeout(),
351 heuristics: Some(ServerHeuristics::with_markers([
352 "build.zig",
353 "build.zig.zon",
354 ])),
355 name: None,
356 handles: None,
357 }
358 }
359}
360
361#[cfg(test)]
362#[allow(clippy::unwrap_used)]
363mod tests {
364 use tempfile::TempDir;
365
366 use super::*;
367
368 #[test]
369 fn test_rust_analyzer_defaults() {
370 let config = LspServerConfig::rust_analyzer();
371
372 assert_eq!(config.language_id, "rust");
373 assert_eq!(config.command, "rust-analyzer");
374 assert!(config.args.is_empty());
375 assert!(config.env.is_empty());
376 assert_eq!(config.file_patterns, vec!["**/*.rs"]);
377 assert!(config.initialization_options.is_none());
378 assert_eq!(config.timeout_seconds, 30);
379 }
380
381 #[test]
382 fn test_pyright_defaults() {
383 let config = LspServerConfig::pyright();
384
385 assert_eq!(config.language_id, "python");
386 assert_eq!(config.command, "pyright-langserver");
387 assert_eq!(config.args, vec!["--stdio"]);
388 assert!(config.env.is_empty());
389 assert_eq!(config.file_patterns, vec!["**/*.py"]);
390 assert!(config.initialization_options.is_none());
391 assert_eq!(config.timeout_seconds, 30);
392 }
393
394 #[test]
395 fn test_typescript_defaults() {
396 let config = LspServerConfig::typescript();
397
398 assert_eq!(config.language_id, "typescript");
399 assert_eq!(config.command, "typescript-language-server");
400 assert_eq!(config.args, vec!["--stdio"]);
401 assert!(config.env.is_empty());
402 assert_eq!(config.file_patterns, vec!["**/*.ts", "**/*.tsx"]);
403 assert!(config.initialization_options.is_none());
404 assert_eq!(config.timeout_seconds, 30);
405 }
406
407 #[test]
408 fn test_default_timeout() {
409 assert_eq!(default_timeout(), 30);
410 }
411
412 #[test]
413 fn test_custom_config() {
414 let mut env = HashMap::new();
415 env.insert("RUST_LOG".to_string(), "debug".to_string());
416
417 let config = LspServerConfig {
418 language_id: "custom".to_string(),
419 command: "custom-lsp".to_string(),
420 args: vec!["--flag".to_string()],
421 env: env.clone(),
422 file_patterns: vec!["**/*.custom".to_string()],
423 initialization_options: Some(serde_json::json!({"key": "value"})),
424 timeout_seconds: 60,
425 heuristics: None,
426 name: None,
427 handles: None,
428 };
429
430 assert_eq!(config.language_id, "custom");
431 assert_eq!(config.command, "custom-lsp");
432 assert_eq!(config.args, vec!["--flag"]);
433 assert_eq!(config.env.get("RUST_LOG"), Some(&"debug".to_string()));
434 assert_eq!(config.file_patterns, vec!["**/*.custom"]);
435 assert!(config.initialization_options.is_some());
436 assert_eq!(config.timeout_seconds, 60);
437 }
438
439 #[test]
440 fn test_serde_roundtrip() {
441 let original = LspServerConfig::rust_analyzer();
442
443 let serialized = serde_json::to_string(&original).unwrap();
444 let deserialized: LspServerConfig = serde_json::from_str(&serialized).unwrap();
445
446 assert_eq!(deserialized.language_id, original.language_id);
447 assert_eq!(deserialized.command, original.command);
448 assert_eq!(deserialized.args, original.args);
449 assert_eq!(deserialized.timeout_seconds, original.timeout_seconds);
450 }
451
452 #[test]
453 fn test_clone() {
454 let config = LspServerConfig::rust_analyzer();
455 let cloned = config.clone();
456
457 assert_eq!(cloned.language_id, config.language_id);
458 assert_eq!(cloned.command, config.command);
459 assert_eq!(cloned.timeout_seconds, config.timeout_seconds);
460 }
461
462 #[test]
463 fn test_empty_env() {
464 let config = LspServerConfig::rust_analyzer();
465 assert!(config.env.is_empty());
466 }
467
468 #[test]
469 fn test_multiple_file_patterns() {
470 let config = LspServerConfig::typescript();
471 assert_eq!(config.file_patterns.len(), 2);
472 assert!(config.file_patterns.contains(&"**/*.ts".to_string()));
473 assert!(config.file_patterns.contains(&"**/*.tsx".to_string()));
474 }
475
476 #[test]
477 fn test_initialization_options_none_by_default() {
478 let configs = vec![
479 LspServerConfig::rust_analyzer(),
480 LspServerConfig::pyright(),
481 LspServerConfig::typescript(),
482 ];
483
484 for config in configs {
485 assert!(config.initialization_options.is_none());
486 }
487 }
488
489 #[test]
491 fn test_heuristics_empty_always_applicable() {
492 let heuristics = ServerHeuristics::default();
493 let tmp = TempDir::new().unwrap();
494 assert!(heuristics.is_applicable(tmp.path()));
495 }
496
497 #[test]
498 fn test_heuristics_marker_present() {
499 let tmp = TempDir::new().unwrap();
500 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
501
502 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
503 assert!(heuristics.is_applicable(tmp.path()));
504 }
505
506 #[test]
507 fn test_heuristics_marker_absent() {
508 let tmp = TempDir::new().unwrap();
509 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
510 assert!(!heuristics.is_applicable(tmp.path()));
511 }
512
513 #[test]
514 fn test_heuristics_any_marker_matches() {
515 let tmp = TempDir::new().unwrap();
516 std::fs::write(tmp.path().join("setup.py"), "").unwrap();
517
518 let heuristics =
519 ServerHeuristics::with_markers(["pyproject.toml", "setup.py", "requirements.txt"]);
520 assert!(heuristics.is_applicable(tmp.path()));
521 }
522
523 #[test]
524 fn test_should_spawn_without_heuristics() {
525 let config = LspServerConfig {
526 language_id: "test".to_string(),
527 command: "test-lsp".to_string(),
528 args: vec![],
529 env: HashMap::new(),
530 file_patterns: vec![],
531 initialization_options: None,
532 timeout_seconds: 30,
533 heuristics: None,
534 name: None,
535 handles: None,
536 };
537
538 let tmp = TempDir::new().unwrap();
539 assert!(config.should_spawn(tmp.path(), None));
540 }
541
542 #[test]
543 fn test_should_spawn_with_heuristics() {
544 let tmp = TempDir::new().unwrap();
545 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
546
547 let config = LspServerConfig::rust_analyzer();
548 assert!(config.should_spawn(tmp.path(), None));
549 }
550
551 #[test]
552 fn test_should_not_spawn_without_markers() {
553 let tmp = TempDir::new().unwrap();
554 let config = LspServerConfig::rust_analyzer();
555 assert!(!config.should_spawn(tmp.path(), None));
556 }
557
558 #[test]
559 fn test_heuristics_serde_roundtrip() {
560 let heuristics = ServerHeuristics::with_markers(["Cargo.toml", "rust-toolchain.toml"]);
561 let json = serde_json::to_string(&heuristics).unwrap();
562 let deserialized: ServerHeuristics = serde_json::from_str(&json).unwrap();
563 assert_eq!(deserialized.project_markers, heuristics.project_markers);
564 }
565
566 #[test]
567 fn test_default_rust_analyzer_heuristics() {
568 let config = LspServerConfig::rust_analyzer();
569 assert!(config.heuristics.is_some());
570 let markers = &config.heuristics.unwrap().project_markers;
571 assert!(markers.contains(&"Cargo.toml".to_string()));
572 }
573
574 #[test]
575 fn test_gopls_defaults() {
576 let config = LspServerConfig::gopls();
577
578 assert_eq!(config.language_id, "go");
579 assert_eq!(config.command, "gopls");
580 assert_eq!(config.args, vec!["serve"]);
581 assert!(config.heuristics.is_some());
582 let markers = &config.heuristics.unwrap().project_markers;
583 assert!(markers.contains(&"go.mod".to_string()));
584 assert!(markers.contains(&"go.sum".to_string()));
585 }
586
587 #[test]
588 fn test_clangd_defaults() {
589 let config = LspServerConfig::clangd();
590
591 assert_eq!(config.language_id, "cpp");
592 assert_eq!(config.command, "clangd");
593 assert!(config.args.is_empty());
594 assert!(config.heuristics.is_some());
595 let markers = &config.heuristics.unwrap().project_markers;
596 assert!(markers.contains(&"CMakeLists.txt".to_string()));
597 assert!(markers.contains(&"compile_commands.json".to_string()));
598 }
599
600 #[test]
601 fn test_zls_defaults() {
602 let config = LspServerConfig::zls();
603
604 assert_eq!(config.language_id, "zig");
605 assert_eq!(config.command, "zls");
606 assert!(config.args.is_empty());
607 assert!(config.heuristics.is_some());
608 let markers = &config.heuristics.unwrap().project_markers;
609 assert!(markers.contains(&"build.zig".to_string()));
610 assert!(markers.contains(&"build.zig.zon".to_string()));
611 }
612
613 #[test]
615 fn test_recursive_empty_markers_always_applicable() {
616 let heuristics = ServerHeuristics::default();
617 let tmp = TempDir::new().unwrap();
618 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
619 }
620
621 #[test]
622 fn test_recursive_marker_at_root() {
623 let tmp = TempDir::new().unwrap();
624 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
625
626 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
627 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
628 }
629
630 #[test]
631 fn test_recursive_nested_python_project() {
632 let tmp = TempDir::new().unwrap();
633 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
635 let python_dir = tmp.path().join("python");
637 std::fs::create_dir(&python_dir).unwrap();
638 std::fs::write(python_dir.join("pyproject.toml"), "").unwrap();
639
640 let heuristics = ServerHeuristics::with_markers(["pyproject.toml", "setup.py"]);
641 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
642 }
643
644 #[test]
645 fn test_recursive_deeply_nested_marker() {
646 let tmp = TempDir::new().unwrap();
647 let deep_path = tmp.path().join("level1").join("level2").join("level3");
649 std::fs::create_dir_all(&deep_path).unwrap();
650 std::fs::write(deep_path.join("go.mod"), "").unwrap();
651
652 let heuristics = ServerHeuristics::with_markers(["go.mod"]);
653 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
654 }
655
656 #[test]
657 fn test_recursive_no_marker_found() {
658 let tmp = TempDir::new().unwrap();
659 std::fs::create_dir(tmp.path().join("src")).unwrap();
660 std::fs::write(tmp.path().join("src").join("main.rs"), "").unwrap();
661
662 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
663 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
664 }
665
666 #[test]
667 fn test_recursive_max_depth_respected() {
668 let tmp = TempDir::new().unwrap();
669 let deep_path = tmp.path().join("a").join("b").join("c").join("d").join("e");
671 std::fs::create_dir_all(&deep_path).unwrap();
672 std::fs::write(deep_path.join("Cargo.toml"), "").unwrap();
673
674 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
675 assert!(!heuristics.is_applicable_recursive(tmp.path(), Some(3)));
677 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
679 }
680
681 #[test]
682 fn test_recursive_excludes_node_modules() {
683 let tmp = TempDir::new().unwrap();
684 let node_modules = tmp.path().join("node_modules").join("some-package");
686 std::fs::create_dir_all(&node_modules).unwrap();
687 std::fs::write(node_modules.join("package.json"), "").unwrap();
688
689 let heuristics = ServerHeuristics::with_markers(["package.json"]);
690 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
691 }
692
693 #[test]
694 fn test_recursive_excludes_target_directory() {
695 let tmp = TempDir::new().unwrap();
696 let target = tmp.path().join("target").join("debug");
698 std::fs::create_dir_all(&target).unwrap();
699 std::fs::write(target.join("Cargo.toml"), "").unwrap();
700
701 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
702 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
703 }
704
705 #[test]
706 fn test_recursive_excludes_git_directory() {
707 let tmp = TempDir::new().unwrap();
708 let git_dir = tmp.path().join(".git").join("hooks");
709 std::fs::create_dir_all(&git_dir).unwrap();
710 std::fs::write(git_dir.join("Cargo.toml"), "").unwrap();
711
712 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
713 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
714 }
715
716 #[test]
717 fn test_recursive_excludes_pycache() {
718 let tmp = TempDir::new().unwrap();
719 let pycache = tmp.path().join("__pycache__");
720 std::fs::create_dir_all(&pycache).unwrap();
721 std::fs::write(pycache.join("pyproject.toml"), "").unwrap();
722
723 let heuristics = ServerHeuristics::with_markers(["pyproject.toml"]);
724 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
725 }
726
727 #[test]
728 fn test_recursive_excludes_venv() {
729 let tmp = TempDir::new().unwrap();
730 let venv = tmp.path().join(".venv").join("lib");
731 std::fs::create_dir_all(&venv).unwrap();
732 std::fs::write(venv.join("setup.py"), "").unwrap();
733
734 let heuristics = ServerHeuristics::with_markers(["setup.py"]);
735 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
736 }
737
738 #[test]
739 fn test_recursive_finds_marker_outside_excluded() {
740 let tmp = TempDir::new().unwrap();
741 let node_modules = tmp.path().join("node_modules");
743 std::fs::create_dir_all(&node_modules).unwrap();
744 std::fs::write(node_modules.join("package.json"), "").unwrap();
745 let src = tmp.path().join("src");
747 std::fs::create_dir_all(&src).unwrap();
748 std::fs::write(src.join("package.json"), "").unwrap();
749
750 let heuristics = ServerHeuristics::with_markers(["package.json"]);
751 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
752 }
753
754 #[test]
755 fn test_recursive_monorepo_structure() {
756 let tmp = TempDir::new().unwrap();
757 let rust_pkg = tmp.path().join("packages").join("rust-lib");
759 let python_pkg = tmp.path().join("packages").join("python-bindings");
760 let ts_pkg = tmp.path().join("packages").join("typescript-client");
761
762 std::fs::create_dir_all(&rust_pkg).unwrap();
763 std::fs::create_dir_all(&python_pkg).unwrap();
764 std::fs::create_dir_all(&ts_pkg).unwrap();
765
766 std::fs::write(rust_pkg.join("Cargo.toml"), "").unwrap();
767 std::fs::write(python_pkg.join("pyproject.toml"), "").unwrap();
768 std::fs::write(ts_pkg.join("package.json"), "").unwrap();
769
770 let rust_heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
772 let python_heuristics = ServerHeuristics::with_markers(["pyproject.toml"]);
773 let ts_heuristics = ServerHeuristics::with_markers(["package.json"]);
774
775 assert!(rust_heuristics.is_applicable_recursive(tmp.path(), None));
776 assert!(python_heuristics.is_applicable_recursive(tmp.path(), None));
777 assert!(ts_heuristics.is_applicable_recursive(tmp.path(), None));
778 }
779
780 #[test]
781 fn test_should_spawn_recursive() {
782 let tmp = TempDir::new().unwrap();
783 let python_dir = tmp.path().join("bindings").join("python");
785 std::fs::create_dir_all(&python_dir).unwrap();
786 std::fs::write(python_dir.join("pyproject.toml"), "").unwrap();
787
788 let config = LspServerConfig::pyright();
789 assert!(config.should_spawn(tmp.path(), None));
790 }
791
792 #[test]
793 fn test_should_spawn_with_custom_max_depth() {
794 let tmp = TempDir::new().unwrap();
795 let deep_path = tmp.path().join("a").join("b").join("c").join("d");
796 std::fs::create_dir_all(&deep_path).unwrap();
797 std::fs::write(deep_path.join("Cargo.toml"), "").unwrap();
798
799 let config = LspServerConfig::rust_analyzer();
800 assert!(!config.should_spawn(tmp.path(), Some(2)));
802 assert!(config.should_spawn(tmp.path(), None));
804 }
805
806 #[test]
807 fn test_default_heuristics_max_depth() {
808 assert_eq!(DEFAULT_HEURISTICS_MAX_DEPTH, 10);
809 }
810
811 #[test]
812 fn test_excluded_directories_constant() {
813 assert!(EXCLUDED_DIRECTORIES.contains(&"node_modules"));
814 assert!(EXCLUDED_DIRECTORIES.contains(&"target"));
815 assert!(EXCLUDED_DIRECTORIES.contains(&".git"));
816 assert!(EXCLUDED_DIRECTORIES.contains(&"__pycache__"));
817 assert!(EXCLUDED_DIRECTORIES.contains(&".venv"));
818 }
819}