Skip to main content

Mountain/IPC/WindServiceHandler/
Search.rs

1#![allow(non_snake_case)]
2
3//! Search domain handlers for Wind IPC.
4
5use std::{path::PathBuf, sync::Arc};
6
7use serde_json::{Value, json};
8use tokio::fs;
9
10use crate::RunTime::ApplicationRunTime::ApplicationRunTime;
11
12/// Search text across all workspace files (line-by-line grep, max 1000
13/// results).
14pub async fn handle_search_find_in_files(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
15	use globset::GlobBuilder;
16
17	let Pattern = Args
18		.first()
19		.and_then(|V| V.as_str())
20		.ok_or("search:findInFiles requires pattern".to_string())?
21		.to_string();
22	let IsRegex = Args.get(1).and_then(|V| V.as_bool()).unwrap_or(false);
23	let IsCaseSensitive = Args.get(2).and_then(|V| V.as_bool()).unwrap_or(false);
24	let _IsWordMatch = Args.get(3).and_then(|V| V.as_bool()).unwrap_or(false);
25	let IncludeGlob = Args.get(4).and_then(|V| V.as_str()).unwrap_or("**").to_string();
26	let ExcludeGlob = Args.get(5).and_then(|V| V.as_str()).unwrap_or("").to_string();
27	let MaxResults = Args.get(6).and_then(|V| V.as_u64()).unwrap_or(1000) as usize;
28
29	let WorkspaceFolders = Runtime.Environment.ApplicationState.Workspace.GetWorkspaceFolders();
30
31	if WorkspaceFolders.is_empty() {
32		return Ok(json!([]));
33	}
34
35	let RootPath = PathBuf::from(&WorkspaceFolders[0].URI.to_string().replace("file://", ""));
36
37	let IncludeMatcher = GlobBuilder::new(&IncludeGlob)
38		.literal_separator(false)
39		.build()
40		.map(|G| G.compile_matcher())
41		.ok();
42
43	let ExcludeMatcher = if !ExcludeGlob.is_empty() {
44		GlobBuilder::new(&ExcludeGlob)
45			.literal_separator(false)
46			.build()
47			.map(|G| G.compile_matcher())
48			.ok()
49	} else {
50		None
51	};
52
53	let SearchText = Pattern.clone();
54	let mut Matches = Vec::new();
55
56	let mut Stack = vec![RootPath.clone()];
57	while let Some(Dir) = Stack.pop() {
58		let mut Entries = match fs::read_dir(&Dir).await {
59			Ok(E) => E,
60			Err(_) => continue,
61		};
62
63		while let Ok(Some(Entry)) = Entries.next_entry().await {
64			let Path = Entry.path();
65			let RelPath = Path.strip_prefix(&RootPath).unwrap_or(&Path).to_string_lossy().to_string();
66
67			if Path.file_name().map(|N| N.to_string_lossy().starts_with('.')).unwrap_or(false) {
68				continue;
69			}
70
71			if Path.is_dir() {
72				Stack.push(Path);
73				continue;
74			}
75
76			if let Some(Ref) = &IncludeMatcher {
77				if !Ref.is_match(&RelPath) {
78					continue;
79				}
80			}
81			if let Some(Ref) = &ExcludeMatcher {
82				if Ref.is_match(&RelPath) {
83					continue;
84				}
85			}
86
87			let Content = match fs::read_to_string(&Path).await {
88				Ok(C) => C,
89				Err(_) => continue,
90			};
91
92			for (LineIndex, Line) in Content.lines().enumerate() {
93				let Hit = if IsRegex {
94					Line.contains(&SearchText)
95				} else if IsCaseSensitive {
96					Line.contains(&SearchText)
97				} else {
98					Line.to_lowercase().contains(&SearchText.to_lowercase())
99				};
100
101				if Hit {
102					let Uri = format!("file://{}", Path.to_string_lossy());
103					Matches.push(json!({
104						"uri": Uri,
105						"lineNumber": LineIndex + 1,
106						"preview": Line.trim(),
107					}));
108
109					if Matches.len() >= MaxResults {
110						return Ok(json!(Matches));
111					}
112				}
113			}
114		}
115	}
116
117	Ok(json!(Matches))
118}
119
120/// Search file paths by glob pattern in workspace.
121pub async fn handle_search_find_files(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
122	use globset::GlobBuilder;
123
124	let Pattern = Args
125		.first()
126		.and_then(|V| V.as_str())
127		.ok_or("search:findFiles requires pattern".to_string())?
128		.to_string();
129	let MaxResults = Args.get(1).and_then(|V| V.as_u64()).unwrap_or(500) as usize;
130
131	let WorkspaceFolders = Runtime.Environment.ApplicationState.Workspace.GetWorkspaceFolders();
132
133	if WorkspaceFolders.is_empty() {
134		return Ok(json!([]));
135	}
136
137	let RootPath = PathBuf::from(&WorkspaceFolders[0].URI.to_string().replace("file://", ""));
138
139	let Matcher = GlobBuilder::new(&Pattern)
140		.literal_separator(false)
141		.build()
142		.map(|G| G.compile_matcher())
143		.map_err(|Error| format!("Invalid glob pattern: {}", Error))?;
144
145	let mut Files = Vec::new();
146	let mut Stack = vec![RootPath.clone()];
147
148	while let Some(Dir) = Stack.pop() {
149		let mut Entries = match fs::read_dir(&Dir).await {
150			Ok(E) => E,
151			Err(_) => continue,
152		};
153
154		while let Ok(Some(Entry)) = Entries.next_entry().await {
155			let Path = Entry.path();
156
157			if Path.file_name().map(|N| N.to_string_lossy().starts_with('.')).unwrap_or(false) {
158				continue;
159			}
160
161			if Path.is_dir() {
162				Stack.push(Path);
163				continue;
164			}
165
166			let RelPath = Path.strip_prefix(&RootPath).unwrap_or(&Path).to_string_lossy().to_string();
167
168			if Matcher.is_match(&RelPath) {
169				Files.push(format!("file://{}", Path.to_string_lossy()));
170
171				if Files.len() >= MaxResults {
172					return Ok(json!(Files));
173				}
174			}
175		}
176	}
177
178	Ok(json!(Files))
179}