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
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_cli_symbol_parsing() {
171        let args = vec!["rust-analyzer-cli", "symbol", "LspClient", "struct", "--exact"];
172        let cli = Cli::try_parse_from(args).unwrap();
173        assert_eq!(cli.port, 60094);
174        assert!(!cli.json);
175        match cli.command {
176            Commands::Symbol { name, kind, exact, body, max_lines } => {
177                assert_eq!(name, "LspClient");
178                assert_eq!(kind, "struct");
179                assert!(exact);
180                assert!(!body);
181                assert_eq!(max_lines, 100);
182            }
183            _ => panic!("Expected Symbol command"),
184        }
185    }
186
187    #[test]
188    fn test_cli_outline_parsing() {
189        let args = vec!["rust-analyzer-cli", "--json", "outline", "-f", "src/main.rs"];
190        let cli = Cli::try_parse_from(args).unwrap();
191        assert!(cli.json);
192        match cli.command {
193            Commands::Outline { file, file_list, output, body, max_lines } => {
194                assert_eq!(file, Some(PathBuf::from("src/main.rs")));
195                assert!(file_list.is_none());
196                assert!(output.is_none());
197                assert!(!body);
198                assert_eq!(max_lines, 100);
199            }
200            _ => panic!("Expected Outline command"),
201        }
202    }
203
204    #[test]
205    fn test_cli_definition_parsing() {
206        let args = vec!["rust-analyzer-cli", "definition", "-f", "src/lib.rs", "-l", "10", "-c", "5", "--body"];
207        let cli = Cli::try_parse_from(args).unwrap();
208        match cli.command {
209            Commands::Definition { file, line, col, body, max_lines, no_line_numbers } => {
210                assert_eq!(file, PathBuf::from("src/lib.rs"));
211                assert_eq!(line, 10);
212                assert_eq!(col, 5);
213                assert!(body);
214                assert_eq!(max_lines, 100);
215                assert!(!no_line_numbers);
216            }
217            _ => panic!("Expected Definition command"),
218        }
219    }
220
221    #[test]
222    fn test_cli_body_parsing() {
223        let args = vec!["rust-analyzer-cli", "body", "-f", "src/lib.rs", "-l", "10", "-c", "5", "--max-lines", "50", "--no-line-numbers"];
224        let cli = Cli::try_parse_from(args).unwrap();
225        match cli.command {
226            Commands::Body { file, line, col, max_lines, no_line_numbers } => {
227                assert_eq!(file, PathBuf::from("src/lib.rs"));
228                assert_eq!(line, 10);
229                assert_eq!(col, 5);
230                assert_eq!(max_lines, 50);
231                assert!(no_line_numbers);
232            }
233            _ => panic!("Expected Body command"),
234        }
235    }
236
237    #[test]
238    fn test_cli_init_skill_parsing() {
239        let args = vec!["rust-analyzer-cli", "init-skill", "-w", "/my/proj", "-d", "custom/SKILL.md"];
240        let cli = Cli::try_parse_from(args).unwrap();
241        match cli.command {
242            Commands::InitSkill { workspace, dir } => {
243                assert_eq!(workspace, PathBuf::from("/my/proj"));
244                assert_eq!(dir, Some(PathBuf::from("custom/SKILL.md")));
245            }
246            _ => panic!("Expected InitSkill command"),
247        }
248    }
249}
250
251
252
253