vtcode_core/tools/code_search/
identity.rs1use std::path::{Component, Path, PathBuf};
2
3use crate::tools::code_search::CodeSearchRequest;
4use vtcode_commons::canonicalize;
5
6pub fn normalised_path(path: &str) -> String {
13 let mut normalised = PathBuf::new();
14 for component in Path::new(path).components() {
15 match component {
16 Component::ParentDir => {
17 if matches!(normalised.components().next_back(), Some(Component::Normal(_))) {
18 normalised.pop();
19 } else if !normalised.has_root() {
20 normalised.push("..");
21 }
22 }
23 Component::CurDir => {}
24 Component::Prefix(prefix) => normalised.push(prefix.as_os_str()),
25 Component::RootDir => normalised.push(component.as_os_str()),
26 Component::Normal(part) => normalised.push(part),
27 }
28 }
29 if normalised.as_os_str().is_empty() {
30 ".".to_string()
31 } else {
32 normalised.to_string_lossy().into_owned()
33 }
34}
35
36fn normalised_identity_value(args: &serde_json::Value, include_max_results: bool) -> Option<serde_json::Value> {
37 let request = serde_json::from_value::<CodeSearchRequest>(args.clone()).ok()?;
38 let mut normalised = request.normalise().ok()?;
39 normalised.filters.file_types.sort_unstable();
40 let mut identity = serde_json::json!({
41 "query": normalised.query,
42 "path": normalised_path(normalised.filters.path.as_str()),
43 "file_types": normalised.filters.file_types,
44 "result_types": normalised.filters.result_types,
45 });
46 if include_max_results {
47 identity["max_results"] = normalised.filters.max_results.into();
48 }
49 Some(identity)
50}
51
52pub fn normalised_identity(args: &serde_json::Value) -> Option<String> {
57 serde_json::to_string(&normalised_identity_value(args, true)?).ok()
58}
59
60pub fn normalised_loop_identity(args: &serde_json::Value) -> Option<String> {
65 serde_json::to_string(&normalised_identity_value(args, false)?).ok()
66}
67
68pub fn scope_contains_mutated_path(args: &serde_json::Value, mutated_path: &Path, workspace_root: &Path) -> bool {
76 let Ok(request) = serde_json::from_value::<CodeSearchRequest>(args.clone()) else {
77 return false;
78 };
79 let Ok(request) = request.normalise() else {
80 return false;
81 };
82 let workspace_root = canonicalize_existing_prefix(workspace_root);
83 let resolve = |path: &Path| {
84 let absolute = if path.is_absolute() {
85 path.to_path_buf()
86 } else {
87 workspace_root.join(path)
88 };
89 canonicalize_existing_prefix(&absolute)
90 };
91 let scope = resolve(Path::new(request.filters.path.as_str()));
92 let mutated_path = resolve(mutated_path);
93 mutated_path.starts_with(&scope) || scope.starts_with(&mutated_path)
94}
95
96pub(super) fn canonicalize_existing_prefix(path: &Path) -> PathBuf {
100 let normalised = crate::utils::path::normalize_path(path);
101 let mut existing_prefix = normalised.as_path();
102 let mut missing_components = Vec::new();
103
104 loop {
105 if let Ok(canonical) = canonicalize(existing_prefix) {
106 return missing_components.iter().rev().fold(canonical, |mut resolved, component| {
107 resolved.push(component);
108 resolved
109 });
110 }
111 let Some(component) = existing_prefix.file_name() else {
112 return normalised;
113 };
114 missing_components.push(component.to_os_string());
115 let Some(parent) = existing_prefix.parent() else {
116 return normalised;
117 };
118 existing_prefix = parent;
119 }
120}