diff --git a/crates/pi-natives/src/ast.rs b/crates/pi-natives/src/ast.rs index 84bcbc5c7..b9b4dc112 100644 --- a/crates/pi-natives/src/ast.rs +++ b/crates/pi-natives/src/ast.rs @@ -14,7 +14,6 @@ use ast_grep_core::{ use ast_grep_language::SupportLang; use napi::bindgen_prelude::*; use napi_derive::napi; -use rayon::prelude::*; use crate::{fs_cache, glob_util, task}; @@ -486,13 +485,6 @@ fn apply_edits(content: &str, edits: &[Edit]) -> Result { Ok(output) } -struct PatternFindResultData { - pattern: String, - matches: Vec, - total_matches: u32, - parse_errors: Vec, -} - fn normalize_pattern_list(patterns: Option>) -> Result> { let mut normalized = Vec::new(); let mut seen = BTreeSet::new(); @@ -534,97 +526,85 @@ fn normalize_rewrite_map( normalized.sort_by(|left, right| left.0.cmp(&right.0)); Ok(normalized) } +struct CompiledFindPattern { + pattern: String, + compiled_by_lang: HashMap, + compile_errors_by_lang: HashMap, +} -fn run_find_for_pattern( - pattern: &str, - candidates: &[FileCandidate], +struct ResolvedCandidate { + candidate: FileCandidate, + language: Option, + language_error: Option, +} + +fn resolve_candidates_for_find( + candidates: Vec, lang: Option<&str>, - selector: Option<&str>, - strictness: &MatchStrictness, - include_meta: bool, ct: &task::CancelToken, -) -> Result { - let mut matches = Vec::new(); - let mut parse_errors = Vec::new(); - let mut total_matches = 0u32; - let mut compiled_cache: HashMap = HashMap::new(); - let mut compile_errors: HashMap = HashMap::new(); +) -> Result<(Vec, HashMap)> { + let mut resolved = Vec::with_capacity(candidates.len()); + let mut languages = HashMap::new(); for candidate in candidates { ct.heartbeat()?; - let source = match std::fs::read_to_string(&candidate.absolute_path) { - Ok(source) => source, - Err(err) => { - parse_errors.push(format!("{pattern}: {}: {err}", candidate.display_path)); - continue; + match resolve_language(lang, &candidate.absolute_path) { + Ok(language) => { + let key = canonical_lang_name(language).to_string(); + languages.entry(key).or_insert(language); + resolved.push(ResolvedCandidate { + candidate, + language: Some(language), + language_error: None, + }); }, - }; - - let language = match resolve_language(lang, &candidate.absolute_path) { - Ok(language) => language, Err(err) => { - parse_errors.push(format!("{pattern}: {}: {err}", candidate.display_path)); - continue; + resolved.push(ResolvedCandidate { + candidate, + language: None, + language_error: Some(err.to_string()), + }); }, - }; - - let lang_key = canonical_lang_name(language).to_string(); - if let Some(error) = compile_errors.get(&lang_key) { - parse_errors.push(format!("{pattern}: {}: {error}", candidate.display_path)); - continue; - } - let compiled = if let Some(existing) = compiled_cache.get(&lang_key) { - existing.clone() - } else { - match compile_pattern(pattern, selector, strictness, language) { - Ok(compiled) => { - compiled_cache.insert(lang_key.clone(), compiled.clone()); - compiled - }, - Err(err) => { - let message = err.to_string(); - compile_errors.insert(lang_key, message.clone()); - parse_errors.push(format!("{pattern}: {}: {message}", candidate.display_path)); - continue; - }, - } - }; - - let ast = language.ast_grep(source); - if has_syntax_error(&ast) { - parse_errors.push(format!( - "{pattern}: {}: parse error (syntax tree contains error nodes)", - candidate.display_path - )); - continue; - } - - for matched in ast.root().find_all(compiled.clone()) { - ct.heartbeat()?; - total_matches = total_matches.saturating_add(1); - let range = matched.range(); - let start = matched.start_pos(); - let end = matched.end_pos(); - let meta_variables = if include_meta { - Some(HashMap::::from(matched.get_env().clone())) - } else { - None - }; - matches.push(AstFindMatch { - path: candidate.display_path.clone(), - text: matched.text().into_owned(), - byte_start: to_u32(range.start), - byte_end: to_u32(range.end), - start_line: to_u32(start.line().saturating_add(1)), - start_column: to_u32(start.column(matched.get_node()).saturating_add(1)), - end_line: to_u32(end.line().saturating_add(1)), - end_column: to_u32(end.column(matched.get_node()).saturating_add(1)), - meta_variables, - }); } } - Ok(PatternFindResultData { pattern: pattern.to_string(), matches, total_matches, parse_errors }) + Ok((resolved, languages)) +} + +fn compile_find_patterns( + patterns: &[String], + languages: &HashMap, + selector: Option<&str>, + strictness: &MatchStrictness, + ct: &task::CancelToken, +) -> Result> { + let mut compiled = Vec::with_capacity(patterns.len()); + + for pattern in patterns { + ct.heartbeat()?; + let mut compiled_by_lang = HashMap::with_capacity(languages.len()); + let mut compile_errors_by_lang = HashMap::new(); + + for (lang_key, &language) in languages { + ct.heartbeat()?; + match compile_pattern(pattern, selector, strictness, language) { + Ok(compiled_pattern) => { + compiled_by_lang.insert(lang_key.clone(), compiled_pattern); + }, + Err(err) => { + compile_errors_by_lang.insert(lang_key.clone(), err.to_string()); + }, + } + } + + compiled.push(CompiledFindPattern { + pattern: pattern.clone(), + compiled_by_lang, + compile_errors_by_lang, + }); + } + + Ok(compiled) } #[napi(js_name = "astGrep")] pub fn ast_grep(options: AstFindOptions<'_>) -> task::Async { @@ -657,32 +637,96 @@ pub fn ast_grep(options: AstFindOptions<'_>) -> task::Async { .filter(|candidate| is_supported_file(&candidate.absolute_path, lang_str)) .collect(); - let mut pattern_results = patterns - .par_iter() - .map(|pattern| { - run_find_for_pattern( - pattern, - &candidates, - lang_str, - selector.as_deref(), - &strictness, - include_meta, - &ct, - ) - }) - .collect::>>()?; - pattern_results.sort_by(|left, right| left.pattern.cmp(&right.pattern)); + let (resolved_candidates, languages) = + resolve_candidates_for_find(candidates, lang_str, &ct)?; + let compiled_patterns = + compile_find_patterns(&patterns, &languages, selector.as_deref(), &strictness, &ct)?; + let files_searched = to_u32(resolved_candidates.len()); let mut all_matches = Vec::new(); let mut parse_errors = Vec::new(); let mut total_matches = 0u32; let mut files_with_matches = BTreeSet::new(); - for pattern_result in pattern_results { - total_matches = total_matches.saturating_add(pattern_result.total_matches); - parse_errors.extend(pattern_result.parse_errors); - for matched in pattern_result.matches { - files_with_matches.insert(matched.path.clone()); - all_matches.push(matched); + for resolved in resolved_candidates { + ct.heartbeat()?; + let ResolvedCandidate { candidate, language, language_error } = resolved; + + if let Some(error) = language_error.as_deref() { + for compiled in &compiled_patterns { + parse_errors + .push(format!("{}: {}: {error}", compiled.pattern, candidate.display_path)); + } + continue; + } + + let Some(language) = language else { + continue; + }; + let lang_key = canonical_lang_name(language); + let source = match std::fs::read_to_string(&candidate.absolute_path) { + Ok(source) => source, + Err(err) => { + for compiled in &compiled_patterns { + parse_errors + .push(format!("{}: {}: {err}", compiled.pattern, candidate.display_path)); + } + continue; + }, + }; + + let mut runnable_patterns: Vec<(&str, &Pattern)> = Vec::new(); + for compiled in &compiled_patterns { + ct.heartbeat()?; + if let Some(error) = compiled.compile_errors_by_lang.get(lang_key) { + parse_errors + .push(format!("{}: {}: {error}", compiled.pattern, candidate.display_path)); + continue; + } + if let Some(pattern) = compiled.compiled_by_lang.get(lang_key) { + runnable_patterns.push((compiled.pattern.as_str(), pattern)); + } + } + if runnable_patterns.is_empty() { + continue; + } + + let ast = language.ast_grep(source); + if has_syntax_error(&ast) { + for (pattern_name, _) in &runnable_patterns { + parse_errors.push(format!( + "{pattern_name}: {}: parse error (syntax tree contains error nodes)", + candidate.display_path + )); + } + continue; + } + + for (_, pattern) in runnable_patterns { + ct.heartbeat()?; + for matched in ast.root().find_all(pattern.clone()) { + ct.heartbeat()?; + total_matches = total_matches.saturating_add(1); + let range = matched.range(); + let start = matched.start_pos(); + let end = matched.end_pos(); + let meta_variables = if include_meta { + Some(HashMap::::from(matched.get_env().clone())) + } else { + None + }; + all_matches.push(AstFindMatch { + path: candidate.display_path.clone(), + text: matched.text().into_owned(), + byte_start: to_u32(range.start), + byte_end: to_u32(range.end), + start_line: to_u32(start.line().saturating_add(1)), + start_column: to_u32(start.column(matched.get_node()).saturating_add(1)), + end_line: to_u32(end.line().saturating_add(1)), + end_column: to_u32(end.column(matched.get_node()).saturating_add(1)), + meta_variables, + }); + files_with_matches.insert(candidate.display_path.clone()); + } } } @@ -712,7 +756,7 @@ pub fn ast_grep(options: AstFindOptions<'_>) -> task::Async { matches, total_matches, files_with_matches: to_u32(files_with_matches.len()), - files_searched: to_u32(candidates.len()), + files_searched, limit_reached, parse_errors: (!parse_errors.is_empty()).then_some(parse_errors), }) @@ -760,6 +804,7 @@ pub fn ast_edit(options: AstReplaceOptions<'_>) -> task::Async let mut parse_errors = Vec::new(); let mut compiled_rules = Vec::new(); for (pattern, rewrite) in rewrite_rules { + ct.heartbeat()?; match compile_pattern(&pattern, selector.as_deref(), &strictness, language) { Ok(compiled) => compiled_rules.push((pattern, rewrite, compiled)), Err(err) => { diff --git a/crates/pi-natives/src/fd.rs b/crates/pi-natives/src/fd.rs index 50730f90a..47151b15b 100644 --- a/crates/pi-natives/src/fd.rs +++ b/crates/pi-natives/src/fd.rs @@ -74,8 +74,7 @@ fn normalize_fuzzy_text(value: &str) -> String { } /// Scores a query as a subsequence of `target`. Returns 0 if not a subsequence. -fn fuzzy_subsequence_score(query: &str, target: &str) -> u32 { - let query_chars: Vec = query.chars().collect(); +fn fuzzy_subsequence_score(query_chars: &[char], target: &str) -> u32 { if query_chars.is_empty() { return 1; } @@ -109,34 +108,42 @@ fn score_fuzzy_path( is_directory: bool, query_lower: &str, normalized_query: &str, + query_chars: &[char], ) -> u32 { - let lower_path = path.to_lowercase(); - let normalized_path = normalize_fuzzy_text(path); - let file_name_source = path.trim_end_matches('/'); - let file_name = Path::new(file_name_source) + if query_lower.is_empty() { + return if is_directory { 11 } else { 1 }; + } + + let file_name = Path::new(path) .file_name() .and_then(|name| name.to_str()) - .unwrap_or(file_name_source); + .unwrap_or(path); let lower_file_name = file_name.to_lowercase(); - let normalized_file_name = normalize_fuzzy_text(file_name); - let mut score = if query_lower.is_empty() { - 1 - } else if lower_file_name == query_lower { + let mut score = if lower_file_name == query_lower { 120 } else if lower_file_name.starts_with(query_lower) { 100 } else if lower_file_name.contains(query_lower) { 80 - } else if lower_path.contains(query_lower) { - 60 } else { - let file_name_fuzzy = fuzzy_subsequence_score(normalized_query, &normalized_file_name); - if file_name_fuzzy > 0 { - 50 + file_name_fuzzy + let lower_path = path.to_lowercase(); + if lower_path.contains(query_lower) { + 60 } else { - let path_fuzzy = fuzzy_subsequence_score(normalized_query, &normalized_path); - if path_fuzzy > 0 { 30 + path_fuzzy } else { 0 } + let normalized_file_name = normalize_fuzzy_text(file_name); + let file_name_fuzzy = fuzzy_subsequence_score(query_chars, &normalized_file_name); + if file_name_fuzzy > 0 { + 50 + file_name_fuzzy + } else { + let normalized_path = normalize_fuzzy_text(path); + let path_fuzzy = if normalized_path == normalized_query { + 40 + } else { + fuzzy_subsequence_score(query_chars, &normalized_path) + }; + if path_fuzzy > 0 { 30 + path_fuzzy } else { 0 } + } } }; @@ -172,6 +179,7 @@ fn fuzzy_find_sync(config: FuzzyFindConfig, ct: task::CancelToken) -> Result = normalized_query.chars().collect(); if !query_lower.is_empty() && normalized_query.is_empty() { return Ok(FuzzyFindResult { matches: Vec::new(), total_matches: 0 }); } @@ -179,7 +187,8 @@ fn fuzzy_find_sync(config: FuzzyFindConfig, ct: task::CancelToken) -> Result Result= fs_cache::empty_recheck_ms() { let fresh = fs_cache::force_rescan(&root, include_hidden, respect_gitignore, true, &ct)?; - scored = score_entries(&fresh, &query_lower, &normalized_query, &ct)?; + scored = score_entries(&fresh, &query_lower, &normalized_query, &query_chars, &ct)?; } scored } else { let fresh = fs_cache::force_rescan(&root, include_hidden, respect_gitignore, false, &ct)?; - score_entries(&fresh, &query_lower, &normalized_query, &ct)? + score_entries(&fresh, &query_lower, &normalized_query, &query_chars, &ct)? }; scored.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.path.cmp(&b.path))); @@ -206,9 +215,10 @@ fn score_entries( entries: &[fs_cache::GlobMatch], query_lower: &str, normalized_query: &str, + query_chars: &[char], ct: &task::CancelToken, ) -> Result> { - let mut scored = Vec::new(); + let mut scored = Vec::with_capacity(entries.len().min(256)); for entry in entries { ct.heartbeat()?; if entry.file_type == fs_cache::FileType::Symlink { @@ -216,16 +226,16 @@ fn score_entries( } let is_directory = entry.file_type == fs_cache::FileType::Dir; - let path = if is_directory { - format!("{}/", entry.path) - } else { - entry.path.clone() - }; - let score = score_fuzzy_path(&path, is_directory, query_lower, normalized_query); + let score = + score_fuzzy_path(&entry.path, is_directory, query_lower, normalized_query, query_chars); if score == 0 { continue; } + let mut path = entry.path.clone(); + if is_directory { + path.push('/'); + } scored.push(FuzzyFindMatch { path, is_directory, score }); } Ok(scored) diff --git a/crates/pi-natives/src/pty.rs b/crates/pi-natives/src/pty.rs index d9332c4d4..fc1cab7eb 100644 --- a/crates/pi-natives/src/pty.rs +++ b/crates/pi-natives/src/pty.rs @@ -191,12 +191,12 @@ impl PtySession { } } +#[cfg(unix)] fn terminate_pty_processes( child: &mut Box, child_pid: Option, process_group_id: Option, ) { - #[cfg(unix)] if let Some(pgid) = process_group_id { let _ = crate::ps::kill_process_group(pgid, TERM_SIGNAL); } @@ -207,7 +207,6 @@ fn terminate_pty_processes( let _ = child.kill(); - #[cfg(unix)] if let Some(pgid) = process_group_id { let _ = crate::ps::kill_process_group(pgid, KILL_SIGNAL); } @@ -217,6 +216,23 @@ fn terminate_pty_processes( } } +#[cfg(not(unix))] +fn terminate_pty_processes( + child: &mut Box, + child_pid: Option, + _process_group_id: Option, +) { + if let Some(pid) = child_pid { + let _ = crate::ps::kill_tree(pid, TERM_SIGNAL); + } + + let _ = child.kill(); + + if let Some(pid) = child_pid { + let _ = crate::ps::kill_tree(pid, KILL_SIGNAL); + } +} + fn run_pty_sync( config: PtyRunConfig, on_chunk: Option>, @@ -396,7 +412,22 @@ fn run_pty_sync( break; } if exit_code.is_none() || !reader_done { - std::thread::sleep(Duration::from_millis(16)); + let wait_duration = reader_drain_deadline.map_or(Duration::from_millis(16), |deadline| { + deadline + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(16)) + }); + match reader_rx.recv_timeout(wait_duration) { + Ok(ReaderEvent::Chunk(chunk)) => emit_chunk(&chunk, on_chunk.as_ref()), + Ok(ReaderEvent::Done) => reader_done = true, + Err(mpsc::RecvTimeoutError::Timeout) => {}, + Err(mpsc::RecvTimeoutError::Disconnected) => { + reader_done = true; + if exit_code.is_none() { + std::thread::sleep(wait_duration); + } + }, + } } } if exit_code.is_none() { @@ -421,14 +452,16 @@ fn run_pty_sync( if !reader_done { let finalize_deadline = Instant::now() + FINAL_READER_DRAIN_TIMEOUT; while Instant::now() < finalize_deadline { - match reader_rx.try_recv() { + let remaining = finalize_deadline.saturating_duration_since(Instant::now()); + let wait_duration = remaining.min(Duration::from_millis(5)); + match reader_rx.recv_timeout(wait_duration) { Ok(ReaderEvent::Chunk(chunk)) => emit_chunk(&chunk, on_chunk.as_ref()), Ok(ReaderEvent::Done) => { reader_done = true; break; }, - Err(mpsc::TryRecvError::Empty) => std::thread::sleep(Duration::from_millis(5)), - Err(mpsc::TryRecvError::Disconnected) => { + Err(mpsc::RecvTimeoutError::Timeout) => {}, + Err(mpsc::RecvTimeoutError::Disconnected) => { reader_done = true; break; }, diff --git a/packages/coding-agent/src/modes/components/settings-defs.ts b/packages/coding-agent/src/modes/components/settings-defs.ts index 46f8ec2d3..5813ca1bb 100644 --- a/packages/coding-agent/src/modes/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/components/settings-defs.ts @@ -206,6 +206,11 @@ const OPTION_PROVIDERS: Partial> = { { value: "openai", label: "OpenAI", description: "api.kimi.com" }, { value: "anthropic", label: "Anthropic", description: "api.moonshot.ai" }, ], + "providers.openaiWebsockets": [ + { value: "auto", label: "Auto", description: "Use model/provider default websocket behavior" }, + { value: "off", label: "Off", description: "Disable websockets for OpenAI Codex models" }, + { value: "on", label: "On", description: "Force websockets for OpenAI Codex models" }, + ], // Default thinking level defaultThinkingLevel: [ { value: "off", label: "off", description: "No reasoning" },