Skip to main content

rumdl_lib/code_block_tools/
registry.rs

1//! Built-in tool registry with definitions for common formatters and linters.
2//!
3//! This module provides default configurations for popular tools like ruff, prettier,
4//! shellcheck, etc. Users can override these in their configuration.
5
6use super::config::ToolDefinition;
7use std::collections::HashMap;
8use std::sync::LazyLock;
9
10/// Registry of built-in tool definitions.
11pub struct ToolRegistry {
12    /// User-defined tools (override built-ins)
13    user_tools: HashMap<String, ToolDefinition>,
14}
15
16impl ToolRegistry {
17    /// Create a new registry with user-defined tools.
18    pub fn new(user_tools: HashMap<String, ToolDefinition>) -> Self {
19        Self { user_tools }
20    }
21
22    /// Get a tool definition by ID.
23    ///
24    /// Checks user tools first, then falls back to built-in tools.
25    pub fn get(&self, tool_id: &str) -> Option<&ToolDefinition> {
26        self.user_tools.get(tool_id).or_else(|| BUILTIN_TOOLS.get(tool_id))
27    }
28
29    /// Check if a tool ID is valid (either user-defined or built-in).
30    pub fn contains(&self, tool_id: &str) -> bool {
31        self.user_tools.contains_key(tool_id) || BUILTIN_TOOLS.contains_key(tool_id)
32    }
33
34    /// List all available tool IDs.
35    pub fn list_tools(&self) -> Vec<&str> {
36        let mut tools: Vec<&str> = self.user_tools.keys().map(|s| s.as_str()).collect();
37        for key in BUILTIN_TOOLS.keys() {
38            if !self.user_tools.contains_key(*key) {
39                tools.push(key);
40            }
41        }
42        tools.sort();
43        tools
44    }
45}
46
47impl Default for ToolRegistry {
48    fn default() -> Self {
49        Self::new(HashMap::new())
50    }
51}
52
53/// Built-in tool definitions.
54///
55/// These are common formatters and linters that work well with stdin/stdout.
56static BUILTIN_TOOLS: LazyLock<HashMap<&'static str, ToolDefinition>> = LazyLock::new(|| {
57    let mut m = HashMap::new();
58
59    // Python - ruff
60    m.insert(
61        "ruff:check",
62        ToolDefinition {
63            command: vec![
64                "ruff".to_string(),
65                "check".to_string(),
66                "--output-format=concise".to_string(),
67                "--stdin-filename=_.py".to_string(),
68                "-".to_string(),
69            ],
70            stdin: true,
71            stdout: true,
72            lint_args: vec![],
73            format_args: vec![],
74        },
75    );
76
77    m.insert(
78        "ruff:format",
79        ToolDefinition {
80            command: vec![
81                "ruff".to_string(),
82                "format".to_string(),
83                "--stdin-filename=_.py".to_string(),
84                "-".to_string(),
85            ],
86            stdin: true,
87            stdout: true,
88            lint_args: vec![],
89            format_args: vec![],
90        },
91    );
92
93    // Python - black
94    m.insert(
95        "black",
96        ToolDefinition {
97            command: vec!["black".to_string(), "--quiet".to_string(), "-".to_string()],
98            stdin: true,
99            stdout: true,
100            lint_args: vec!["--check".to_string()],
101            format_args: vec![],
102        },
103    );
104
105    // JavaScript/TypeScript - prettier
106    m.insert(
107        "prettier",
108        ToolDefinition {
109            command: vec!["prettier".to_string(), "--stdin-filepath=_.js".to_string()],
110            stdin: true,
111            stdout: true,
112            lint_args: vec!["--check".to_string()],
113            format_args: vec![],
114        },
115    );
116
117    m.insert(
118        "prettier:json",
119        ToolDefinition {
120            command: vec!["prettier".to_string(), "--stdin-filepath=_.json".to_string()],
121            stdin: true,
122            stdout: true,
123            lint_args: vec!["--check".to_string()],
124            format_args: vec![],
125        },
126    );
127
128    m.insert(
129        "prettier:yaml",
130        ToolDefinition {
131            command: vec!["prettier".to_string(), "--stdin-filepath=_.yaml".to_string()],
132            stdin: true,
133            stdout: true,
134            lint_args: vec!["--check".to_string()],
135            format_args: vec![],
136        },
137    );
138
139    m.insert(
140        "prettier:html",
141        ToolDefinition {
142            command: vec!["prettier".to_string(), "--stdin-filepath=_.html".to_string()],
143            stdin: true,
144            stdout: true,
145            lint_args: vec!["--check".to_string()],
146            format_args: vec![],
147        },
148    );
149
150    m.insert(
151        "prettier:css",
152        ToolDefinition {
153            command: vec!["prettier".to_string(), "--stdin-filepath=_.css".to_string()],
154            stdin: true,
155            stdout: true,
156            lint_args: vec!["--check".to_string()],
157            format_args: vec![],
158        },
159    );
160
161    m.insert(
162        "prettier:markdown",
163        ToolDefinition {
164            command: vec!["prettier".to_string(), "--stdin-filepath=_.md".to_string()],
165            stdin: true,
166            stdout: true,
167            lint_args: vec!["--check".to_string()],
168            format_args: vec![],
169        },
170    );
171
172    // JavaScript/TypeScript - eslint (lint only)
173    m.insert(
174        "eslint",
175        ToolDefinition {
176            command: vec![
177                "eslint".to_string(),
178                "--stdin".to_string(),
179                "--stdin-filename=_.js".to_string(),
180            ],
181            stdin: true,
182            stdout: true,
183            lint_args: vec![],
184            format_args: vec!["--fix-dry-run".to_string()],
185        },
186    );
187
188    // Shell - shellcheck (lint only)
189    m.insert(
190        "shellcheck",
191        ToolDefinition {
192            command: vec!["shellcheck".to_string(), "-".to_string()],
193            stdin: true,
194            stdout: true,
195            lint_args: vec![],
196            format_args: vec![],
197        },
198    );
199
200    // Shell - shfmt
201    m.insert(
202        "shfmt",
203        ToolDefinition {
204            command: vec!["shfmt".to_string()],
205            stdin: true,
206            stdout: true,
207            lint_args: vec!["-d".to_string()], // diff mode for lint
208            format_args: vec![],
209        },
210    );
211
212    // Rust - rustfmt
213    m.insert(
214        "rustfmt",
215        ToolDefinition {
216            command: vec!["rustfmt".to_string()],
217            stdin: true,
218            stdout: true,
219            lint_args: vec!["--check".to_string()],
220            format_args: vec![],
221        },
222    );
223
224    // Go - gofmt
225    m.insert(
226        "gofmt",
227        ToolDefinition {
228            command: vec!["gofmt".to_string()],
229            stdin: true,
230            stdout: true,
231            lint_args: vec!["-d".to_string()], // diff mode for lint
232            format_args: vec![],
233        },
234    );
235
236    // Go - goimports
237    m.insert(
238        "goimports",
239        ToolDefinition {
240            command: vec!["goimports".to_string()],
241            stdin: true,
242            stdout: true,
243            lint_args: vec!["-d".to_string()],
244            format_args: vec![],
245        },
246    );
247
248    // C/C++ - clang-format
249    m.insert(
250        "clang-format",
251        ToolDefinition {
252            command: vec!["clang-format".to_string()],
253            stdin: true,
254            stdout: true,
255            lint_args: vec!["--dry-run".to_string(), "--Werror".to_string()],
256            format_args: vec![],
257        },
258    );
259
260    // SQL - sqlfluff
261    m.insert(
262        "sqlfluff:lint",
263        ToolDefinition {
264            command: vec!["sqlfluff".to_string(), "lint".to_string(), "-".to_string()],
265            stdin: true,
266            stdout: true,
267            lint_args: vec![],
268            format_args: vec![],
269        },
270    );
271
272    m.insert(
273        "sqlfluff:fix",
274        ToolDefinition {
275            command: vec!["sqlfluff".to_string(), "fix".to_string(), "-".to_string()],
276            stdin: true,
277            stdout: true,
278            lint_args: vec![],
279            format_args: vec![],
280        },
281    );
282
283    // JSON - jq (format)
284    m.insert(
285        "jq",
286        ToolDefinition {
287            command: vec!["jq".to_string(), ".".to_string()],
288            stdin: true,
289            stdout: true,
290            lint_args: vec![],
291            format_args: vec![],
292        },
293    );
294
295    // YAML - yamlfmt
296    m.insert(
297        "yamlfmt",
298        ToolDefinition {
299            command: vec!["yamlfmt".to_string(), "-".to_string()],
300            stdin: true,
301            stdout: true,
302            lint_args: vec!["-dry".to_string()],
303            format_args: vec![],
304        },
305    );
306
307    // TOML - taplo
308    m.insert(
309        "taplo",
310        ToolDefinition {
311            command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
312            stdin: true,
313            stdout: true,
314            lint_args: vec!["--check".to_string()],
315            format_args: vec![],
316        },
317    );
318
319    // Terraform - terraform fmt
320    m.insert(
321        "terraform-fmt",
322        ToolDefinition {
323            command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
324            stdin: true,
325            stdout: true,
326            lint_args: vec!["-check".to_string()],
327            format_args: vec![],
328        },
329    );
330
331    // Nix - nixfmt
332    m.insert(
333        "nixfmt",
334        ToolDefinition {
335            command: vec!["nixfmt".to_string()],
336            stdin: true,
337            stdout: true,
338            lint_args: vec!["--check".to_string()],
339            format_args: vec![],
340        },
341    );
342
343    // Lua - stylua
344    m.insert(
345        "stylua",
346        ToolDefinition {
347            command: vec!["stylua".to_string(), "-".to_string()],
348            stdin: true,
349            stdout: true,
350            lint_args: vec!["--check".to_string()],
351            format_args: vec![],
352        },
353    );
354
355    // Ruby - rubocop
356    m.insert(
357        "rubocop",
358        ToolDefinition {
359            command: vec!["rubocop".to_string(), "--stdin".to_string(), "_.rb".to_string()],
360            stdin: true,
361            stdout: true,
362            lint_args: vec![],
363            format_args: vec!["--autocorrect".to_string()],
364        },
365    );
366
367    // Haskell - ormolu
368    m.insert(
369        "ormolu",
370        ToolDefinition {
371            command: vec!["ormolu".to_string()],
372            stdin: true,
373            stdout: true,
374            lint_args: vec!["--check-idempotence".to_string()],
375            format_args: vec![],
376        },
377    );
378
379    // Elm - elm-format
380    m.insert(
381        "elm-format",
382        ToolDefinition {
383            command: vec!["elm-format".to_string(), "--stdin".to_string()],
384            stdin: true,
385            stdout: true,
386            lint_args: vec!["--validate".to_string()],
387            format_args: vec![],
388        },
389    );
390
391    // Zig - zig fmt
392    m.insert(
393        "zig-fmt",
394        ToolDefinition {
395            command: vec!["zig".to_string(), "fmt".to_string(), "--stdin".to_string()],
396            stdin: true,
397            stdout: true,
398            lint_args: vec!["--check".to_string()],
399            format_args: vec![],
400        },
401    );
402
403    // Dart - dart format
404    m.insert(
405        "dart-format",
406        ToolDefinition {
407            command: vec!["dart".to_string(), "format".to_string()],
408            stdin: true,
409            stdout: true,
410            lint_args: vec!["--output=none".to_string(), "--set-exit-if-changed".to_string()],
411            format_args: vec![],
412        },
413    );
414
415    // Swift - swift-format
416    m.insert(
417        "swift-format",
418        ToolDefinition {
419            command: vec!["swift-format".to_string()],
420            stdin: true,
421            stdout: true,
422            lint_args: vec!["lint".to_string()],
423            format_args: vec![],
424        },
425    );
426
427    // Kotlin - ktfmt
428    m.insert(
429        "ktfmt",
430        ToolDefinition {
431            command: vec!["ktfmt".to_string(), "--stdin".to_string()],
432            stdin: true,
433            stdout: true,
434            lint_args: vec!["--dry-run".to_string()],
435            format_args: vec![],
436        },
437    );
438
439    // Jinja/HTML - djlint
440    m.insert(
441        "djlint",
442        ToolDefinition {
443            command: vec!["djlint".to_string(), "-".to_string()],
444            stdin: true,
445            stdout: true,
446            lint_args: vec![],
447            format_args: vec!["--reformat".to_string()],
448        },
449    );
450
451    m.insert(
452        "djlint:lint",
453        ToolDefinition {
454            command: vec!["djlint".to_string(), "-".to_string()],
455            stdin: true,
456            stdout: true,
457            lint_args: vec![],
458            format_args: vec![],
459        },
460    );
461
462    m.insert(
463        "djlint:reformat",
464        ToolDefinition {
465            command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
466            stdin: true,
467            stdout: true,
468            lint_args: vec![],
469            format_args: vec![],
470        },
471    );
472
473    // Shell - beautysh
474    m.insert(
475        "beautysh",
476        ToolDefinition {
477            command: vec!["beautysh".to_string(), "-".to_string()],
478            stdin: true,
479            stdout: true,
480            lint_args: vec!["--check".to_string()],
481            format_args: vec![],
482        },
483    );
484
485    // TOML - tombi
486    m.insert(
487        "tombi",
488        ToolDefinition {
489            command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
490            stdin: true,
491            stdout: true,
492            lint_args: vec![],
493            format_args: vec![],
494        },
495    );
496
497    m.insert(
498        "tombi:format",
499        ToolDefinition {
500            command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
501            stdin: true,
502            stdout: true,
503            lint_args: vec![],
504            format_args: vec![],
505        },
506    );
507
508    m.insert(
509        "tombi:lint",
510        ToolDefinition {
511            command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
512            stdin: true,
513            stdout: true,
514            lint_args: vec![],
515            format_args: vec![],
516        },
517    );
518
519    // JavaScript/CSS/HTML/JSON - oxfmt (OXC formatter)
520    m.insert(
521        "oxfmt",
522        ToolDefinition {
523            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
524            stdin: true,
525            stdout: true,
526            lint_args: vec!["--check".to_string()],
527            format_args: vec![],
528        },
529    );
530
531    m.insert(
532        "oxfmt:js",
533        ToolDefinition {
534            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
535            stdin: true,
536            stdout: true,
537            lint_args: vec!["--check".to_string()],
538            format_args: vec![],
539        },
540    );
541
542    m.insert(
543        "oxfmt:ts",
544        ToolDefinition {
545            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
546            stdin: true,
547            stdout: true,
548            lint_args: vec!["--check".to_string()],
549            format_args: vec![],
550        },
551    );
552
553    m.insert(
554        "oxfmt:jsx",
555        ToolDefinition {
556            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
557            stdin: true,
558            stdout: true,
559            lint_args: vec!["--check".to_string()],
560            format_args: vec![],
561        },
562    );
563
564    m.insert(
565        "oxfmt:tsx",
566        ToolDefinition {
567            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
568            stdin: true,
569            stdout: true,
570            lint_args: vec!["--check".to_string()],
571            format_args: vec![],
572        },
573    );
574
575    m.insert(
576        "oxfmt:json",
577        ToolDefinition {
578            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
579            stdin: true,
580            stdout: true,
581            lint_args: vec!["--check".to_string()],
582            format_args: vec![],
583        },
584    );
585
586    m.insert(
587        "oxfmt:css",
588        ToolDefinition {
589            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
590            stdin: true,
591            stdout: true,
592            lint_args: vec!["--check".to_string()],
593            format_args: vec![],
594        },
595    );
596
597    m
598});
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603
604    #[test]
605    fn test_get_builtin_tool() {
606        let registry = ToolRegistry::default();
607
608        let tool = registry.get("ruff:check").expect("Should find ruff:check");
609        assert!(tool.command.contains(&"ruff".to_string()));
610        assert!(tool.stdin);
611        assert!(tool.stdout);
612
613        let tool = registry.get("shellcheck").expect("Should find shellcheck");
614        assert!(tool.command.contains(&"shellcheck".to_string()));
615    }
616
617    #[test]
618    fn test_get_user_tool_overrides_builtin() {
619        let mut user_tools = HashMap::new();
620        user_tools.insert(
621            "ruff:check".to_string(),
622            ToolDefinition {
623                command: vec!["custom-ruff".to_string()],
624                stdin: false,
625                stdout: false,
626                lint_args: vec![],
627                format_args: vec![],
628            },
629        );
630
631        let registry = ToolRegistry::new(user_tools);
632
633        let tool = registry.get("ruff:check").expect("Should find ruff:check");
634        assert_eq!(tool.command, vec!["custom-ruff"]);
635        assert!(!tool.stdin); // User override
636    }
637
638    #[test]
639    fn test_contains() {
640        let registry = ToolRegistry::default();
641
642        assert!(registry.contains("ruff:check"));
643        assert!(registry.contains("prettier"));
644        assert!(registry.contains("shellcheck"));
645        assert!(!registry.contains("nonexistent-tool"));
646    }
647
648    #[test]
649    fn test_list_tools() {
650        let registry = ToolRegistry::default();
651        let tools = registry.list_tools();
652
653        assert!(tools.contains(&"ruff:check"));
654        assert!(tools.contains(&"ruff:format"));
655        assert!(tools.contains(&"prettier"));
656        assert!(tools.contains(&"shellcheck"));
657        assert!(tools.contains(&"shfmt"));
658        assert!(tools.contains(&"rustfmt"));
659        assert!(tools.contains(&"gofmt"));
660    }
661
662    #[test]
663    fn test_user_tools_in_list() {
664        let mut user_tools = HashMap::new();
665        user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
666
667        let registry = ToolRegistry::new(user_tools);
668        let tools = registry.list_tools();
669
670        assert!(tools.contains(&"my-custom-tool"));
671        assert!(tools.contains(&"ruff:check")); // Built-in still available
672    }
673
674    #[test]
675    fn test_new_builtin_tools() {
676        let registry = ToolRegistry::default();
677
678        // djlint
679        let tool = registry.get("djlint").expect("Should find djlint");
680        assert!(tool.command.contains(&"djlint".to_string()));
681        assert!(tool.stdin);
682
683        // beautysh
684        let tool = registry.get("beautysh").expect("Should find beautysh");
685        assert!(tool.command.contains(&"beautysh".to_string()));
686        assert!(tool.stdin);
687
688        // tombi
689        let tool = registry.get("tombi").expect("Should find tombi");
690        assert!(tool.command.contains(&"tombi".to_string()));
691        assert!(tool.stdin);
692
693        let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
694        assert!(tool.command.contains(&"lint".to_string()));
695
696        // oxfmt
697        let tool = registry.get("oxfmt").expect("Should find oxfmt");
698        assert!(tool.command.contains(&"oxfmt".to_string()));
699        assert!(tool.stdin);
700
701        let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
702        assert!(tool.command.iter().any(|s| s.contains("_.ts")));
703    }
704}