Skip to main content

rust_analyzer_cli/
cli.rs

1use clap::{Parser, Subcommand};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
5#[command(
6    name = "rust-analyzer-cli",
7    author,
8    version,
9    about = "Compiler-accurate Rust codebase navigation powered by rust-analyzer LSP"
10)]
11pub struct Cli {
12    /// Port for the background daemon HTTP server
13    #[arg(short, long, env = "RUST_ANALYZER_CLI_PORT", default_value_t = 60094)]
14    pub port: u16,
15
16    /// Output results in JSON format
17    #[arg(short, long, global = true)]
18    pub json: bool,
19
20    #[command(subcommand)]
21    pub command: Commands,
22}
23
24#[derive(Subcommand, Debug)]
25pub enum Commands {
26    /// Start the background LSP daemon and HTTP server
27    Daemon {
28        /// Workspace root directory
29        #[arg(short, long, default_value = ".")]
30        workspace: PathBuf,
31    },
32    /// Query daemon and rust-analyzer status
33    Status,
34    /// Refresh / restart the rust-analyzer session
35    Refresh,
36    /// Search workspace symbols
37    Symbol {
38        /// Symbol name to search for
39        name: String,
40        /// Optional symbol filter: any, struct, enum, fn, trait, type, const, var
41        #[arg(default_value = "any")]
42        kind: String,
43        /// Match exact symbol name
44        #[arg(long)]
45        exact: bool,
46        /// Include source code body/snippet
47        #[arg(short, long)]
48        body: bool,
49        /// Max lines for source code body (0 for unlimited)
50        #[arg(long, default_value_t = 100)]
51        max_lines: usize,
52    },
53    /// Extract file document symbols outline
54    Outline {
55        /// Target file path
56        #[arg(short, long)]
57        file: Option<PathBuf>,
58        /// Comma-separated list of target file paths
59        #[arg(long)]
60        file_list: Option<String>,
61        /// Write outline output to a file
62        #[arg(short, long)]
63        output: Option<PathBuf>,
64        /// Include source code body/snippet for symbols
65        #[arg(short, long)]
66        body: bool,
67        /// Max lines for source code body (0 for unlimited)
68        #[arg(long, default_value_t = 100)]
69        max_lines: usize,
70    },
71    /// Go to definition at file line and column
72    Definition {
73        /// File path
74        #[arg(short, long)]
75        file: PathBuf,
76        /// Line number (1-based)
77        #[arg(short, long)]
78        line: u32,
79        /// Column number (1-based)
80        #[arg(short, long)]
81        col: u32,
82        /// Include source code body/snippet of the definition target
83        #[arg(short, long)]
84        body: bool,
85        /// Max lines for source code body (0 for unlimited)
86        #[arg(long, default_value_t = 100)]
87        max_lines: usize,
88        /// Omit line numbers in terminal output
89        #[arg(long)]
90        no_line_numbers: bool,
91    },
92    /// Inspect source code body of struct, function, enum, impl, trait, or module
93    Body {
94        /// File path
95        #[arg(short, long)]
96        file: PathBuf,
97        /// Line number (1-based)
98        #[arg(short, long)]
99        line: u32,
100        /// Column number (1-based)
101        #[arg(short, long)]
102        col: u32,
103        /// Max lines for source code body (0 for unlimited)
104        #[arg(long, default_value_t = 100)]
105        max_lines: usize,
106        /// Omit line numbers in terminal output
107        #[arg(long)]
108        no_line_numbers: bool,
109    },
110    /// Find references or call hierarchy at file line and column
111    Cursor {
112        /// File path
113        #[arg(short, long)]
114        file: PathBuf,
115        /// Line number (1-based)
116        #[arg(short, long)]
117        line: u32,
118        /// Column number (1-based)
119        #[arg(short, long)]
120        col: u32,
121        /// Mode: incoming, outgoing, or references
122        #[arg(short, long, default_value = "incoming")]
123        mode: String,
124        /// Traversal depth (default: 1)
125        #[arg(short, long, default_value_t = 1)]
126        depth: u32,
127    },
128    /// Query type hierarchy (supertypes / subtypes)
129    TypeHierarchy {
130        /// File path
131        #[arg(short, long)]
132        file: PathBuf,
133        /// Line number (1-based)
134        #[arg(short, long)]
135        line: u32,
136        /// Column number (1-based)
137        #[arg(short, long)]
138        col: u32,
139        /// Mode: supertypes or subtypes
140        #[arg(short, long, default_value = "supertypes")]
141        mode: String,
142        /// Traversal depth (default: 1)
143        #[arg(short, long, default_value_t = 1)]
144        depth: u32,
145    },
146    /// Run cargo check compilation verification
147    Check {
148        /// Optional cargo build target
149        #[arg(short, long)]
150        target: Option<String>,
151    },
152    /// Initialize/install the rust-codebase-navigation SKILL.md in a workspace
153    InitSkill {
154        /// Target workspace directory (default: .)
155        #[arg(short, long, default_value = ".")]
156        workspace: PathBuf,
157        /// Custom relative skill path (default: .agents/skills/rust-codebase-navigation/SKILL.md)
158        #[arg(short, long)]
159        dir: Option<PathBuf>,
160    },
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_cli_symbol_parsing() {
169        let args = vec![
170            "rust-analyzer-cli",
171            "symbol",
172            "ExampleStruct",
173            "struct",
174            "--exact",
175        ];
176        let cli = Cli::try_parse_from(args).unwrap();
177        assert_eq!(cli.port, 60094);
178        assert!(!cli.json);
179        match cli.command {
180            Commands::Symbol {
181                name,
182                kind,
183                exact,
184                body,
185                max_lines,
186            } => {
187                assert_eq!(name, "ExampleStruct");
188                assert_eq!(kind, "struct");
189                assert!(exact);
190                assert!(!body);
191                assert_eq!(max_lines, 100);
192            }
193            _ => panic!("Expected Symbol command"),
194        }
195    }
196
197    #[test]
198    fn test_cli_outline_parsing() {
199        let args = vec![
200            "rust-analyzer-cli",
201            "--json",
202            "outline",
203            "-f",
204            "tests/code_example.rs",
205        ];
206        let cli = Cli::try_parse_from(args).unwrap();
207        assert!(cli.json);
208        match cli.command {
209            Commands::Outline {
210                file,
211                file_list,
212                output,
213                body,
214                max_lines,
215            } => {
216                assert_eq!(file, Some(PathBuf::from("tests/code_example.rs")));
217                assert!(file_list.is_none());
218                assert!(output.is_none());
219                assert!(!body);
220                assert_eq!(max_lines, 100);
221            }
222            _ => panic!("Expected Outline command"),
223        }
224    }
225
226    #[test]
227    fn test_cli_definition_parsing() {
228        let args = vec![
229            "rust-analyzer-cli",
230            "definition",
231            "-f",
232            "tests/code_example.rs",
233            "-l",
234            "10",
235            "-c",
236            "5",
237            "--body",
238        ];
239        let cli = Cli::try_parse_from(args).unwrap();
240        match cli.command {
241            Commands::Definition {
242                file,
243                line,
244                col,
245                body,
246                max_lines,
247                no_line_numbers,
248            } => {
249                assert_eq!(file, PathBuf::from("tests/code_example.rs"));
250                assert_eq!(line, 10);
251                assert_eq!(col, 5);
252                assert!(body);
253                assert_eq!(max_lines, 100);
254                assert!(!no_line_numbers);
255            }
256            _ => panic!("Expected Definition command"),
257        }
258    }
259
260    #[test]
261    fn test_cli_body_parsing() {
262        let args = vec![
263            "rust-analyzer-cli",
264            "body",
265            "-f",
266            "tests/code_example.rs",
267            "-l",
268            "10",
269            "-c",
270            "5",
271            "--max-lines",
272            "50",
273            "--no-line-numbers",
274        ];
275        let cli = Cli::try_parse_from(args).unwrap();
276        match cli.command {
277            Commands::Body {
278                file,
279                line,
280                col,
281                max_lines,
282                no_line_numbers,
283            } => {
284                assert_eq!(file, PathBuf::from("tests/code_example.rs"));
285                assert_eq!(line, 10);
286                assert_eq!(col, 5);
287                assert_eq!(max_lines, 50);
288                assert!(no_line_numbers);
289            }
290            _ => panic!("Expected Body command"),
291        }
292    }
293
294    #[test]
295    fn test_cli_init_skill_parsing() {
296        let args = vec![
297            "rust-analyzer-cli",
298            "init-skill",
299            "-w",
300            "/my/proj",
301            "-d",
302            "custom/SKILL.md",
303        ];
304        let cli = Cli::try_parse_from(args).unwrap();
305        match cli.command {
306            Commands::InitSkill { workspace, dir } => {
307                assert_eq!(workspace, PathBuf::from("/my/proj"));
308                assert_eq!(dir, Some(PathBuf::from("custom/SKILL.md")));
309            }
310            _ => panic!("Expected InitSkill command"),
311        }
312    }
313}