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 const fn is_empty(&self) -> bool {
38        match self {
39            Self::Field { matches } => matches.is_empty(),
40            Self::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 {
104        matches: all_results,
105    })
106}
107
108/// Re-search across all node types — used by the CLI to suggest near-misses
109/// when a typed search returns nothing. Exposed so embedders can offer the
110/// same hint UX.
111pub fn run_unfiltered_by_node_type(args: &FindArgs) -> Result<Vec<InspectResult>> {
112    let files = collect_rust_files_with_exclusions(&args.paths, &args.exclude)?;
113    let mut hint_results: Vec<InspectResult> = Vec::new();
114
115    for file in &files {
116        let content = std::fs::read_to_string(file)
117            .with_context(|| format!("Failed to read file: {:?}", file))?;
118
119        let editor = match RustEditor::new(&content) {
120            Ok(e) => e,
121            Err(_) => continue,
122        };
123        let mut results =
124            editor.inspect(None, args.name.as_deref(), args.variant.as_deref(), false)?;
125
126        for result in &mut results {
127            result.file_path = file.to_string_lossy().to_string();
128        }
129
130        if let Some(filter) = &args.content_filter {
131            results.retain(|r| r.snippet.contains(filter));
132        }
133
134        hint_results.extend(results);
135    }
136
137    Ok(hint_results)
138}
139
140fn find_field(files: &[PathBuf], field: &str) -> Result<Vec<FieldLocation>> {
141    let mut all_locations: Vec<FieldLocation> = Vec::new();
142
143    for file in files {
144        let content = std::fs::read_to_string(file)
145            .with_context(|| format!("Failed to read file: {:?}", file))?;
146
147        let editor = match RustEditor::new(&content) {
148            Ok(e) => e,
149            Err(e) => {
150                eprintln!("⚠️  Skipping {}: {}", file.display(), e);
151                continue;
152            }
153        };
154        let mut locations = editor.find_field_locations(field)?;
155        for location in &mut locations {
156            location.file_path = file.to_string_lossy().to_string();
157        }
158        all_locations.extend(locations);
159    }
160
161    Ok(all_locations)
162}