Skip to main content

rs_hack/commands/
find.rs

1//! `find` command as a lib API. Returns structured matches; rendering (text,
2//! snippets, hints) is the caller's job — see `main.rs` for the CLI renderer.
3
4use std::path::PathBuf;
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8
9use crate::editor::RustEditor;
10use crate::files::{collect_rust_files_with_exclusions, expand_kind_to_node_types};
11use crate::operations::{FieldLocation, InspectResult};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct FindArgs {
15    pub paths: Vec<PathBuf>,
16    pub exclude: Vec<String>,
17    pub kind: Option<String>,
18    pub node_type: Option<String>,
19    pub name: Option<String>,
20    pub variant: Option<String>,
21    pub content_filter: Option<String>,
22    pub field_name: Option<String>,
23    pub include_comments: bool,
24    /// Number of raw source lines to show before each snippet match (like grep -B N)
25    #[serde(default)]
26    pub context: Option<usize>,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum FindResult {
32    Field { matches: Vec<FieldLocation> },
33    Nodes { matches: Vec<InspectResult> },
34}
35
36impl FindResult {
37    pub fn is_empty(&self) -> bool {
38        match self {
39            FindResult::Field { matches } => matches.is_empty(),
40            FindResult::Nodes { matches } => matches.is_empty(),
41        }
42    }
43}
44
45pub fn run(args: &FindArgs) -> Result<FindResult> {
46    let files = collect_rust_files_with_exclusions(&args.paths, &args.exclude)?;
47
48    if let Some(field) = &args.field_name {
49        return Ok(FindResult::Field {
50            matches: find_field(&files, field)?,
51        });
52    }
53
54    let node_types_to_search: Vec<Option<&str>> = if let Some(k) = &args.kind {
55        let expanded = expand_kind_to_node_types(k);
56        if expanded.is_empty() {
57            anyhow::bail!(
58                "Unknown kind '{}'. Valid kinds: struct, function, enum, match, identifier, type, macro, const, trait, mod, use",
59                k
60            );
61        }
62        expanded.into_iter().map(Some).collect()
63    } else if let Some(nt) = &args.node_type {
64        vec![Some(nt.as_str())]
65    } else {
66        vec![None]
67    };
68
69    let mut all_results: Vec<InspectResult> = Vec::new();
70
71    for file in &files {
72        let content = std::fs::read_to_string(file)
73            .with_context(|| format!("Failed to read file: {:?}", file))?;
74
75        let editor = match RustEditor::new(&content) {
76            Ok(e) => e,
77            Err(e) => {
78                eprintln!("⚠️  Skipping {}: {}", file.display(), e);
79                continue;
80            }
81        };
82
83        for node_type_to_search in &node_types_to_search {
84            let mut results = editor.inspect(
85                *node_type_to_search,
86                args.name.as_deref(),
87                args.variant.as_deref(),
88                args.include_comments,
89            )?;
90
91            for result in &mut results {
92                result.file_path = file.to_string_lossy().to_string();
93            }
94
95            if let Some(filter) = &args.content_filter {
96                results.retain(|r| r.snippet.contains(filter));
97            }
98
99            all_results.extend(results);
100        }
101    }
102
103    Ok(FindResult::Nodes { matches: all_results })
104}
105
106/// Re-search across all node types — used by the CLI to suggest near-misses
107/// when a typed search returns nothing. Exposed so embedders can offer the
108/// same hint UX.
109pub fn run_unfiltered_by_node_type(args: &FindArgs) -> Result<Vec<InspectResult>> {
110    let files = collect_rust_files_with_exclusions(&args.paths, &args.exclude)?;
111    let mut hint_results: Vec<InspectResult> = Vec::new();
112
113    for file in &files {
114        let content = std::fs::read_to_string(file)
115            .with_context(|| format!("Failed to read file: {:?}", file))?;
116
117        let editor = match RustEditor::new(&content) {
118            Ok(e) => e,
119            Err(_) => continue,
120        };
121        let mut results = editor.inspect(
122            None,
123            args.name.as_deref(),
124            args.variant.as_deref(),
125            false,
126        )?;
127
128        for result in &mut results {
129            result.file_path = file.to_string_lossy().to_string();
130        }
131
132        if let Some(filter) = &args.content_filter {
133            results.retain(|r| r.snippet.contains(filter));
134        }
135
136        hint_results.extend(results);
137    }
138
139    Ok(hint_results)
140}
141
142fn find_field(files: &[PathBuf], field: &str) -> Result<Vec<FieldLocation>> {
143    let mut all_locations: Vec<FieldLocation> = Vec::new();
144
145    for file in files {
146        let content = std::fs::read_to_string(file)
147            .with_context(|| format!("Failed to read file: {:?}", file))?;
148
149        let editor = match RustEditor::new(&content) {
150            Ok(e) => e,
151            Err(e) => {
152                eprintln!("⚠️  Skipping {}: {}", file.display(), e);
153                continue;
154            }
155        };
156        let mut locations = editor.find_field_locations(field)?;
157        for location in &mut locations {
158            location.file_path = file.to_string_lossy().to_string();
159        }
160        all_locations.extend(locations);
161    }
162
163    Ok(all_locations)
164}