Skip to main content

Mountain/RPC/CocoonService/FileSystem/
FindTextInFiles.rs

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