Skip to main content

Mountain/RPC/CocoonService/FileSystem/
FindTextInFiles.rs

1//! Substring search across the workspace, capped at 1,000 matches. Skips
2//! hidden directories plus `node_modules` and `target`. Runs the walk in
3//! `tokio::task::spawn_blocking` so the event loop stays responsive.
4
5use tonic::{Response, Status};
6use ::Vine::Generated::{FindTextInFilesRequest, FindTextInFilesResponse, Position, Range, TextMatch, Uri};
7
8use crate::{RPC::CocoonService::CocoonServiceImpl, dev_log};
9
10pub async fn Fn(
11	Service:&CocoonServiceImpl,
12
13	Request:FindTextInFilesRequest,
14) -> Result<Response<FindTextInFilesResponse>, Status> {
15	if Request.pattern.is_empty() {
16		return Ok(Response::new(FindTextInFilesResponse::default()));
17	}
18
19	dev_log!("cocoon", "[CocoonService] find_text_in_files: pattern='{}'", Request.pattern);
20
21	let Roots:Vec<std::path::PathBuf> = {
22		match Service.environment.ApplicationState.Workspace.WorkspaceFolders.lock() {
23			Ok(Guard) => Guard.iter().map(|F| std::path::PathBuf::from(F.URI.path())).collect(),
24
25			Err(_) => Vec::new(),
26		}
27	};
28
29	let SearchRoots = if Roots.is_empty() {
30		vec![std::env::current_dir().unwrap_or_default()]
31	} else {
32		Roots
33	};
34
35	let Pattern = Request.pattern.clone();
36
37	let Matches = tokio::task::spawn_blocking(move || {
38		let mut Results:Vec<TextMatch> = Vec::new();
39
40		const MAX_MATCHES:usize = 1000;
41
42		fn WalkAndSearch(Directory:&std::path::Path, Pattern:&str, Results:&mut Vec<TextMatch>) {
43			if Results.len() >= MAX_MATCHES {
44				return;
45			}
46
47			if let Ok(Entries) = std::fs::read_dir(Directory) {
48				for Entry in Entries.flatten() {
49					if Results.len() >= MAX_MATCHES {
50						break;
51					}
52
53					let Path = Entry.path();
54
55					if Path.is_dir() {
56						let Name = Path.file_name().and_then(|N| N.to_str()).unwrap_or("");
57
58						if Name.starts_with('.') || Name == "node_modules" || Name == "target" {
59							continue;
60						}
61
62						WalkAndSearch(&Path, Pattern, Results);
63					} else if Path.is_file() {
64						if let Ok(Content) = std::fs::read_to_string(&Path) {
65							for (LineIndex, Line) in Content.lines().enumerate() {
66								if Results.len() >= MAX_MATCHES {
67									break;
68								}
69
70								if let Some(ColumnIndex) = Line.find(Pattern) {
71									Results.push(TextMatch {
72										uri:Some(Uri { value:format!("file://{}", Path.display()) }),
73										range:Some(Range {
74											start:Some(Position {
75												line:LineIndex as u32,
76												character:ColumnIndex as u32,
77											}),
78											end:Some(Position {
79												line:LineIndex as u32,
80												character:(ColumnIndex + Pattern.len()) as u32,
81											}),
82										}),
83										preview:Line.to_string(),
84									});
85								}
86							}
87						}
88					}
89				}
90			}
91		}
92
93		for Root in &SearchRoots {
94			WalkAndSearch(Root, &Pattern, &mut Results);
95
96			if Results.len() >= MAX_MATCHES {
97				break;
98			}
99		}
100
101		Results
102	})
103	.await
104	.unwrap_or_default();
105
106	dev_log!(
107		"cocoon",
108		"[CocoonService] find_text_in_files: {} matches for '{}'",
109		Matches.len(),
110		Request.pattern
111	);
112
113	Ok(Response::new(FindTextInFilesResponse { matches:Matches }))
114}