1use crate::args::Cli;
4use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph};
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(index_root, &config)
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::Other => "other",
418 }
419}
420
421struct FuzzySearchScopeInfo {
423 used_ancestor_index: bool,
424 filtered_to: Option<String>,
425}
426
427struct FuzzyIndexResolution {
429 index_root: PathBuf,
430 scope_filter: Option<PathBuf>,
431 is_file_query: bool,
432 scope_info: FuzzySearchScopeInfo,
433}
434
435fn resolve_fuzzy_index(search_path: &Path) -> FuzzyIndexResolution {
437 let index_location = find_nearest_index(search_path);
438
439 if let Some(ref loc) = index_location {
440 let scope = if loc.requires_scope_filter {
441 loc.relative_scope()
442 } else {
443 None
444 };
445 let info = FuzzySearchScopeInfo {
446 used_ancestor_index: loc.is_ancestor,
447 filtered_to: scope.as_ref().map(|p| {
448 if loc.is_file_query {
449 p.to_string_lossy().into_owned()
450 } else {
451 format!("{}/**", p.display())
452 }
453 }),
454 };
455 FuzzyIndexResolution {
456 index_root: loc.index_root.clone(),
457 scope_filter: scope,
458 is_file_query: loc.is_file_query,
459 scope_info: info,
460 }
461 } else {
462 FuzzyIndexResolution {
463 index_root: search_path.to_path_buf(),
464 scope_filter: None,
465 is_file_query: false,
466 scope_info: FuzzySearchScopeInfo {
467 used_ancestor_index: false,
468 filtered_to: None,
469 },
470 }
471 }
472}
473
474fn build_trigram_index_from_graph(graph: &CodeGraph) -> Arc<TrigramIndex> {
476 let mut trigram_index = TrigramIndex::new();
477 for (str_id, s) in graph.strings().iter() {
478 trigram_index.add_symbol(str_id.index() as usize, s);
479 }
480 Arc::new(trigram_index)
481}
482
483fn run_fuzzy_search(
486 cli: &Cli,
487 pattern: &str,
488 search_path: &str,
489) -> Result<(Vec<ScoredSymbol>, u64, FuzzySearchScopeInfo)> {
490 let search_path_path = Path::new(search_path);
491
492 let resolution = resolve_fuzzy_index(search_path_path);
494 let FuzzyIndexResolution {
495 index_root,
496 scope_filter,
497 is_file_query,
498 scope_info,
499 } = resolution;
500
501 let config = GraphLoadConfig::default();
502 let graph = load_unified_graph(&index_root, &config)
503 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
504
505 let age_seconds = 0;
507
508 let trigram_index_arc = build_trigram_index_from_graph(&graph);
510
511 let algorithm = parse_fuzzy_algorithm(&cli.fuzzy_algorithm)?;
512 let fuzzy_config = build_fuzzy_config(cli, 0.1);
513 let match_config = build_match_config(cli, algorithm);
514
515 let generator = CandidateGenerator::with_config(trigram_index_arc, fuzzy_config);
517
518 maybe_log_fuzzy_config(cli, algorithm);
519
520 let candidate_ids = generator.generate(pattern);
522
523 if candidate_ids.is_empty() {
524 return Ok((Vec::new(), age_seconds, scope_info));
525 }
526
527 let matcher = FuzzyMatcher::with_config(match_config.clone());
529
530 let resolved_candidates: Vec<(usize, Arc<str>)> = candidate_ids
532 .iter()
533 .filter_map(|&id| {
534 let str_id = u32::try_from(id).ok()?;
535 let str_id = sqry_core::graph::unified::string::StringId::new(str_id);
536 graph.strings().resolve(str_id).map(|s| (id, s))
537 })
538 .collect();
539
540 let candidate_targets = resolved_candidates.iter().map(|(id, s)| (*id, s.as_ref()));
541
542 let match_results = matcher.match_many(pattern, candidate_targets);
544
545 let mut symbols = Vec::new();
547 let indices = graph.indices();
548
549 for result in match_results {
550 let Ok(str_id) = u32::try_from(result.entry_id) else {
551 continue;
552 };
553 let str_id = sqry_core::graph::unified::string::StringId::new(str_id);
554
555 let mut node_ids = Vec::new();
562 node_ids.extend_from_slice(indices.by_qualified_name(str_id));
563 node_ids.extend_from_slice(indices.by_name(str_id));
564 node_ids.sort_unstable();
565 node_ids.dedup();
566
567 for node_id in node_ids {
568 if let Some(symbol) = convert_node_to_display_symbol(&graph, node_id) {
569 symbols.push((symbol, result.score));
572 }
573 }
574 }
575
576 symbols.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
578
579 maybe_log_fuzzy_results(symbols.len());
580
581 let mut final_symbols = symbols;
582
583 if let Some(ref scope) = scope_filter {
585 filter_fuzzy_results_by_scope(&mut final_symbols, scope, is_file_query);
586 }
587
588 Ok((final_symbols, age_seconds, scope_info))
589}
590
591fn filter_fuzzy_results_by_scope(
593 symbols: &mut Vec<ScoredSymbol>,
594 scope: &Path,
595 is_file_query: bool,
596) {
597 symbols.retain(|(symbol, _)| {
598 if is_file_query {
599 symbol.file_path == scope
600 } else {
601 symbol.file_path.starts_with(scope)
602 }
603 });
604}
605
606fn run_json_stream_search(cli: &Cli, pattern: &str, search_path: &str) -> Result<()> {
607 let (mut symbols, age_seconds, scope_info) = run_fuzzy_search(cli, pattern, search_path)?;
608
609 apply_scored_search_filters(cli, &mut symbols);
611
612 let limit = cli.limit.unwrap_or(50);
613 let mut count = 0;
614
615 for (symbol, score) in symbols.iter().take(limit) {
616 let json_symbol = JsonSymbol::from(symbol);
617 let event = StreamEvent::PartialResult {
618 result: json_symbol,
619 score: *score,
620 };
621 let json = serde_json::to_string(&event)?;
622 println!("{json}");
623 count += 1;
624 }
625
626 emit_stream_summary(symbols.len(), count, age_seconds, Some(&scope_info))?;
627
628 Ok(())
629}
630
631fn apply_scored_search_filters(cli: &Cli, symbols: &mut Vec<ScoredSymbol>) {
633 if let Some(kind) = cli.kind {
634 let target_type_str = kind.to_string().to_lowercase();
635 symbols.retain(|(s, _)| s.kind.to_lowercase() == target_type_str);
636 }
637
638 if let Some(ref lang) = cli.lang {
639 symbols.retain(|(s, _)| {
640 s.file_path
641 .extension()
642 .and_then(|ext| ext.to_str())
643 .is_some_and(|ext| matches_language(ext, lang))
644 });
645 }
646}
647
648fn parse_fuzzy_algorithm(algorithm: &str) -> Result<MatchAlgorithm> {
649 match algorithm.to_lowercase().as_str() {
650 "levenshtein" => Ok(MatchAlgorithm::Levenshtein),
651 "jaro-winkler" | "jaro_winkler" => Ok(MatchAlgorithm::JaroWinkler),
652 _ => anyhow::bail!(
653 "Unknown fuzzy algorithm '{algorithm}'. Use 'levenshtein' or 'jaro-winkler'."
654 ),
655 }
656}
657
658fn build_fuzzy_config(cli: &Cli, min_similarity: f64) -> FuzzyConfig {
659 FuzzyConfig {
660 max_candidates: cli.fuzzy_max_candidates,
661 min_similarity,
662 }
663}
664
665fn build_match_config(cli: &Cli, algorithm: MatchAlgorithm) -> MatchConfig {
666 MatchConfig {
667 algorithm,
668 min_score: cli.fuzzy_threshold,
669 case_sensitive: !cli.ignore_case,
670 }
671}
672
673fn maybe_log_fuzzy_config(cli: &Cli, algorithm: MatchAlgorithm) {
674 if std::env::var("RUST_LOG").is_ok() {
675 eprintln!("[DEBUG] Using fuzzy algorithm: {algorithm:?}");
676 eprintln!("[DEBUG] Min score threshold: {}", cli.fuzzy_threshold);
677 }
678}
679
680fn maybe_log_fuzzy_results(count: usize) {
681 if std::env::var("RUST_LOG").is_ok() {
682 eprintln!("[DEBUG] Found {count} fuzzy matches");
683 }
684}
685
686fn emit_stream_summary(
687 final_count: usize,
688 total_streamed: usize,
689 age_seconds: u64,
690 scope_info: Option<&FuzzySearchScopeInfo>,
691) -> Result<()> {
692 let mut stats = Stats::new(final_count, total_streamed).with_index_age(age_seconds);
693 if let Some(scope) = scope_info
695 && (scope.used_ancestor_index || scope.filtered_to.is_some())
696 {
697 stats = stats.with_scope_info(scope.used_ancestor_index, scope.filtered_to.clone());
698 }
699 let summary = StreamEvent::<JsonSymbol>::FinalSummary { stats };
700 let json = serde_json::to_string(&summary)?;
701 println!("{json}");
702 Ok(())
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn test_matches_language_rust() {
711 assert!(matches_language("rs", "rust"));
712 assert!(matches_language("rs", "Rust"));
713 assert!(matches_language("rs", "rs"));
714 assert!(!matches_language("js", "rust"));
715 }
716
717 #[test]
718 fn test_matches_language_javascript() {
719 assert!(matches_language("js", "javascript"));
720 assert!(matches_language("jsx", "javascript"));
721 assert!(matches_language("js", "js"));
722 assert!(!matches_language("ts", "javascript"));
723 }
724
725 #[test]
726 fn test_matches_language_typescript() {
727 assert!(matches_language("ts", "typescript"));
728 assert!(matches_language("tsx", "typescript"));
729 assert!(matches_language("ts", "ts"));
730 assert!(!matches_language("js", "typescript"));
731 }
732
733 #[test]
734 fn test_matches_language_swift() {
735 assert!(matches_language("swift", "swift"));
736 assert!(matches_language("swift", "Swift"));
737 assert!(!matches_language("c", "swift"));
738 }
739
740 #[test]
741 fn test_matches_language_c() {
742 assert!(matches_language("c", "c"));
743 assert!(matches_language("h", "c"));
744 assert!(matches_language("C", "c"));
745 assert!(!matches_language("cpp", "c"));
746 }
747
748 #[test]
749 fn test_matches_language_cpp() {
750 assert!(matches_language("cpp", "cpp"));
751 assert!(matches_language("cc", "cpp"));
752 assert!(matches_language("cxx", "cpp"));
753 assert!(matches_language("hpp", "cpp"));
754 assert!(matches_language("hh", "cpp"));
755 assert!(matches_language("hxx", "cpp"));
756 assert!(matches_language("h", "cpp")); assert!(matches_language("cpp", "c++")); assert!(!matches_language("c", "cpp"));
759 }
760
761 #[test]
762 fn test_matches_language_csharp() {
763 assert!(matches_language("cs", "csharp"));
764 assert!(matches_language("cs", "c#"));
765 assert!(matches_language("csx", "csharp"));
766 assert!(matches_language("cs", "CSharp"));
767 assert!(!matches_language("cpp", "csharp"));
768 }
769
770 #[test]
771 fn test_matches_language_dart() {
772 assert!(matches_language("dart", "dart"));
773 assert!(matches_language("dart", "Dart"));
774 assert!(!matches_language("d", "dart"));
775 }
776
777 #[test]
778 fn test_matches_language_sql() {
779 assert!(matches_language("sql", "sql"));
780 assert!(matches_language("sql", "SQL"));
781 assert!(!matches_language("rs", "sql"));
782 }
783
784 #[test]
785 fn test_matches_language_servicenow() {
786 assert!(matches_language("snjs", "servicenow"));
787 assert!(matches_language("snjs", "ServiceNow-Xanadu"));
788 assert!(matches_language("snjs", "servicenow-xanadu-js"));
789 assert!(!matches_language("js", "servicenow"));
790 }
791}