Skip to main content

Mountain/RPC/CocoonService/FileSystem/
FindTextInFiles.rs

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