1use crate::args::Cli;
4use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
5use crate::index_discovery::find_nearest_index;
6use crate::output::{
7 DisplaySymbol, FormatterMetadata, JsonSymbol, OutputStreams, create_formatter,
8};
9use anyhow::{Context, Result};
10use regex::RegexBuilder;
11use sqry_core::graph::unified::concurrent::CodeGraph;
12use sqry_core::graph::unified::node::NodeKind;
13use sqry_core::json_response::{Filters, FuzzyFilters, Stats, StreamEvent};
14use sqry_core::search::fuzzy::{CandidateGenerator, FuzzyConfig};
15use sqry_core::search::matcher::{FuzzyMatcher, MatchAlgorithm, MatchConfig};
16use sqry_core::search::trigram::TrigramIndex;
17use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20use std::time::Instant;
21
22type ScoredSymbol = (DisplaySymbol, f64);
24
25fn apply_search_filters(cli: &Cli, symbols: &mut Vec<DisplaySymbol>) {
27 if let Some(kind) = cli.kind {
29 let target_type_str = kind.to_string().to_lowercase();
30 symbols.retain(|s| s.kind.to_lowercase() == target_type_str);
31 }
32
33 if let Some(ref lang) = cli.lang {
35 symbols.retain(|s| {
36 s.file_path
37 .extension()
38 .and_then(|ext| ext.to_str())
39 .is_some_and(|ext| matches_language(ext, lang))
40 });
41 }
42}
43
44fn build_search_metadata(
46 cli: &Cli,
47 pattern: &str,
48 scope_info: Option<&FuzzySearchScopeInfo>,
49 index_age_seconds: Option<u64>,
50 total_matches: usize,
51 execution_time: std::time::Duration,
52) -> FormatterMetadata {
53 let (used_ancestor_index, filtered_to) = if let Some(scope) = scope_info {
54 let used_ancestor = if scope.used_ancestor_index || scope.filtered_to.is_some() {
56 Some(scope.used_ancestor_index)
57 } else {
58 None
59 };
60 (used_ancestor, scope.filtered_to.clone())
61 } else {
62 (None, None)
63 };
64
65 FormatterMetadata {
66 pattern: Some(pattern.to_string()),
67 total_matches,
68 execution_time,
69 filters: build_filters(cli),
70 index_age_seconds,
71 used_ancestor_index,
72 filtered_to,
73 }
74}
75
76pub fn run_search(cli: &Cli, pattern: &str, search_path: &str) -> Result<()> {
82 if cli.json_stream {
84 return run_json_stream_search(cli, pattern, search_path);
85 }
86
87 let start_time = Instant::now();
88
89 let (mut all_symbols, index_age_seconds, scope_info) = if cli.fuzzy {
91 let (scored_symbols, age, scope) = run_fuzzy_search(cli, pattern, search_path)?;
92 let symbols = scored_symbols.into_iter().map(|(s, _)| s).collect();
93 (symbols, Some(age), Some(scope))
94 } else {
95 (run_regular_search(cli, pattern, search_path)?, None, None)
96 };
97
98 apply_search_filters(cli, &mut all_symbols);
99
100 if cli.count {
102 println!("{} matches found", all_symbols.len());
103 return Ok(());
104 }
105
106 let total_matches = all_symbols.len();
108
109 if let Some(sort_field) = cli.sort {
111 crate::commands::sort::sort_symbols(&mut all_symbols, sort_field);
112 }
113
114 let limit = cli.limit.unwrap_or(if cli.fuzzy { 50 } else { 100 });
115 let symbols_to_output = if all_symbols.len() > limit {
116 all_symbols.truncate(limit);
117 all_symbols
118 } else {
119 all_symbols
120 };
121
122 let execution_time = start_time.elapsed();
123
124 let metadata = build_search_metadata(
125 cli,
126 pattern,
127 scope_info.as_ref(),
128 index_age_seconds,
129 total_matches,
130 execution_time,
131 );
132
133 let formatter = create_formatter(cli);
134
135 let mut streams = OutputStreams::with_pager(cli.pager_config());
137 formatter.format(&symbols_to_output, Some(&metadata), &mut streams)?;
138
139 if !cli.json && total_matches > limit {
141 eprintln!("\nShowing {limit} of {total_matches} matches (use --limit to adjust)");
142 }
143
144 streams.finish_checked()
147}
148
149fn build_filters(cli: &Cli) -> Filters {
151 Filters {
152 kind: cli.kind.map(|k| k.to_string()),
153 lang: cli.lang.clone(),
154 ignore_case: cli.ignore_case,
155 exact: cli.exact,
156 fuzzy: if cli.fuzzy {
157 Some(FuzzyFilters {
158 algorithm: cli.fuzzy_algorithm.clone(),
159 threshold: cli.fuzzy_threshold,
160 max_candidates: Some(cli.fuzzy_max_candidates),
161 })
162 } else {
163 None
164 },
165 }
166}
167
168fn language_from_path(path: &Path) -> &'static str {
169 path.extension()
170 .and_then(|ext| ext.to_str())
171 .map_or("unknown", |ext| match ext.to_lowercase().as_str() {
172 "rs" => "rust",
173 "js" | "mjs" | "cjs" => "javascript",
174 "ts" | "mts" | "cts" => "typescript",
175 "jsx" => "javascriptreact",
176 "tsx" => "typescriptreact",
177 "py" | "pyw" => "python",
178 "rb" => "ruby",
179 "go" => "go",
180 "java" => "java",
181 "kt" | "kts" => "kotlin",
182 "scala" | "sc" => "scala",
183 "c" | "h" => "c",
184 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
185 "cs" => "csharp",
186 "php" => "php",
187 "swift" => "swift",
188 "sql" => "sql",
189 "dart" => "dart",
190 "lua" => "lua",
191 "sh" | "bash" | "zsh" => "shell",
192 "pl" | "pm" => "perl",
193 "groovy" | "gvy" => "groovy",
194 "ex" | "exs" => "elixir",
195 "r" | "R" => "r",
196 "hs" | "lhs" => "haskell",
197 "svelte" => "svelte",
198 "vue" => "vue",
199 "zig" => "zig",
200 "css" | "scss" | "sass" | "less" => "css",
201 "html" | "htm" => "html",
202 "tf" | "tfvars" => "terraform",
203 "pp" => "puppet",
204 "pls" | "plb" | "pck" => "plsql",
205 "cls" | "trigger" => "apex",
206 "abap" => "abap",
207 _ => "unknown",
208 })
209}
210
211fn matches_language(ext: &str, lang: &str) -> bool {
213 let ext_lower = ext.to_lowercase();
214 let lang_lower = lang.to_lowercase();
215
216 match lang_lower.as_str() {
217 "rust" | "rs" => ext_lower == "rs",
219 "javascript" | "js" => matches!(ext_lower.as_str(), "js" | "jsx" | "mjs" | "cjs"),
220 "typescript" | "ts" => matches!(ext_lower.as_str(), "ts" | "tsx"),
221 "python" | "py" => matches!(ext_lower.as_str(), "py" | "pyi" | "pyw"),
222 "go" => ext_lower == "go",
223 "java" => ext_lower == "java",
224
225 "swift" => ext_lower == "swift",
227 "c" => matches!(ext_lower.as_str(), "c" | "h"),
228 "cpp" | "c++" | "cxx" => {
229 matches!(
230 ext_lower.as_str(),
231 "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" | "h"
232 )
233 }
234 "csharp" | "c#" | "cs" => matches!(ext_lower.as_str(), "cs" | "csx"),
235 "dart" => ext_lower == "dart",
236 "kotlin" | "kt" => matches!(ext_lower.as_str(), "kt" | "kts"),
237 "ruby" | "rb" => matches!(ext_lower.as_str(), "rb" | "rake" | "gemspec"),
238 "scala" => matches!(ext_lower.as_str(), "scala" | "sc"),
239 "php" => ext_lower == "php",
240
241 "lua" => ext_lower == "lua",
243 "elixir" | "ex" => matches!(ext_lower.as_str(), "ex" | "exs"),
244 "haskell" | "hs" => matches!(ext_lower.as_str(), "hs" | "lhs"),
245 "perl" | "pl" => matches!(ext_lower.as_str(), "pl" | "pm"),
246 "r" => ext_lower == "r",
247 "shell" | "sh" | "bash" => matches!(ext_lower.as_str(), "sh" | "bash" | "zsh"),
248 "zig" => ext_lower == "zig",
249 "groovy" => matches!(ext_lower.as_str(), "groovy" | "gvy" | "gy" | "gsh"),
250
251 "vue" => ext_lower == "vue",
253 "svelte" => ext_lower == "svelte",
254 "html" => matches!(ext_lower.as_str(), "html" | "htm"),
255 "css" => matches!(ext_lower.as_str(), "css" | "scss" | "sass" | "less"),
256
257 "terraform" | "tf" | "hcl" => {
259 matches!(ext_lower.as_str(), "tf" | "tfvars" | "hcl")
260 }
261 "puppet" | "pp" => ext_lower == "pp",
262
263 "sql" => ext_lower == "sql",
265 "servicenow" | "servicenow-xanadu" | "servicenow-xanadu-js" | "snjs" => ext_lower == "snjs",
266 "apex" | "salesforce" => matches!(ext_lower.as_str(), "cls" | "trigger"),
267 "abap" => ext_lower == "abap",
268 "plsql" | "oracle-plsql" => matches!(ext_lower.as_str(), "pks" | "pkb" | "pls"),
269
270 _ => ext_lower == lang_lower,
272 }
273}
274
275fn run_regular_search(cli: &Cli, pattern: &str, search_path: &str) -> Result<Vec<DisplaySymbol>> {
277 let search_path_path = Path::new(search_path);
279 let index_location = find_nearest_index(search_path_path);
280 let index_root = index_location
281 .as_ref()
282 .map_or(search_path_path, |loc| loc.index_root.as_path());
283
284 let config = GraphLoadConfig::default();
285 let graph = load_unified_graph_for_cli(index_root, &config, cli)
286 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
287
288 let pattern_regex = build_pattern_regex(cli, pattern)?;
290
291 let mut matches = Vec::new();
293 let strings = graph.strings();
294 let indices = graph.indices();
295
296 if let Some(regex) = pattern_regex {
297 for (str_id, s) in strings.iter() {
299 if regex.is_match(s) {
300 matches.extend_from_slice(indices.by_qualified_name(str_id));
302 matches.extend_from_slice(indices.by_name(str_id));
303 }
304 }
305 } else {
306 let node_ids = graph.snapshot().find_by_pattern(pattern);
308 matches.extend(node_ids);
309 }
310
311 matches.sort_unstable();
313 matches.dedup();
314
315 let mut all_symbols = Vec::with_capacity(matches.len());
317
318 for node_id in matches {
319 if let Some(symbol) = convert_node_to_display_symbol(&graph, node_id) {
320 all_symbols.push(symbol);
321 }
322 }
323
324 Ok(all_symbols)
325}
326
327fn build_pattern_regex(cli: &Cli, pattern: &str) -> Result<Option<regex::Regex>> {
328 if cli.exact {
329 return Ok(None);
330 }
331
332 let regex = RegexBuilder::new(pattern)
333 .case_insensitive(cli.ignore_case)
334 .build()
335 .context("Invalid regex pattern")?;
336 Ok(Some(regex))
337}
338
339fn convert_node_to_display_symbol(
341 graph: &CodeGraph,
342 node_id: sqry_core::graph::unified::node::NodeId,
343) -> Option<DisplaySymbol> {
344 let entry = graph.nodes().get(node_id)?;
345 let strings = graph.strings();
346 let files = graph.files();
347
348 let name = strings
349 .resolve(entry.name)
350 .map(|s| s.to_string())
351 .unwrap_or_default();
352
353 let file_path = files
354 .resolve(entry.file)
355 .map(|s| PathBuf::from(s.as_ref()))
356 .unwrap_or_default();
357
358 let language = language_from_path(&file_path).to_string();
359
360 let mut metadata = HashMap::new();
361 metadata.insert(
362 "__raw_file_path".to_string(),
363 file_path.to_string_lossy().to_string(),
364 );
365 metadata.insert("__raw_language".to_string(), language.clone());
366
367 let qualified_name = entry
368 .qualified_name
369 .and_then(|id| strings.resolve(id))
370 .map_or_else(|| name.clone(), |s| s.to_string());
371
372 Some(DisplaySymbol {
373 name,
374 qualified_name,
375 kind: node_kind_to_string(entry.kind).to_string(),
376 file_path,
377 start_line: entry.start_line as usize,
378 start_column: entry.start_column as usize,
379 end_line: entry.end_line as usize,
380 end_column: entry.end_column as usize,
381 metadata,
382 caller_identity: None,
383 callee_identity: None,
384 })
385}
386
387fn node_kind_to_string(kind: NodeKind) -> &'static str {
389 match kind {
390 NodeKind::Function => "function",
391 NodeKind::Method => "method",
392 NodeKind::Class => "class",
393 NodeKind::Interface => "interface",
394 NodeKind::Trait => "trait",
395 NodeKind::Module => "module",
396 NodeKind::Variable => "variable",
397 NodeKind::Constant => "constant",
398 NodeKind::Type => "type",
399 NodeKind::Struct => "struct",
400 NodeKind::Enum => "enum",
401 NodeKind::EnumVariant => "enum_variant",
402 NodeKind::Macro => "macro",
403 NodeKind::Parameter => "parameter",
404 NodeKind::Property => "property",
405 NodeKind::Import => "import",
406 NodeKind::Export => "export",
407 NodeKind::Component => "component",
408 NodeKind::Service => "service",
409 NodeKind::Resource => "resource",
410 NodeKind::Endpoint => "endpoint",
411 NodeKind::Test => "test",
412 NodeKind::CallSite => "call_site",
413 NodeKind::StyleRule => "style_rule",
414 NodeKind::StyleAtRule => "style_at_rule",
415 NodeKind::StyleVariable => "style_variable",
416 NodeKind::Lifetime => "lifetime",
417 NodeKind::TypeParameter => "type_parameter",
418 NodeKind::Annotation => "annotation",
419 NodeKind::AnnotationValue => "annotation_value",
420 NodeKind::LambdaTarget => "lambda_target",
421 NodeKind::JavaModule => "java_module",
422 NodeKind::EnumConstant => "enum_constant",
423 NodeKind::Other => "other",
424 }
425}
426
427struct FuzzySearchScopeInfo {
429 used_ancestor_index: bool,
430 filtered_to: Option<String>,
431}
432
433struct FuzzyIndexResolution {
435 index_root: PathBuf,
436 scope_filter: Option<PathBuf>,
437 is_file_query: bool,
438 scope_info: FuzzySearchScopeInfo,
439}
440
441fn resolve_fuzzy_index(search_path: &Path) -> FuzzyIndexResolution {
443 let index_location = find_nearest_index(search_path);
444
445 if let Some(ref loc) = index_location {
446 let scope = if loc.requires_scope_filter {
447 loc.relative_scope()
448 } else {
449 None
450 };
451 let info = FuzzySearchScopeInfo {
452 used_ancestor_index: loc.is_ancestor,
453 filtered_to: scope.as_ref().map(|p| {
454 if loc.is_file_query {
455 p.to_string_lossy().into_owned()
456 } else {
457 format!("{}/**", p.display())
458 }
459 }),
460 };
461 FuzzyIndexResolution {
462 index_root: loc.index_root.clone(),
463 scope_filter: scope,
464 is_file_query: loc.is_file_query,
465 scope_info: info,
466 }
467 } else {
468 FuzzyIndexResolution {
469 index_root: search_path.to_path_buf(),
470 scope_filter: None,
471 is_file_query: false,
472 scope_info: FuzzySearchScopeInfo {
473 used_ancestor_index: false,
474 filtered_to: None,
475 },
476 }
477 }
478}
479
480fn build_trigram_index_from_graph(graph: &CodeGraph) -> Arc<TrigramIndex> {
482 let mut trigram_index = TrigramIndex::new();
483 for (str_id, s) in graph.strings().iter() {
484 trigram_index.add_symbol(str_id.index() as usize, s);
485 }
486 Arc::new(trigram_index)
487}
488
489fn run_fuzzy_search(
492 cli: &Cli,
493 pattern: &str,
494 search_path: &str,
495) -> Result<(Vec<ScoredSymbol>, u64, FuzzySearchScopeInfo)> {
496 let search_path_path = Path::new(search_path);
497
498 let resolution = resolve_fuzzy_index(search_path_path);
500 let FuzzyIndexResolution {
501 index_root,
502 scope_filter,
503 is_file_query,
504 scope_info,
505 } = resolution;
506
507 let config = GraphLoadConfig::default();
508 let graph = load_unified_graph_for_cli(&index_root, &config, cli)
509 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
510
511 let age_seconds = 0;
513
514 let trigram_index_arc = build_trigram_index_from_graph(&graph);
516
517 let algorithm = parse_fuzzy_algorithm(&cli.fuzzy_algorithm)?;
518 let fuzzy_config = build_fuzzy_config(cli, 0.1);
519 let match_config = build_match_config(cli, algorithm);
520
521 let generator = CandidateGenerator::with_config(trigram_index_arc, fuzzy_config);
523
524 maybe_log_fuzzy_config(cli, algorithm);
525
526 let candidate_ids = generator.generate(pattern);
528
529 if candidate_ids.is_empty() {
530 return Ok((Vec::new(), age_seconds, scope_info));
531 }
532
533 let matcher = FuzzyMatcher::with_config(match_config.clone());
535
536 let resolved_candidates: Vec<(usize, Arc<str>)> = candidate_ids
538 .iter()
539 .filter_map(|&id| {
540 let str_id = u32::try_from(id).ok()?;
541 let str_id = sqry_core::graph::unified::string::StringId::new(str_id);
542 graph.strings().resolve(str_id).map(|s| (id, s))
543 })
544 .collect();
545
546 let candidate_targets = resolved_candidates.iter().map(|(id, s)| (*id, s.as_ref()));
547
548 let match_results = matcher.match_many(pattern, candidate_targets);
550
551 let mut symbols = Vec::new();
553 let indices = graph.indices();
554
555 for result in match_results {
556 let Ok(str_id) = u32::try_from(result.entry_id) else {
557 continue;
558 };
559 let str_id = sqry_core::graph::unified::string::StringId::new(str_id);
560
561 let mut node_ids = Vec::new();
568 node_ids.extend_from_slice(indices.by_qualified_name(str_id));
569 node_ids.extend_from_slice(indices.by_name(str_id));
570 node_ids.sort_unstable();
571 node_ids.dedup();
572
573 for node_id in node_ids {
574 if let Some(symbol) = convert_node_to_display_symbol(&graph, node_id) {
575 symbols.push((symbol, result.score));
578 }
579 }
580 }
581
582 symbols.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
584
585 maybe_log_fuzzy_results(symbols.len());
586
587 let mut final_symbols = symbols;
588
589 if let Some(ref scope) = scope_filter {
591 filter_fuzzy_results_by_scope(&mut final_symbols, scope, is_file_query);
592 }
593
594 Ok((final_symbols, age_seconds, scope_info))
595}
596
597fn filter_fuzzy_results_by_scope(
599 symbols: &mut Vec<ScoredSymbol>,
600 scope: &Path,
601 is_file_query: bool,
602) {
603 symbols.retain(|(symbol, _)| {
604 if is_file_query {
605 symbol.file_path == scope
606 } else {
607 symbol.file_path.starts_with(scope)
608 }
609 });
610}
611
612fn run_json_stream_search(cli: &Cli, pattern: &str, search_path: &str) -> Result<()> {
613 let (mut symbols, age_seconds, scope_info) = run_fuzzy_search(cli, pattern, search_path)?;
614
615 apply_scored_search_filters(cli, &mut symbols);
617
618 let limit = cli.limit.unwrap_or(50);
619 let mut count = 0;
620
621 for (symbol, score) in symbols.iter().take(limit) {
622 let json_symbol = JsonSymbol::from(symbol);
623 let event = StreamEvent::PartialResult {
624 result: json_symbol,
625 score: *score,
626 };
627 let json = serde_json::to_string(&event)?;
628 println!("{json}");
629 count += 1;
630 }
631
632 emit_stream_summary(symbols.len(), count, age_seconds, Some(&scope_info))?;
633
634 Ok(())
635}
636
637fn apply_scored_search_filters(cli: &Cli, symbols: &mut Vec<ScoredSymbol>) {
639 if let Some(kind) = cli.kind {
640 let target_type_str = kind.to_string().to_lowercase();
641 symbols.retain(|(s, _)| s.kind.to_lowercase() == target_type_str);
642 }
643
644 if let Some(ref lang) = cli.lang {
645 symbols.retain(|(s, _)| {
646 s.file_path
647 .extension()
648 .and_then(|ext| ext.to_str())
649 .is_some_and(|ext| matches_language(ext, lang))
650 });
651 }
652}
653
654fn parse_fuzzy_algorithm(algorithm: &str) -> Result<MatchAlgorithm> {
655 match algorithm.to_lowercase().as_str() {
656 "levenshtein" => Ok(MatchAlgorithm::Levenshtein),
657 "jaro-winkler" | "jaro_winkler" => Ok(MatchAlgorithm::JaroWinkler),
658 _ => anyhow::bail!(
659 "Unknown fuzzy algorithm '{algorithm}'. Use 'levenshtein' or 'jaro-winkler'."
660 ),
661 }
662}
663
664fn build_fuzzy_config(cli: &Cli, min_similarity: f64) -> FuzzyConfig {
665 FuzzyConfig {
666 max_candidates: cli.fuzzy_max_candidates,
667 min_similarity,
668 }
669}
670
671fn build_match_config(cli: &Cli, algorithm: MatchAlgorithm) -> MatchConfig {
672 MatchConfig {
673 algorithm,
674 min_score: cli.fuzzy_threshold,
675 case_sensitive: !cli.ignore_case,
676 }
677}
678
679fn maybe_log_fuzzy_config(cli: &Cli, algorithm: MatchAlgorithm) {
680 if std::env::var("RUST_LOG").is_ok() {
681 eprintln!("[DEBUG] Using fuzzy algorithm: {algorithm:?}");
682 eprintln!("[DEBUG] Min score threshold: {}", cli.fuzzy_threshold);
683 }
684}
685
686fn maybe_log_fuzzy_results(count: usize) {
687 if std::env::var("RUST_LOG").is_ok() {
688 eprintln!("[DEBUG] Found {count} fuzzy matches");
689 }
690}
691
692fn emit_stream_summary(
693 final_count: usize,
694 total_streamed: usize,
695 age_seconds: u64,
696 scope_info: Option<&FuzzySearchScopeInfo>,
697) -> Result<()> {
698 let mut stats = Stats::new(final_count, total_streamed).with_index_age(age_seconds);
699 if let Some(scope) = scope_info
701 && (scope.used_ancestor_index || scope.filtered_to.is_some())
702 {
703 stats = stats.with_scope_info(scope.used_ancestor_index, scope.filtered_to.clone());
704 }
705 let summary = StreamEvent::<JsonSymbol>::FinalSummary { stats };
706 let json = serde_json::to_string(&summary)?;
707 println!("{json}");
708 Ok(())
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn test_matches_language_rust() {
717 assert!(matches_language("rs", "rust"));
718 assert!(matches_language("rs", "Rust"));
719 assert!(matches_language("rs", "rs"));
720 assert!(!matches_language("js", "rust"));
721 }
722
723 #[test]
724 fn test_matches_language_javascript() {
725 assert!(matches_language("js", "javascript"));
726 assert!(matches_language("jsx", "javascript"));
727 assert!(matches_language("js", "js"));
728 assert!(!matches_language("ts", "javascript"));
729 }
730
731 #[test]
732 fn test_matches_language_typescript() {
733 assert!(matches_language("ts", "typescript"));
734 assert!(matches_language("tsx", "typescript"));
735 assert!(matches_language("ts", "ts"));
736 assert!(!matches_language("js", "typescript"));
737 }
738
739 #[test]
740 fn test_matches_language_swift() {
741 assert!(matches_language("swift", "swift"));
742 assert!(matches_language("swift", "Swift"));
743 assert!(!matches_language("c", "swift"));
744 }
745
746 #[test]
747 fn test_matches_language_c() {
748 assert!(matches_language("c", "c"));
749 assert!(matches_language("h", "c"));
750 assert!(matches_language("C", "c"));
751 assert!(!matches_language("cpp", "c"));
752 }
753
754 #[test]
755 fn test_matches_language_cpp() {
756 assert!(matches_language("cpp", "cpp"));
757 assert!(matches_language("cc", "cpp"));
758 assert!(matches_language("cxx", "cpp"));
759 assert!(matches_language("hpp", "cpp"));
760 assert!(matches_language("hh", "cpp"));
761 assert!(matches_language("hxx", "cpp"));
762 assert!(matches_language("h", "cpp")); assert!(matches_language("cpp", "c++")); assert!(!matches_language("c", "cpp"));
765 }
766
767 #[test]
768 fn test_matches_language_csharp() {
769 assert!(matches_language("cs", "csharp"));
770 assert!(matches_language("cs", "c#"));
771 assert!(matches_language("csx", "csharp"));
772 assert!(matches_language("cs", "CSharp"));
773 assert!(!matches_language("cpp", "csharp"));
774 }
775
776 #[test]
777 fn test_matches_language_dart() {
778 assert!(matches_language("dart", "dart"));
779 assert!(matches_language("dart", "Dart"));
780 assert!(!matches_language("d", "dart"));
781 }
782
783 #[test]
784 fn test_matches_language_sql() {
785 assert!(matches_language("sql", "sql"));
786 assert!(matches_language("sql", "SQL"));
787 assert!(!matches_language("rs", "sql"));
788 }
789
790 #[test]
791 fn test_matches_language_servicenow() {
792 assert!(matches_language("snjs", "servicenow"));
793 assert!(matches_language("snjs", "ServiceNow-Xanadu"));
794 assert!(matches_language("snjs", "servicenow-xanadu-js"));
795 assert!(!matches_language("js", "servicenow"));
796 }
797}