#[path = "code_nav_outline.rs"]
mod code_nav_outline;
use crate::cm_tools::tools::symbol;
use crate::cm_tools::tools::tool_param_types::FindReferencesArgs;
use code_nav_outline::collect_outline;
use regex::Regex;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
const MAX_FILE_SIZE_BYTES: usize = 2 * 1024 * 1024;
const DEFAULT_MAX_RESULTS: usize = 80;
const MAX_RESULTS_LIMIT: usize = 300;
const DEFAULT_MAX_OUTLINE_ITEMS: usize = 200;
pub fn find_references(args_json: &str, workspace_root: &Path) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let args: FindReferencesArgs = match serde_json::from_value(v) {
Ok(a) => a,
Err(e) => return format!("参数解析错误: {e}"),
};
let symbol = args.symbol.trim().to_string();
if symbol.is_empty() {
return "错误:缺少 symbol 参数".to_string();
}
let sub_path = args
.path
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let max_results = (args
.max_results
.unwrap_or(DEFAULT_MAX_RESULTS as u64)
.max(1) as usize)
.min(MAX_RESULTS_LIMIT);
let case_insensitive = !args.case_sensitive;
let exclude_definitions = args.exclude_definitions;
let include_hidden = args.include_hidden;
let root = match resolve_root(workspace_root, sub_path.as_deref()) {
Ok(p) => p,
Err(e) => return e,
};
let esc = regex::escape(&symbol);
let ref_pat = format!(r"\b{}\b", esc);
let ref_re = match regex::RegexBuilder::new(&ref_pat)
.case_insensitive(case_insensitive)
.build()
{
Ok(r) => r,
Err(e) => return format!("错误:无法构建引用搜索规则:{}", e),
};
let mut results: Vec<(PathBuf, usize, String)> = Vec::new();
let mut visited = 0usize;
let mut walk_ctx = RsWalkCtx {
ref_re: &ref_re,
symbol: &symbol,
exclude_definitions,
case_insensitive,
results: &mut results,
visited_files: &mut visited,
max_results,
ignore_hidden_dirs: include_hidden,
};
if let Err(e) = walk_rs(&root, &mut walk_ctx) {
return format!("搜索过程中发生错误:{}", e);
}
if results.is_empty() {
return format!(
"未找到引用:\"{}\"(遍历 {} 个 .rs 文件,根:{})",
symbol,
visited,
root.display()
);
}
let mut out = String::new();
out.push_str(&format!(
"引用搜索:\"{}\",根目录:{}\n(词边界匹配;exclude_definitions={})\n匹配 {} 条(上限 {}):\n\n",
symbol,
root.display(),
exclude_definitions,
results.len(),
max_results
));
for (path, line_no, line) in results {
out.push_str(&format!(
"{}:{}: {}\n",
path.display(),
line_no,
truncate_line(&line, 320)
));
}
out.trim_end().to_string()
}
pub fn rust_file_outline(args_json: &str, workspace_root: &Path) -> String {
let v = match crate::cm_tools::tools::parse_args_json(args_json) {
Ok(v) => v,
Err(e) => return e,
};
let path = v
.get("path")
.and_then(|p| p.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let Some(rel) = path else {
return "错误:缺少 path 参数".to_string();
};
let include_use = v
.get("include_use")
.and_then(|b| b.as_bool())
.unwrap_or(false);
let max_items = v
.get("max_items")
.and_then(|n| n.as_u64())
.map(|n| n.max(1) as usize)
.unwrap_or(DEFAULT_MAX_OUTLINE_ITEMS)
.min(500);
let target = match resolve_file(workspace_root, rel) {
Ok(p) => p,
Err(e) => return e,
};
if !target.is_file() {
return "错误:path 必须是已存在的文件".to_string();
}
if target.extension().and_then(|e| e.to_str()) != Some("rs") {
return "错误:当前仅支持 .rs 文件大纲".to_string();
}
let mut buf = String::new();
if let Err(e) = fs::File::open(&target).and_then(|mut f| f.read_to_string(&mut buf)) {
return format!("读取文件失败:{}", e);
}
if buf.len() > MAX_FILE_SIZE_BYTES {
buf = super::output_util::truncate_to_char_boundary(&buf, MAX_FILE_SIZE_BYTES);
}
let items = collect_outline(&buf, include_use, max_items);
if items.is_empty() {
return format!(
"文件 {} 中未匹配到常见顶层结构(可尝试 include_use=true)",
rel
);
}
let mut out = String::new();
out.push_str(&format!(
"Rust 文件大纲:{}({} 项,最多 {})\n\n",
rel,
items.len(),
max_items
));
for (line_no, summary) in items {
out.push_str(&format!("{:>5}: {}\n", line_no, summary));
}
out.trim_end().to_string()
}
fn resolve_root(base: &Path, sub: Option<&str>) -> Result<PathBuf, String> {
match sub {
None => Ok(base.to_path_buf()),
Some(s) => {
let sub_path = Path::new(s);
if sub_path.is_absolute() {
return Err("路径必须为工作区内相对路径,不能使用绝对路径".to_string());
}
if s.contains("..") {
return Err("路径不能包含 ..".to_string());
}
let joined = base.join(sub_path);
let canon_base = base
.canonicalize()
.map_err(|e| format!("工作区根目录无法解析: {}", e))?;
let canon_joined = joined
.canonicalize()
.map_err(|e| format!("搜索路径无法解析: {}", e))?;
if !canon_joined.starts_with(&canon_base) {
return Err("搜索路径不能超出工作区根目录".to_string());
}
Ok(canon_joined)
}
}
}
fn resolve_file(base: &Path, sub: &str) -> Result<PathBuf, String> {
let sub_path = Path::new(sub);
if sub_path.is_absolute() {
return Err("路径必须为工作区内相对路径,不能使用绝对路径".to_string());
}
if sub.contains("..") {
return Err("路径不能包含 ..".to_string());
}
let joined = base.join(sub_path);
let canon_base = base
.canonicalize()
.map_err(|e| format!("工作区根目录无法解析: {}", e))?;
let canonical = joined
.canonicalize()
.map_err(|e| format!("目标文件路径无法解析: {}", e))?;
if !canonical.starts_with(&canon_base) {
return Err("目标路径不能超出工作区根目录".to_string());
}
Ok(canonical)
}
struct RsWalkCtx<'a> {
ref_re: &'a Regex,
symbol: &'a str,
exclude_definitions: bool,
case_insensitive: bool,
results: &'a mut Vec<(PathBuf, usize, String)>,
visited_files: &'a mut usize,
max_results: usize,
ignore_hidden_dirs: bool,
}
fn walk_rs(root: &Path, ctx: &mut RsWalkCtx<'_>) -> Result<(), String> {
if !root.exists() {
return Ok(());
}
if root.is_file() {
return walk_rs_one_file(root, ctx);
}
walk_rs_dir(root, ctx)
}
fn walk_rs_dir(dir: &Path, ctx: &mut RsWalkCtx<'_>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
let name = entry.file_name();
if !is_rs_entry_visitable(&path, &name, ctx) {
continue;
}
if path.is_dir() {
walk_rs(&path, ctx)?;
} else {
walk_rs_one_file(&path, ctx)?;
}
if ctx.results.len() >= ctx.max_results {
break;
}
}
Ok(())
}
fn is_rs_entry_visitable(path: &Path, name: &std::ffi::OsStr, ctx: &RsWalkCtx<'_>) -> bool {
if ctx.ignore_hidden_dirs && name.to_string_lossy().starts_with('.') {
return false;
}
if path.is_dir() {
return true;
}
path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("rs")
}
fn walk_rs_one_file(path: &Path, ctx: &mut RsWalkCtx<'_>) -> Result<(), String> {
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
return Ok(());
}
*ctx.visited_files += 1;
search_refs_in_file(
path,
ctx.ref_re,
ctx.symbol,
ctx.exclude_definitions,
ctx.case_insensitive,
ctx.results,
ctx.max_results,
)
}
fn search_refs_in_file(
path: &Path,
ref_re: &Regex,
symbol: &str,
exclude_definitions: bool,
case_insensitive: bool,
results: &mut Vec<(PathBuf, usize, String)>,
max_results: usize,
) -> Result<(), String> {
let mut buf = String::new();
let mut f = fs::File::open(path).map_err(|e| e.to_string())?;
f.read_to_string(&mut buf).map_err(|e| e.to_string())?;
if buf.len() > MAX_FILE_SIZE_BYTES {
buf = super::output_util::truncate_to_char_boundary(&buf, MAX_FILE_SIZE_BYTES);
}
for (idx, line) in buf.lines().enumerate() {
if !ref_re.is_match(line) {
continue;
}
if exclude_definitions
&& symbol::line_looks_like_rust_definition(line, symbol, case_insensitive)
{
continue;
}
results.push((path.to_path_buf(), idx + 1, line.to_string()));
if results.len() >= max_results {
break;
}
}
Ok(())
}
fn truncate_line(s: &str, max_chars: usize) -> String {
let s = s.trim_end();
let mut chars = s.chars();
let mut out = String::new();
for _ in 0..max_chars {
if let Some(ch) = chars.next() {
out.push(ch);
} else {
return out;
}
}
if s.chars().count() > max_chars {
format!("{}... (截断)", out)
} else {
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outline_finds_fn_and_mod() {
let src = r"
mod foo {
}
pub fn bar(x: u32) -> u32 { x }
";
let v = collect_outline(src, false, 50);
assert!(v.iter().any(|(_, s)| s.contains("mod foo")));
assert!(v.iter().any(|(_, s)| s.contains("fn bar")));
}
#[test]
fn outline_respects_max_items() {
let mut s = String::new();
for i in 0..30 {
s.push_str(&format!("fn f{i}() {{}}\n"));
}
let v = collect_outline(&s, false, 5);
assert_eq!(v.len(), 5);
}
}