Skip to main content

kindly_guard_server/setup/
mcp_detector.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum IdeType {
9    ClaudeDesktop,
10    ClaudeCode,
11    VsCode,
12    Cursor,
13    Zed,
14    Neovim,
15    Unknown,
16}
17
18impl IdeType {
19    pub fn as_str(&self) -> &str {
20        match self {
21            Self::ClaudeDesktop => "Claude Desktop",
22            Self::ClaudeCode => "Claude Code",
23            Self::VsCode => "VS Code",
24            Self::Cursor => "Cursor",
25            Self::Zed => "Zed",
26            Self::Neovim => "Neovim",
27            Self::Unknown => "Unknown",
28        }
29    }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum ConfigFormat {
34    Json,
35    JsonLocal,
36    Toml,
37    Yaml,
38}
39
40impl ConfigFormat {
41    pub fn extension(&self) -> &str {
42        match self {
43            Self::Json => "json",
44            Self::JsonLocal => "json.local",
45            Self::Toml => "toml",
46            Self::Yaml => "yaml",
47        }
48    }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ConfigLocation {
53    pub path: PathBuf,
54    pub format: ConfigFormat,
55    pub exists: bool,
56    pub ide: IdeType,
57}
58
59impl ConfigLocation {
60    fn new(path: PathBuf, format: ConfigFormat, ide: IdeType) -> Self {
61        let exists = path.exists();
62        Self {
63            path,
64            format,
65            exists,
66            ide,
67        }
68    }
69}
70
71pub struct McpDetector {
72    platform: Platform,
73}
74
75#[derive(Debug, Clone, Copy)]
76enum Platform {
77    Windows,
78    MacOs,
79    Linux,
80}
81
82impl McpDetector {
83    pub fn new() -> Self {
84        let platform = if cfg!(target_os = "windows") {
85            Platform::Windows
86        } else if cfg!(target_os = "macos") {
87            Platform::MacOs
88        } else {
89            Platform::Linux
90        };
91
92        Self { platform }
93    }
94
95    /// Detect all possible MCP configuration locations
96    pub fn detect_all(&self) -> Result<Vec<ConfigLocation>> {
97        let mut locations = Vec::new();
98
99        // Claude Desktop configs
100        locations.extend(self.get_claude_desktop_configs()?);
101
102        // Claude Code configs
103        locations.extend(self.get_claude_code_configs()?);
104
105        // VS Code configs
106        locations.extend(self.get_vscode_configs()?);
107
108        // Cursor configs
109        locations.extend(self.get_cursor_configs()?);
110
111        // Zed configs
112        locations.extend(self.get_zed_configs()?);
113
114        // Neovim configs
115        locations.extend(self.get_neovim_configs()?);
116
117        // Global MCP configs
118        locations.extend(self.get_global_mcp_configs()?);
119
120        Ok(locations)
121    }
122
123    /// Detect which IDE/terminal is currently running
124    pub fn detect_active_ide(&self) -> Result<IdeType> {
125        // Check environment variables first
126        if env::var("CLAUDE_CODE").is_ok() {
127            return Ok(IdeType::ClaudeCode);
128        }
129
130        if env::var("CLAUDE_DESKTOP").is_ok() {
131            return Ok(IdeType::ClaudeDesktop);
132        }
133
134        if env::var("VSCODE_PID").is_ok() {
135            return Ok(IdeType::VsCode);
136        }
137
138        if env::var("CURSOR_PID").is_ok() {
139            return Ok(IdeType::Cursor);
140        }
141
142        if env::var("NVIM").is_ok() || env::var("NVIM_LISTEN_ADDRESS").is_ok() {
143            return Ok(IdeType::Neovim);
144        }
145
146        // Check running processes
147        match self.platform {
148            Platform::Windows => self.detect_windows_processes(),
149            Platform::MacOs => self.detect_macos_processes(),
150            Platform::Linux => self.detect_linux_processes(),
151        }
152    }
153
154    /// Get configuration location for a specific IDE
155    pub fn get_config_location(&self, ide: IdeType) -> Result<ConfigLocation> {
156        let path = self.get_config_path(ide)?;
157        let format = ConfigFormat::Json; // Default to JSON for now
158        Ok(ConfigLocation::new(path, format, ide))
159    }
160
161    /// Get configuration path for a specific IDE
162    pub fn get_config_path(&self, ide: IdeType) -> Result<PathBuf> {
163        match ide {
164            IdeType::ClaudeDesktop => self.get_claude_desktop_config_path(),
165            IdeType::ClaudeCode => self.get_claude_code_config_path(),
166            IdeType::VsCode => self.get_vscode_config_path(),
167            IdeType::Cursor => self.get_cursor_config_path(),
168            IdeType::Zed => self.get_zed_config_path(),
169            IdeType::Neovim => self.get_neovim_config_path(),
170            IdeType::Unknown => Err(anyhow::anyhow!("Cannot get config path for unknown IDE")),
171        }
172    }
173
174    /// Detect Claude-specific configurations
175    pub fn detect_claude_configs(&self) -> Result<Vec<ConfigLocation>> {
176        let mut configs = Vec::new();
177        configs.extend(self.get_claude_desktop_configs()?);
178        configs.extend(self.get_claude_code_configs()?);
179        Ok(configs)
180    }
181
182    // Platform-specific home directory helpers
183    fn get_home_dir(&self) -> Result<PathBuf> {
184        match self.platform {
185            Platform::Windows => env::var("USERPROFILE")
186                .or_else(|_| {
187                    env::var("HOMEDRIVE").and_then(|drive| {
188                        env::var("HOMEPATH").map(|path| format!("{}{}", drive, path))
189                    })
190                })
191                .map(PathBuf::from)
192                .context("Failed to get Windows home directory"),
193            Platform::MacOs | Platform::Linux => env::var("HOME")
194                .map(PathBuf::from)
195                .context("Failed to get Unix home directory"),
196        }
197    }
198
199    fn get_config_dir(&self) -> Result<PathBuf> {
200        match self.platform {
201            Platform::Windows => env::var("APPDATA")
202                .map(PathBuf::from)
203                .context("Failed to get Windows config directory"),
204            Platform::MacOs => {
205                let home = self.get_home_dir()?;
206                Ok(home.join("Library").join("Application Support"))
207            },
208            Platform::Linux => env::var("XDG_CONFIG_HOME")
209                .map(PathBuf::from)
210                .or_else(|_| self.get_home_dir().map(|home| home.join(".config")))
211                .context("Failed to get Linux config directory"),
212        }
213    }
214
215    // Claude Desktop configurations
216    fn get_claude_desktop_configs(&self) -> Result<Vec<ConfigLocation>> {
217        let mut configs = Vec::new();
218
219        match self.platform {
220            Platform::Windows => {
221                let appdata = env::var("APPDATA").map(PathBuf::from)?;
222                configs.push(ConfigLocation::new(
223                    appdata.join("Claude").join("claude_desktop_config.json"),
224                    ConfigFormat::Json,
225                    IdeType::ClaudeDesktop,
226                ));
227            },
228            Platform::MacOs => {
229                let home = self.get_home_dir()?;
230                configs.push(ConfigLocation::new(
231                    home.join("Library")
232                        .join("Application Support")
233                        .join("Claude")
234                        .join("claude_desktop_config.json"),
235                    ConfigFormat::Json,
236                    IdeType::ClaudeDesktop,
237                ));
238            },
239            Platform::Linux => {
240                let config = self.get_config_dir()?;
241                configs.push(ConfigLocation::new(
242                    config.join("claude").join("claude_desktop_config.json"),
243                    ConfigFormat::Json,
244                    IdeType::ClaudeDesktop,
245                ));
246            },
247        }
248
249        Ok(configs)
250    }
251
252    // Claude Code configurations
253    fn get_claude_code_configs(&self) -> Result<Vec<ConfigLocation>> {
254        let mut configs = Vec::new();
255        let home = self.get_home_dir()?;
256
257        // Primary config location
258        configs.push(ConfigLocation::new(
259            home.join(".mcp.json"),
260            ConfigFormat::Json,
261            IdeType::ClaudeCode,
262        ));
263
264        // Local override
265        configs.push(ConfigLocation::new(
266            home.join(".mcp.json.local"),
267            ConfigFormat::JsonLocal,
268            IdeType::ClaudeCode,
269        ));
270
271        // Alternative locations
272        match self.platform {
273            Platform::Windows => {
274                let appdata = env::var("APPDATA").map(PathBuf::from)?;
275                configs.push(ConfigLocation::new(
276                    appdata.join("claude-code").join("mcp.json"),
277                    ConfigFormat::Json,
278                    IdeType::ClaudeCode,
279                ));
280            },
281            Platform::MacOs => {
282                configs.push(ConfigLocation::new(
283                    home.join("Library")
284                        .join("Application Support")
285                        .join("claude-code")
286                        .join("mcp.json"),
287                    ConfigFormat::Json,
288                    IdeType::ClaudeCode,
289                ));
290            },
291            Platform::Linux => {
292                let config = self.get_config_dir()?;
293                configs.push(ConfigLocation::new(
294                    config.join("claude-code").join("mcp.json"),
295                    ConfigFormat::Json,
296                    IdeType::ClaudeCode,
297                ));
298            },
299        }
300
301        Ok(configs)
302    }
303
304    // VS Code configurations
305    fn get_vscode_configs(&self) -> Result<Vec<ConfigLocation>> {
306        let mut configs = Vec::new();
307
308        match self.platform {
309            Platform::Windows => {
310                let appdata = env::var("APPDATA").map(PathBuf::from)?;
311                configs.push(ConfigLocation::new(
312                    appdata.join("Code").join("User").join("mcp.json"),
313                    ConfigFormat::Json,
314                    IdeType::VsCode,
315                ));
316            },
317            Platform::MacOs => {
318                let home = self.get_home_dir()?;
319                configs.push(ConfigLocation::new(
320                    home.join("Library")
321                        .join("Application Support")
322                        .join("Code")
323                        .join("User")
324                        .join("mcp.json"),
325                    ConfigFormat::Json,
326                    IdeType::VsCode,
327                ));
328            },
329            Platform::Linux => {
330                let config = self.get_config_dir()?;
331                configs.push(ConfigLocation::new(
332                    config.join("Code").join("User").join("mcp.json"),
333                    ConfigFormat::Json,
334                    IdeType::VsCode,
335                ));
336            },
337        }
338
339        Ok(configs)
340    }
341
342    // Cursor configurations
343    fn get_cursor_configs(&self) -> Result<Vec<ConfigLocation>> {
344        let mut configs = Vec::new();
345
346        match self.platform {
347            Platform::Windows => {
348                let appdata = env::var("APPDATA").map(PathBuf::from)?;
349                configs.push(ConfigLocation::new(
350                    appdata.join("Cursor").join("User").join("mcp.json"),
351                    ConfigFormat::Json,
352                    IdeType::Cursor,
353                ));
354            },
355            Platform::MacOs => {
356                let home = self.get_home_dir()?;
357                configs.push(ConfigLocation::new(
358                    home.join("Library")
359                        .join("Application Support")
360                        .join("Cursor")
361                        .join("User")
362                        .join("mcp.json"),
363                    ConfigFormat::Json,
364                    IdeType::Cursor,
365                ));
366            },
367            Platform::Linux => {
368                let config = self.get_config_dir()?;
369                configs.push(ConfigLocation::new(
370                    config.join("Cursor").join("User").join("mcp.json"),
371                    ConfigFormat::Json,
372                    IdeType::Cursor,
373                ));
374            },
375        }
376
377        Ok(configs)
378    }
379
380    // Zed configurations
381    fn get_zed_configs(&self) -> Result<Vec<ConfigLocation>> {
382        let mut configs = Vec::new();
383
384        match self.platform {
385            Platform::Windows => {
386                let appdata = env::var("APPDATA").map(PathBuf::from)?;
387                configs.push(ConfigLocation::new(
388                    appdata.join("Zed").join("mcp.json"),
389                    ConfigFormat::Json,
390                    IdeType::Zed,
391                ));
392            },
393            Platform::MacOs => {
394                let home = self.get_home_dir()?;
395                configs.push(ConfigLocation::new(
396                    home.join("Library")
397                        .join("Application Support")
398                        .join("Zed")
399                        .join("mcp.json"),
400                    ConfigFormat::Json,
401                    IdeType::Zed,
402                ));
403            },
404            Platform::Linux => {
405                let config_home = env::var("XDG_CONFIG_HOME")
406                    .map(PathBuf::from)
407                    .unwrap_or_else(|_| {
408                        let home = self.get_home_dir().unwrap_or_default();
409                        home.join(".config")
410                    });
411                configs.push(ConfigLocation::new(
412                    config_home.join("zed").join("mcp.json"),
413                    ConfigFormat::Json,
414                    IdeType::Zed,
415                ));
416            },
417        }
418
419        Ok(configs)
420    }
421
422    // Neovim configurations
423    fn get_neovim_configs(&self) -> Result<Vec<ConfigLocation>> {
424        let mut configs = Vec::new();
425        let home = self.get_home_dir()?;
426
427        // Standard Neovim config locations
428        configs.push(ConfigLocation::new(
429            home.join(".config").join("nvim").join("mcp.json"),
430            ConfigFormat::Json,
431            IdeType::Neovim,
432        ));
433
434        configs.push(ConfigLocation::new(
435            home.join(".config").join("nvim").join("mcp.toml"),
436            ConfigFormat::Toml,
437            IdeType::Neovim,
438        ));
439
440        // Legacy location
441        configs.push(ConfigLocation::new(
442            home.join(".nvim").join("mcp.json"),
443            ConfigFormat::Json,
444            IdeType::Neovim,
445        ));
446
447        Ok(configs)
448    }
449
450    // Global MCP configurations
451    fn get_global_mcp_configs(&self) -> Result<Vec<ConfigLocation>> {
452        let mut configs = Vec::new();
453        let home = self.get_home_dir()?;
454
455        // Global config in home directory
456        configs.push(ConfigLocation::new(
457            home.join(".mcp").join("config.json"),
458            ConfigFormat::Json,
459            IdeType::Unknown,
460        ));
461
462        configs.push(ConfigLocation::new(
463            home.join(".mcp").join("config.toml"),
464            ConfigFormat::Toml,
465            IdeType::Unknown,
466        ));
467
468        configs.push(ConfigLocation::new(
469            home.join(".mcp").join("config.yaml"),
470            ConfigFormat::Yaml,
471            IdeType::Unknown,
472        ));
473
474        // System-wide configs
475        match self.platform {
476            Platform::Windows => {
477                let programdata = env::var("PROGRAMDATA")
478                    .map(PathBuf::from)
479                    .unwrap_or_else(|_| PathBuf::from("C:\\ProgramData"));
480                configs.push(ConfigLocation::new(
481                    programdata.join("mcp").join("config.json"),
482                    ConfigFormat::Json,
483                    IdeType::Unknown,
484                ));
485            },
486            Platform::MacOs | Platform::Linux => {
487                configs.push(ConfigLocation::new(
488                    PathBuf::from("/etc/mcp/config.json"),
489                    ConfigFormat::Json,
490                    IdeType::Unknown,
491                ));
492                configs.push(ConfigLocation::new(
493                    PathBuf::from("/etc/mcp/config.toml"),
494                    ConfigFormat::Toml,
495                    IdeType::Unknown,
496                ));
497            },
498        }
499
500        Ok(configs)
501    }
502
503    // Process detection methods
504    fn detect_windows_processes(&self) -> Result<IdeType> {
505        use std::process::Command;
506
507        let output = Command::new("tasklist")
508            .output()
509            .context("Failed to run tasklist")?;
510
511        let processes = String::from_utf8_lossy(&output.stdout);
512
513        if processes.contains("Claude.exe") || processes.contains("claude.exe") {
514            return Ok(IdeType::ClaudeDesktop);
515        }
516
517        if processes.contains("ClaudeCode.exe") || processes.contains("claude-code.exe") {
518            return Ok(IdeType::ClaudeCode);
519        }
520
521        if processes.contains("Code.exe") {
522            return Ok(IdeType::VsCode);
523        }
524
525        if processes.contains("Cursor.exe") {
526            return Ok(IdeType::Cursor);
527        }
528
529        if processes.contains("nvim.exe") || processes.contains("nvim-qt.exe") {
530            return Ok(IdeType::Neovim);
531        }
532
533        Ok(IdeType::Unknown)
534    }
535
536    fn detect_macos_processes(&self) -> Result<IdeType> {
537        use std::process::Command;
538
539        let output = Command::new("ps")
540            .args(&["-ax"])
541            .output()
542            .context("Failed to run ps")?;
543
544        let processes = String::from_utf8_lossy(&output.stdout);
545
546        if processes.contains("Claude.app") || processes.contains("Claude Desktop") {
547            return Ok(IdeType::ClaudeDesktop);
548        }
549
550        if processes.contains("Claude Code") || processes.contains("claude-code") {
551            return Ok(IdeType::ClaudeCode);
552        }
553
554        if processes.contains("Visual Studio Code.app") || processes.contains("Code Helper") {
555            return Ok(IdeType::VsCode);
556        }
557
558        if processes.contains("Cursor.app") || processes.contains("Cursor Helper") {
559            return Ok(IdeType::Cursor);
560        }
561
562        if processes.contains("nvim") {
563            return Ok(IdeType::Neovim);
564        }
565
566        Ok(IdeType::Unknown)
567    }
568
569    fn detect_linux_processes(&self) -> Result<IdeType> {
570        use std::process::Command;
571
572        let output = Command::new("ps")
573            .args(&["aux"])
574            .output()
575            .context("Failed to run ps")?;
576
577        let processes = String::from_utf8_lossy(&output.stdout);
578
579        if processes.contains("claude-desktop") || processes.contains("Claude") {
580            return Ok(IdeType::ClaudeDesktop);
581        }
582
583        if processes.contains("claude-code") {
584            return Ok(IdeType::ClaudeCode);
585        }
586
587        if processes.contains("code") && !processes.contains("claude-code") {
588            return Ok(IdeType::VsCode);
589        }
590
591        if processes.contains("cursor") {
592            return Ok(IdeType::Cursor);
593        }
594
595        if processes.contains("nvim") {
596            return Ok(IdeType::Neovim);
597        }
598
599        Ok(IdeType::Unknown)
600    }
601
602    // Helper methods to get primary config paths
603    fn get_claude_desktop_config_path(&self) -> Result<PathBuf> {
604        match self.platform {
605            Platform::Windows => {
606                let appdata = env::var("APPDATA").map(PathBuf::from)?;
607                Ok(appdata.join("Claude").join("claude_desktop_config.json"))
608            },
609            Platform::MacOs => {
610                let home = self.get_home_dir()?;
611                Ok(home
612                    .join("Library")
613                    .join("Application Support")
614                    .join("Claude")
615                    .join("claude_desktop_config.json"))
616            },
617            Platform::Linux => {
618                let config = self.get_config_dir()?;
619                Ok(config.join("claude").join("claude_desktop_config.json"))
620            },
621        }
622    }
623
624    fn get_claude_code_config_path(&self) -> Result<PathBuf> {
625        let home = self.get_home_dir()?;
626        Ok(home.join(".mcp.json"))
627    }
628
629    fn get_vscode_config_path(&self) -> Result<PathBuf> {
630        match self.platform {
631            Platform::Windows => {
632                let appdata = env::var("APPDATA").map(PathBuf::from)?;
633                Ok(appdata.join("Code").join("User").join("mcp.json"))
634            },
635            Platform::MacOs => {
636                let home = self.get_home_dir()?;
637                Ok(home
638                    .join("Library")
639                    .join("Application Support")
640                    .join("Code")
641                    .join("User")
642                    .join("mcp.json"))
643            },
644            Platform::Linux => {
645                let config = self.get_config_dir()?;
646                Ok(config.join("Code").join("User").join("mcp.json"))
647            },
648        }
649    }
650
651    fn get_cursor_config_path(&self) -> Result<PathBuf> {
652        match self.platform {
653            Platform::Windows => {
654                let appdata = env::var("APPDATA").map(PathBuf::from)?;
655                Ok(appdata.join("Cursor").join("User").join("mcp.json"))
656            },
657            Platform::MacOs => {
658                let home = self.get_home_dir()?;
659                Ok(home
660                    .join("Library")
661                    .join("Application Support")
662                    .join("Cursor")
663                    .join("User")
664                    .join("mcp.json"))
665            },
666            Platform::Linux => {
667                let config = self.get_config_dir()?;
668                Ok(config.join("Cursor").join("User").join("mcp.json"))
669            },
670        }
671    }
672
673    fn get_zed_config_path(&self) -> Result<PathBuf> {
674        match self.platform {
675            Platform::Windows => {
676                let appdata = env::var("APPDATA").map(PathBuf::from)?;
677                Ok(appdata.join("Zed").join("mcp.json"))
678            },
679            Platform::MacOs => {
680                let home = self.get_home_dir()?;
681                Ok(home
682                    .join("Library")
683                    .join("Application Support")
684                    .join("Zed")
685                    .join("mcp.json"))
686            },
687            Platform::Linux => {
688                let config_home = env::var("XDG_CONFIG_HOME")
689                    .map(PathBuf::from)
690                    .unwrap_or_else(|_| {
691                        let home = self.get_home_dir().unwrap_or_default();
692                        home.join(".config")
693                    });
694                Ok(config_home.join("zed").join("mcp.json"))
695            },
696        }
697    }
698
699    fn get_neovim_config_path(&self) -> Result<PathBuf> {
700        let home = self.get_home_dir()?;
701        Ok(home.join(".config").join("nvim").join("mcp.json"))
702    }
703
704    /// Check if a specific MCP server is configured
705    pub fn is_server_configured(&self, server_name: &str) -> Result<bool> {
706        let configs = self.detect_claude_configs()?;
707
708        for config in configs.iter().filter(|c| c.exists) {
709            if self.check_server_in_config(&config.path, server_name)? {
710                return Ok(true);
711            }
712        }
713
714        Ok(false)
715    }
716
717    fn check_server_in_config(&self, path: &Path, server_name: &str) -> Result<bool> {
718        let content = fs::read_to_string(path)?;
719
720        // Basic check - could be enhanced with proper JSON/TOML parsing
721        Ok(content.contains(server_name))
722    }
723
724    /// Get a summary of MCP configuration status
725    pub fn get_status_summary(&self) -> Result<String> {
726        let active_ide = self.detect_active_ide()?;
727        let all_configs = self.detect_all()?;
728        let existing_configs: Vec<_> = all_configs.iter().filter(|c| c.exists).collect();
729
730        let mut summary = format!("MCP Configuration Status\n");
731        summary.push_str(&format!("========================\n"));
732        summary.push_str(&format!("Active IDE: {}\n", active_ide.as_str()));
733        summary.push_str(&format!("Platform: {:?}\n", self.platform));
734        summary.push_str(&format!(
735            "Total config locations checked: {}\n",
736            all_configs.len()
737        ));
738        summary.push_str(&format!(
739            "Existing configurations: {}\n\n",
740            existing_configs.len()
741        ));
742
743        if !existing_configs.is_empty() {
744            summary.push_str("Found configurations:\n");
745            for config in existing_configs {
746                summary.push_str(&format!(
747                    "  - {} ({:?}): {}\n",
748                    config.ide.as_str(),
749                    config.format,
750                    config.path.display()
751                ));
752            }
753        }
754
755        Ok(summary)
756    }
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762
763    #[test]
764    fn test_detector_creation() {
765        let detector = McpDetector::new();
766        assert!(matches!(
767            detector.platform,
768            Platform::Windows | Platform::MacOs | Platform::Linux
769        ));
770    }
771
772    #[test]
773    fn test_config_format_extensions() {
774        assert_eq!(ConfigFormat::Json.extension(), "json");
775        assert_eq!(ConfigFormat::JsonLocal.extension(), "json.local");
776        assert_eq!(ConfigFormat::Toml.extension(), "toml");
777        assert_eq!(ConfigFormat::Yaml.extension(), "yaml");
778    }
779
780    #[test]
781    fn test_ide_type_strings() {
782        assert_eq!(IdeType::ClaudeDesktop.as_str(), "Claude Desktop");
783        assert_eq!(IdeType::ClaudeCode.as_str(), "Claude Code");
784        assert_eq!(IdeType::VsCode.as_str(), "VS Code");
785        assert_eq!(IdeType::Cursor.as_str(), "Cursor");
786        assert_eq!(IdeType::Neovim.as_str(), "Neovim");
787        assert_eq!(IdeType::Unknown.as_str(), "Unknown");
788    }
789}