Skip to content

Commit 2fb4062

Browse files
committed
Enhance search functionality with improved preview display and backend optimizations
- Added a new SearchPreview component to format and highlight search matches in the frontend. - Updated CSS styles for search previews to improve visibility of matched text. - Enhanced backend search logic to ignore files larger than 100 MB and return results more efficiently. - Refactored file search function to streamline result handling and improve cancellation support. - Search deadlock fixed.
1 parent 43956b1 commit 2fb4062

3 files changed

Lines changed: 116 additions & 108 deletions

File tree

anycode-backend/src/search.rs

Lines changed: 51 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ fn collect_files_inner(dir_path: &Path, collected: &mut Vec<PathBuf>) -> Result<
3535
if is_ignored_path(&path) {
3636
continue;
3737
}
38+
// Ignore files larger than 100 MB to avoid blocking search
39+
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100 MB
40+
if let Ok(metadata) = std::fs::metadata(&path) {
41+
if metadata.len() > MAX_FILE_SIZE {
42+
continue;
43+
}
44+
}
3845
collected.push(path);
3946
}
4047
}
@@ -131,35 +138,25 @@ pub async fn file_search(
131138
file_path: &str,
132139
pattern: &str,
133140
cancel_token: CancellationToken,
134-
result_tx: mpsc::Sender<SearchResult>,
135-
) -> Result<()> {
141+
) -> Result<Vec<SearchResult>> {
142+
let mut results = Vec::new();
143+
136144
// Check if pattern is multi-line (contains newline)
137145
let is_multiline = pattern.contains('\n');
138146

139147
if is_multiline {
140148
// For multi-line patterns, read entire file content
141149
if cancel_token.is_cancelled() {
142-
return Ok(());
150+
return Ok(results);
143151
}
144152

145153
let content = tokio::fs::read_to_string(file_path).await?;
146154

147155
if cancel_token.is_cancelled() {
148-
return Ok(());
156+
return Ok(results);
149157
}
150158

151-
let results = multiline_search(&content, pattern);
152-
153-
for result in results {
154-
if cancel_token.is_cancelled() {
155-
break;
156-
}
157-
158-
if let Err(e) = result_tx.send(result).await {
159-
eprintln!("Failed to send result: {}", e);
160-
break;
161-
}
162-
}
159+
results = multiline_search(&content, pattern);
163160
} else {
164161
// For single-line patterns, use line-by-line processing (more memory efficient)
165162
let path = Path::new(file_path);
@@ -171,32 +168,31 @@ pub async fn file_search(
171168

172169
loop {
173170
tokio::select! {
174-
line = lines.next_line() => {
175-
match line? {
171+
line_result = lines.next_line() => {
172+
match line_result? {
176173
Some(content) => {
177-
if cancel_token.is_cancelled() { break }
178-
179-
let line_results = line_search(&content, pattern, line_number);
180-
181-
for result in line_results {
182-
if let Err(e) = result_tx.send(result).await {
183-
eprintln!("Failed to send result: {}", e);
184-
break;
185-
}
174+
if cancel_token.is_cancelled() {
175+
break;
186176
}
187177

178+
let line_results = line_search(&content, pattern, line_number);
179+
results.extend(line_results);
188180
line_number += 1;
189181
}
190182
// End of file reached
191-
None => { break }
183+
None => {
184+
break;
185+
}
192186
}
193187
}
194-
_ = cancel_token.cancelled() => { break }
188+
_ = cancel_token.cancelled() => {
189+
break;
190+
}
195191
}
196192
}
197193
}
198194

199-
Ok(())
195+
Ok(results)
200196
}
201197

202198
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -240,37 +236,25 @@ pub async fn global_search(
240236
let handle = tokio::spawn(async move {
241237
let _permit = permit;
242238

243-
let (search_result_tx, mut search_result_rx) = mpsc::channel(100);
244-
let cancel = cancel_token.clone();
245-
246239
let file_path_str = path_buf.to_string_lossy().to_string();
247240
let display_path = relative_to_current_dir(&path_buf)
248241
.map(|p| p.to_string_lossy().to_string())
249242
.unwrap_or_else(|| file_path_str.clone());
250-
251-
tokio::select! {
252-
res = file_search(&file_path_str, &pattern, cancel, search_result_tx) => {
253-
if let Err(err) = res {
254-
// eprintln!("Error searching in file {}: {}", file_path_str, err);
255-
return;
256-
}
257-
}
258-
_ = cancel_token.cancelled() => {
243+
244+
let matches = match file_search(&file_path_str, &pattern, cancel_token.clone()).await {
245+
Ok(m) => m,
246+
Err(_err) => {
247+
// Error reading/searching file, skip it
259248
return;
260249
}
261-
}
262-
263-
let mut matches = Vec::new();
264-
while let Some(result) = search_result_rx.recv().await {
265-
matches.push(result);
266-
}
250+
};
267251

268252
if !matches.is_empty() {
269253
if result_tx.send(FileSearchResult {
270254
file_path: display_path,
271255
matches,
272256
}).await.is_err() {
273-
eprintln!("Global receiver dropped. Skipping results");
257+
// Global receiver dropped, skip results
274258
}
275259
}
276260
});
@@ -375,23 +359,12 @@ pub mod search_exp {
375359
let temp_file_path = temp_file.path().to_path_buf();
376360

377361
let cancel = CancellationToken::new();
378-
let (result_tx, mut result_rx) = mpsc::channel(10);
379-
380-
let handle = tokio::spawn(async move {
381-
file_search(
382-
temp_file_path.to_string_lossy().as_ref(),
383-
pattern,
384-
cancel,
385-
result_tx,
386-
).await.unwrap();
387-
});
388-
389-
let mut results = Vec::new();
390-
while let Some(result) = result_rx.recv().await {
391-
results.push(result);
392-
}
393362

394-
handle.await?;
363+
let results = file_search(
364+
temp_file_path.to_string_lossy().as_ref(),
365+
pattern,
366+
cancel,
367+
).await?;
395368

396369
println!("Results: {:?}", results);
397370

@@ -422,41 +395,23 @@ pub mod search_exp {
422395
let temp_file_path = temp_file.path().to_path_buf();
423396

424397
let cancel = CancellationToken::new();
425-
let (result_tx, mut result_rx) = mpsc::channel(10);
426-
427-
let cancel_clone = cancel.clone();
428398

429-
// Spawn the function in a task
430-
let handle = tokio::spawn(async move {
431-
file_search(
432-
temp_file_path.to_string_lossy().as_ref(),
433-
pattern,
434-
cancel_clone,
435-
result_tx,
436-
).await.unwrap();
437-
});
438-
439-
// Send cancellation signal after a short delay
440-
tokio::spawn(async move {
441-
// sleep(Duration::from_millis(10)).await; // Adjust the delay as needed
442-
cancel.cancel();
443-
});
399+
// Send cancellation signal immediately
400+
cancel.cancel();
444401

445-
// Collect results until cancellation
446-
let mut results = Vec::new();
447-
while let Some(result) = result_rx.recv().await {
448-
results.push(result);
449-
}
402+
// Search should return empty results when cancelled
403+
let results = file_search(
404+
temp_file_path.to_string_lossy().as_ref(),
405+
pattern,
406+
cancel,
407+
).await?;
450408

451409
println!("Results len: {}", results.len());
452410
println!("Results: {:?}", results);
453411

454412
// Assert that processing stopped before completing
455413
// We expect 0 results to be returned.
456414
assert!(results.len() == 0);
457-
458-
// Ensure the search task completes
459-
handle.await?;
460415

461416
Ok(())
462417
}
@@ -557,23 +512,12 @@ pub mod search_exp {
557512
let pattern = "second line\nthird line";
558513

559514
let cancel = CancellationToken::new();
560-
let (result_tx, mut result_rx) = mpsc::channel(10);
561-
562-
let handle = tokio::spawn(async move {
563-
file_search(
564-
temp_file_path.to_string_lossy().as_ref(),
565-
pattern,
566-
cancel,
567-
result_tx,
568-
).await.unwrap();
569-
});
570-
571-
let mut results = Vec::new();
572-
while let Some(result) = result_rx.recv().await {
573-
results.push(result);
574-
}
575515

576-
handle.await?;
516+
let results = file_search(
517+
temp_file_path.to_string_lossy().as_ref(),
518+
pattern,
519+
cancel,
520+
).await?;
577521

578522
assert_eq!(results.len(), 1);
579523
assert_eq!(results[0].line, 1); // second line is at index 1

anycode/components/Search.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@
208208
.search-preview {
209209
color: #888;
210210
}
211+
212+
.search-preview mark.search-match {
213+
background-color: rgba(92, 92, 92, 0.977);
214+
color: inherit;
215+
padding: 0;
216+
font-weight: 500;
217+
}
211218
.search-item:hover {
212219
color: #888;
213220
text-shadow: 0 0 6px rgba(255, 255, 255, 0.8);

anycode/components/Search.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,60 @@ const StopIcon = () => (
1414
</svg>
1515
);
1616

17+
interface SearchPreviewProps {
18+
match: SearchMatch;
19+
pattern: string;
20+
maxLength?: number;
21+
}
22+
23+
const SearchPreview = ({ match, pattern, maxLength = 100 }: SearchPreviewProps) => {
24+
const displayPreview = maxLength > 0 ? match.preview.slice(0, maxLength) : match.preview;
25+
26+
if (!pattern.trim()) {
27+
return <span className="search-preview" title={match.preview}>{displayPreview}</span>;
28+
}
29+
30+
// Preview is created as: chars[preview_start..preview_end]
31+
// where preview_start = max(0, match.column - 50)
32+
// So the match position in preview is: match.column - preview_start = min(50, match.column)
33+
const previewStart = Math.max(0, match.column - 50);
34+
const matchPositionInPreview = match.column - previewStart;
35+
const patternLength = pattern.length;
36+
37+
// Ensure we don't go beyond preview bounds
38+
if (matchPositionInPreview < 0 || matchPositionInPreview + patternLength > displayPreview.length) {
39+
// Fallback: try to find pattern in preview
40+
const matchIndex = displayPreview.indexOf(pattern);
41+
if (matchIndex === -1) {
42+
return <span className="search-preview" title={match.preview}>{displayPreview}</span>;
43+
}
44+
const beforeMatch = displayPreview.slice(0, matchIndex);
45+
const matchText = displayPreview.slice(matchIndex, matchIndex + patternLength);
46+
const afterMatch = displayPreview.slice(matchIndex + patternLength);
47+
48+
return (
49+
<span className="search-preview" title={match.preview}>
50+
{beforeMatch}
51+
<mark className="search-match">{matchText}</mark>
52+
{afterMatch}
53+
</span>
54+
);
55+
}
56+
57+
// Split preview using match.column position
58+
const beforeMatch = displayPreview.slice(0, matchPositionInPreview);
59+
const matchText = displayPreview.slice(matchPositionInPreview, matchPositionInPreview + patternLength);
60+
const afterMatch = displayPreview.slice(matchPositionInPreview + patternLength);
61+
62+
return (
63+
<span className="search-preview" title={match.preview}>
64+
{beforeMatch}
65+
<mark className="search-match">{matchText}</mark>
66+
{afterMatch}
67+
</span>
68+
);
69+
};
70+
1771
interface SearchProps {
1872
id: string;
1973
onEnter: (data: { id: string; pattern: string }) => void;
@@ -25,6 +79,7 @@ interface SearchProps {
2579

2680
const Search = ({ id, onEnter, onCancel, onMatchClick, results, searchEnded }: SearchProps) => {
2781
const [input, setInput] = useState("");
82+
const searchPatternRef = useRef("");
2883
const [visibleMatches, setVisibleMatches] = useState<Record<string, Set<string> | undefined>>({});
2984
const [elapsedTime, setElapsedTime] = useState<number>(0);
3085
const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -84,6 +139,7 @@ const Search = ({ id, onEnter, onCancel, onMatchClick, results, searchEnded }: S
84139
// Enter submits the search, Shift+Enter inserts newline
85140
if (e.key === "Enter" && !e.shiftKey) {
86141
e.preventDefault();
142+
searchPatternRef.current = input; // Save the pattern used for search
87143
if (onEnter) {
88144
onEnter({ id: id, pattern: input });
89145
}
@@ -145,6 +201,7 @@ const Search = ({ id, onEnter, onCancel, onMatchClick, results, searchEnded }: S
145201
<button
146202
className="search-button replay"
147203
onClick={() => {
204+
searchPatternRef.current = input; // Save the pattern used for search
148205
onEnter({ id: id, pattern: input });
149206
}}
150207
title="Replay search"
@@ -186,7 +243,7 @@ const Search = ({ id, onEnter, onCancel, onMatchClick, results, searchEnded }: S
186243
onClick={() => handleMatchClick(fileResult.file_path, match)}
187244
>
188245
<strong>{match.line + 1}:{match.column + 1} </strong>
189-
<span className="search-preview" title={match.preview}>{match.preview.slice(0, 100)}</span>
246+
<SearchPreview match={match} pattern={searchPatternRef.current} />
190247
</div>
191248
);
192249
})}

0 commit comments

Comments
 (0)