1use crate::license_detection::LicenseDetectionError;
9use crate::license_detection::index::LicenseIndex;
10use crate::license_detection::index::dictionary::{KnownToken, QueryToken, TokenId, TokenKind};
11use crate::license_detection::models::PositionSpan;
12use crate::license_detection::position_set::PositionSet;
13use crate::license_detection::spdx_lid::split_spdx_lid;
14use crate::license_detection::tokenize::STOPWORDS;
15use crate::license_detection::tokenize::tokenize_as_ids;
16use regex::Regex;
17use std::cell::{OnceCell, RefCell};
18use std::collections::HashMap;
19use std::sync::LazyLock;
20use std::time::Instant;
21
22static QUERY_PATTERN: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new(r"[^_\W]+\+?[^_\W]*").expect("valid query regex"));
24static MATCHED_TEXT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
25 Regex::new(r"(?P<token>[^_\W]+\+?[^_\W]*)|(?P<punct>[_\W\s\+]+[_\W\s]?)")
26 .expect("valid matched text regex")
27});
28
29#[derive(Clone)]
30struct MatchedTextToken {
31 value: String,
32 line_num: usize,
33 pos: Option<usize>,
34 is_text: bool,
35 is_matched: bool,
36}
37
38#[derive(Debug)]
50pub struct Query<'a> {
51 pub text: String,
55
56 pub tokens: Vec<TokenId>,
60
61 pub line_by_pos: Vec<usize>,
68
69 pub unknowns_by_pos: HashMap<Option<usize>, usize>,
77
78 pub stopwords_by_pos: HashMap<Option<usize>, usize>,
84
85 pub shorts_and_digits_pos: PositionSet,
91
92 pub high_matchables: PositionSet,
98
99 pub low_matchables: PositionSet,
105
106 pub is_binary: bool,
110
111 pub(crate) query_run_ranges: Vec<(usize, Option<usize>)>,
117
118 pub spdx_lines: Vec<(String, usize, usize)>,
125
126 pub(crate) line_content_ranges: std::sync::OnceLock<Vec<std::ops::Range<usize>>>,
134
135 pub index: &'a LicenseIndex,
137}
138
139pub fn matched_text_from_text(text: &str, start_line: usize, end_line: usize) -> String {
140 if start_line == 0 || end_line == 0 || start_line > end_line {
141 return String::new();
142 }
143
144 text.lines()
145 .enumerate()
146 .filter_map(|(idx, line)| {
147 let line_num = idx + 1;
148 if line_num >= start_line && line_num <= end_line {
149 Some(line)
150 } else {
151 None
152 }
153 })
154 .collect::<Vec<_>>()
155 .join("\n")
156}
157
158pub fn matched_text_diagnostics_from_text(
159 text: &str,
160 query: &Query<'_>,
161 matched_positions: &PositionSet,
162 start_pos: usize,
163 end_pos: usize,
164 start_line: usize,
165 end_line: usize,
166) -> String {
167 let tokens = tokenize_matched_text(text, query);
168 let reportable_tokens = collect_reportable_tokens(
169 tokens,
170 matched_positions,
171 start_pos,
172 end_pos,
173 start_line,
174 end_line,
175 );
176 let line_endings = collect_line_endings(text);
177
178 render_diagnostic_tokens(&reportable_tokens, &line_endings)
179}
180
181pub fn matched_text_from_tokens(
191 text: &str,
192 query: &Query<'_>,
193 matched_positions: &PositionSet,
194 start_pos: usize,
195 end_pos: usize,
196 start_line: usize,
197 end_line: usize,
198) -> String {
199 let tokens = tokenize_matched_text(text, query);
200 let reportable_tokens = collect_reportable_tokens(
201 tokens,
202 matched_positions,
203 start_pos,
204 end_pos,
205 start_line,
206 end_line,
207 );
208 let line_endings = collect_line_endings(text);
209
210 render_plain_tokens(&reportable_tokens, &line_endings)
211}
212
213fn render_plain_tokens(tokens: &[MatchedTextToken], line_endings: &[String]) -> String {
214 let mut rendered = String::new();
215 let mut previous_line: Option<usize> = None;
216
217 for token in tokens {
218 if let Some(prev_line) = previous_line
219 && token.line_num > prev_line
220 {
221 for line in prev_line..token.line_num {
222 if let Some(line_ending) = line_endings.get(line.saturating_sub(1)) {
223 rendered.push_str(line_ending.as_str());
224 }
225 }
226 }
227
228 let token_value = if token.is_text {
229 token.value.as_str()
230 } else {
231 token
232 .value
233 .strip_suffix("\r\n")
234 .or_else(|| token.value.strip_suffix('\n'))
235 .unwrap_or(token.value.as_str())
236 };
237
238 rendered.push_str(token_value);
239
240 previous_line = Some(token.line_num);
241 }
242
243 rendered
244}
245
246fn tokenize_matched_text(text: &str, query: &Query<'_>) -> Vec<MatchedTextToken> {
247 let mut tokens = Vec::new();
248 let mut pos = 0usize;
249 for (line_num, line) in (1usize..).zip(text.split_inclusive('\n')) {
250 for capture in MATCHED_TEXT_PATTERN.captures_iter(line) {
251 if let Some(token_match) = capture.name("token") {
252 let token_text = token_match.as_str();
253 let retokenized: Vec<String> = QUERY_PATTERN
254 .find_iter(&token_text.to_lowercase())
255 .map(|m| m.as_str().to_string())
256 .filter(|token| !STOPWORDS.contains(token.as_str()))
257 .collect();
258
259 if retokenized.is_empty() {
260 tokens.push(MatchedTextToken {
261 value: token_text.to_string(),
262 line_num,
263 pos: None,
264 is_text: true,
265 is_matched: false,
266 });
267 } else if retokenized.len() == 1 {
268 let token = &retokenized[0];
269 let token_pos = if query.index.dictionary.get(token).is_some() {
270 let current_pos = pos;
271 pos += 1;
272 Some(current_pos)
273 } else {
274 None
275 };
276
277 tokens.push(MatchedTextToken {
278 value: token_text.to_string(),
279 line_num,
280 pos: token_pos,
281 is_text: true,
282 is_matched: false,
283 });
284 } else {
285 for token in retokenized {
286 let token_pos = if query.index.dictionary.get(&token).is_some() {
287 let current_pos = pos;
288 pos += 1;
289 Some(current_pos)
290 } else {
291 None
292 };
293
294 tokens.push(MatchedTextToken {
295 value: token,
296 line_num,
297 pos: token_pos,
298 is_text: true,
299 is_matched: false,
300 });
301 }
302 }
303 } else if let Some(punct_match) = capture.name("punct") {
304 tokens.push(MatchedTextToken {
305 value: punct_match.as_str().to_string(),
306 line_num,
307 pos: None,
308 is_text: false,
309 is_matched: false,
310 });
311 }
312 }
313 }
314
315 tokens
316}
317
318fn collect_reportable_tokens(
319 tokens: Vec<MatchedTextToken>,
320 matched_positions: &PositionSet,
321 start_pos: usize,
322 end_pos: usize,
323 start_line: usize,
324 end_line: usize,
325) -> Vec<MatchedTextToken> {
326 let mut reportable = Vec::new();
327 let mut started = false;
328 let mut finished = false;
329 let mut end_real_pos = None;
330 let mut last_real_pos = None;
331
332 for (real_pos, mut token) in tokens.into_iter().enumerate() {
333 if token.line_num < start_line {
334 continue;
335 }
336
337 if token.line_num > end_line {
338 break;
339 }
340
341 let mut is_included = false;
342
343 if token.pos.is_some_and(|pos| matched_positions.contains(pos)) {
344 token.is_matched = true;
345 is_included = true;
346 }
347
348 if !started && token.pos == Some(start_pos) {
349 started = true;
350 is_included = true;
351 }
352
353 if started && !finished {
354 is_included = true;
355 }
356
357 if token.pos == Some(end_pos) {
358 finished = true;
359 started = false;
360 end_real_pos = Some(real_pos);
361 }
362
363 if finished && !started && end_real_pos.is_some() && last_real_pos == end_real_pos {
364 end_real_pos = None;
365 if !token.is_text && !token.value.trim().is_empty() {
366 is_included = true;
367 }
368 }
369
370 last_real_pos = Some(real_pos);
371
372 if is_included {
373 reportable.push(token);
374 }
375 }
376
377 reportable
378}
379
380fn collect_line_endings(text: &str) -> Vec<String> {
381 text.split_inclusive('\n')
382 .map(|line| {
383 if line.ends_with("\r\n") {
384 "\r\n".to_string()
385 } else if line.ends_with('\n') {
386 "\n".to_string()
387 } else {
388 String::new()
389 }
390 })
391 .collect()
392}
393
394fn render_diagnostic_tokens(tokens: &[MatchedTextToken], line_endings: &[String]) -> String {
395 let mut rendered = String::new();
396 let mut previous_line: Option<usize> = None;
397
398 for token in tokens {
399 if let Some(prev_line) = previous_line
400 && token.line_num > prev_line
401 {
402 for line in prev_line..token.line_num {
403 if let Some(line_ending) = line_endings.get(line.saturating_sub(1)) {
404 rendered.push_str(line_ending.as_str());
405 }
406 }
407 }
408
409 let token_value = if token.is_text {
410 token.value.as_str()
411 } else {
412 token
413 .value
414 .strip_suffix("\r\n")
415 .or_else(|| token.value.strip_suffix('\n'))
416 .unwrap_or(token.value.as_str())
417 };
418
419 if token.is_text && !STOPWORDS.contains(token.value.to_lowercase().as_str()) {
420 if token.is_matched {
421 rendered.push_str(token_value);
422 } else {
423 rendered.push('[');
424 rendered.push_str(token_value);
425 rendered.push(']');
426 }
427 } else {
428 rendered.push_str(token_value);
429 }
430
431 previous_line = Some(token.line_num);
432 }
433
434 rendered
435}
436
437impl<'a> Query<'a> {
438 const TEXT_LINE_THRESHOLD: usize = 15;
453 const BINARY_LINE_THRESHOLD: usize = 50;
454 const MAX_TOKEN_PER_LINE: usize = 25;
455
456 fn compute_spdx_offset(
457 tokens: &[QueryToken],
458 dictionary: &crate::license_detection::index::dictionary::TokenDictionary,
459 ) -> Option<usize> {
460 let get_known_id = |i: usize| -> Option<TokenId> {
461 match tokens.get(i)? {
462 QueryToken::Known(known) => Some(known.id),
463 _ => None,
464 }
465 };
466
467 let spdx_id = dictionary.get("spdx")?;
468 let license_id = dictionary.get("license")?;
469 let identifier_id = dictionary.get("identifier")?;
470 let licence_id = dictionary.get("licence");
471
472 let licenses_id = dictionary.get("licenses");
473 let nuget_id = dictionary.get("nuget");
474 let org_id = dictionary.get("org");
475
476 let is_spdx_prefix = |ids: [Option<TokenId>; 3]| -> bool {
477 ids.iter().all(|id| id.is_some())
478 && ids[0] == Some(spdx_id)
479 && (ids[1] == Some(license_id) || ids[1] == licence_id)
480 && ids[2] == Some(identifier_id)
481 };
482
483 let is_nuget_prefix = |ids: [Option<TokenId>; 3]| -> bool {
484 licenses_id.is_some()
485 && nuget_id.is_some()
486 && org_id.is_some()
487 && ids[0] == licenses_id
488 && ids[1] == Some(nuget_id.unwrap())
489 && ids[2] == Some(org_id.unwrap())
490 };
491
492 if tokens.len() >= 3 {
493 let first_three = [get_known_id(0), get_known_id(1), get_known_id(2)];
494 if is_spdx_prefix(first_three) || is_nuget_prefix(first_three) {
495 return Some(0);
496 }
497 }
498
499 if tokens.len() >= 4 {
500 let second_three = [get_known_id(1), get_known_id(2), get_known_id(3)];
501 if is_spdx_prefix(second_three) || is_nuget_prefix(second_three) {
502 return Some(1);
503 }
504 }
505
506 if tokens.len() >= 5 {
507 let third_three = [get_known_id(2), get_known_id(3), get_known_id(4)];
508 if is_spdx_prefix(third_three) || is_nuget_prefix(third_three) {
509 return Some(2);
510 }
511 }
512
513 None
514 }
515
516 pub(crate) fn from_extracted_text(
517 text: &str,
518 index: &'a LicenseIndex,
519 binary_derived: bool,
520 ) -> Result<Self, LicenseDetectionError> {
521 Self::from_extracted_text_with_deadline(text, index, binary_derived, None)
522 }
523
524 pub(crate) fn from_extracted_text_with_deadline(
525 text: &str,
526 index: &'a LicenseIndex,
527 binary_derived: bool,
528 deadline: Option<Instant>,
529 ) -> Result<Self, LicenseDetectionError> {
530 let line_threshold = if binary_derived {
531 Self::BINARY_LINE_THRESHOLD
532 } else {
533 Self::TEXT_LINE_THRESHOLD
534 };
535
536 Self::with_source_options(text, index, line_threshold, Some(binary_derived), deadline)
537 }
538
539 pub fn query_runs(&self) -> Vec<QueryRun<'_>> {
543 self.query_run_ranges
544 .iter()
545 .map(|&(start, end)| QueryRun::new(self, start, end))
546 .collect()
547 }
548
549 fn with_source_options(
550 text: &str,
551 index: &'a LicenseIndex,
552 line_threshold: usize,
553 binary_derived: Option<bool>,
554 deadline: Option<Instant>,
555 ) -> Result<Self, LicenseDetectionError> {
556 crate::license_detection::ensure_within_deadline(deadline)?;
557 let is_binary = match binary_derived {
558 Some(is_binary) => is_binary,
559 None => Self::detect_binary(text),
560 };
561 let has_long_lines = Self::detect_long_lines(text);
562
563 let mut tokens = Vec::new();
564 let mut line_by_pos = Vec::new();
565 let mut unknowns_by_pos: HashMap<Option<usize>, usize> = HashMap::new();
566 let mut stopwords_by_pos: HashMap<Option<usize>, usize> = HashMap::new();
567 let mut shorts_and_digits_pos = PositionSet::new();
568 let mut spdx_lines: Vec<(String, usize, usize)> = Vec::new();
569
570 let mut known_pos: Option<usize> = None;
571 let mut started = false;
572 let mut tokens_by_line: Vec<Vec<Option<KnownToken>>> = Vec::new();
573
574 for (current_line, (line_index, line)) in (1usize..).zip(text.lines().enumerate()) {
575 if line_index.is_multiple_of(128) {
576 crate::license_detection::ensure_within_deadline(deadline)?;
577 }
578
579 let line_trimmed = line.trim();
580 let mut line_tokens: Vec<Option<KnownToken>> = Vec::new();
581
582 let mut line_first_known_pos = None;
583
584 let line_query_tokens = tokenize_as_ids(line_trimmed, &index.dictionary);
585
586 for query_token in &line_query_tokens {
587 match query_token {
588 QueryToken::Known(known_token) => {
589 known_pos = Some(known_pos.map_or(0, |p| p + 1));
590 started = true;
591 tokens.push(known_token.id);
592 line_by_pos.push(current_line);
593 line_tokens.push(Some(*known_token));
594
595 if line_first_known_pos.is_none() {
596 line_first_known_pos = known_pos;
597 }
598
599 if known_token.is_short_or_digit {
600 let _ = shorts_and_digits_pos.insert(known_pos.unwrap());
601 }
602 }
603 QueryToken::Unknown if !started => {
604 *unknowns_by_pos.entry(None).or_insert(0) += 1;
605 line_tokens.push(None);
606 }
607 QueryToken::Unknown => {
608 *unknowns_by_pos.entry(known_pos).or_insert(0) += 1;
609 line_tokens.push(None);
610 }
611 QueryToken::Stopword if !started => {
612 *stopwords_by_pos.entry(None).or_insert(0) += 1;
613 }
614 QueryToken::Stopword => {
615 *stopwords_by_pos.entry(known_pos).or_insert(0) += 1;
616 }
617 }
618 }
619
620 let line_last_known_pos = known_pos;
621
622 let spdx_start_offset =
623 Self::compute_spdx_offset(&line_query_tokens, &index.dictionary);
624
625 if let Some(offset) = spdx_start_offset
626 && let Some(line_first_known_pos) = line_first_known_pos
627 {
628 let (spdx_prefix, spdx_expression) = split_spdx_lid(line);
629 let spdx_text = format!("{}{}", spdx_prefix.unwrap_or_default(), spdx_expression);
630 let spdx_start_known_pos = line_first_known_pos + offset;
631
632 if spdx_start_known_pos <= line_last_known_pos.unwrap() {
633 let spdx_end = line_last_known_pos.unwrap() + 1;
634 spdx_lines.push((spdx_text, spdx_start_known_pos, spdx_end));
635 }
636 }
637 tokens_by_line.push(line_tokens);
638 }
639
640 crate::license_detection::ensure_within_deadline(deadline)?;
641
642 let high_matchables: PositionSet = tokens
643 .iter()
644 .enumerate()
645 .filter(|(_pos, tid)| index.dictionary.token_kind(**tid) == TokenKind::Legalese)
646 .map(|(pos, _tid)| pos)
647 .collect();
648
649 let low_matchables: PositionSet = tokens
650 .iter()
651 .enumerate()
652 .filter(|(_pos, tid)| index.dictionary.token_kind(**tid) == TokenKind::Regular)
653 .map(|(pos, _tid)| pos)
654 .collect();
655
656 let query_runs = Self::compute_query_runs(&tokens_by_line, line_threshold, has_long_lines);
657
658 Ok(Query {
659 text: text.to_string(),
660 tokens,
661 line_by_pos,
662 unknowns_by_pos,
663 stopwords_by_pos,
664 shorts_and_digits_pos,
665 high_matchables,
666 low_matchables,
667 is_binary,
668 query_run_ranges: query_runs,
669 spdx_lines,
670 line_content_ranges: std::sync::OnceLock::new(),
671 index,
672 })
673 }
674
675 fn detect_binary(text: &str) -> bool {
689 let null_byte_count = text.bytes().filter(|&b| b == 0).count();
690
691 if null_byte_count > 0 {
692 return true;
693 }
694
695 let non_printable_ratio = text
696 .chars()
697 .filter(|&c| {
698 !c.is_ascii() && !c.is_ascii_graphic() && c != '\n' && c != '\r' && c != '\t'
699 })
700 .count() as f64
701 / text.len().max(1) as f64;
702
703 non_printable_ratio > 0.3
704 }
705
706 fn detect_long_lines(text: &str) -> bool {
716 text.lines()
717 .any(|line| crate::license_detection::tokenize::count_tokens(line) > 25)
718 }
719
720 fn break_long_lines(lines: &[Vec<Option<KnownToken>>]) -> Vec<Vec<Option<KnownToken>>> {
721 lines
722 .iter()
723 .flat_map(|line| {
724 if line.is_empty() {
725 return Vec::new();
726 }
727
728 if line.len() <= Self::MAX_TOKEN_PER_LINE {
729 vec![line.clone()]
730 } else {
731 line.chunks(Self::MAX_TOKEN_PER_LINE)
732 .map(|chunk| chunk.to_vec())
733 .collect()
734 }
735 })
736 .collect()
737 }
738
739 fn compute_query_runs(
740 tokens_by_line: &[Vec<Option<KnownToken>>],
741 line_threshold: usize,
742 has_long_lines: bool,
743 ) -> Vec<(usize, Option<usize>)> {
744 let processed_lines = if has_long_lines {
745 Self::break_long_lines(tokens_by_line)
746 } else {
747 tokens_by_line.to_vec()
748 };
749
750 let mut query_runs = Vec::new();
751 let mut query_run_start = 0usize;
752 let mut query_run_end = None;
753 let mut empty_lines = 0usize;
754 let mut pos = 0usize;
755 let mut query_run_is_all_digit = true;
756
757 for line_tokens in processed_lines {
758 if query_run_end.is_some() && empty_lines >= line_threshold {
759 if !query_run_is_all_digit {
760 query_runs.push((query_run_start, query_run_end));
761 }
762 query_run_start = pos;
763 query_run_end = None;
764 empty_lines = 0;
765 query_run_is_all_digit = true;
766 }
767
768 if query_run_end.is_none() {
769 query_run_start = pos;
770 }
771
772 if line_tokens.is_empty() {
773 empty_lines += 1;
774 continue;
775 }
776
777 let line_is_all_digit = line_tokens
778 .iter()
779 .all(|token_id| token_id.map(|known| known.is_digit_only).unwrap_or(true));
780 let mut line_has_known_tokens = false;
781 let mut line_has_good_tokens = false;
782
783 for known in line_tokens.into_iter().flatten() {
784 line_has_known_tokens = true;
785 if known.kind == TokenKind::Legalese {
786 line_has_good_tokens = true;
787 }
788 if !known.is_digit_only {
789 query_run_is_all_digit = false;
790 }
791 query_run_end = Some(pos);
792 pos += 1;
793 }
794
795 if line_is_all_digit || !line_has_known_tokens {
796 empty_lines += 1;
797 continue;
798 }
799
800 if line_has_good_tokens {
801 empty_lines = 0;
802 } else {
803 empty_lines += 1;
804 }
805 }
806
807 if let Some(end) = query_run_end
808 && !query_run_is_all_digit
809 {
810 query_runs.push((query_run_start, Some(end)));
811 }
812
813 query_runs
814 }
815
816 #[inline]
826 pub fn line_for_pos(&self, pos: usize) -> Option<usize> {
827 self.line_by_pos.get(pos).copied()
828 }
829
830 #[inline]
832 pub fn is_empty(&self) -> bool {
833 self.tokens.is_empty()
834 }
835
836 pub fn whole_query_run(&self) -> QueryRun<'a> {
840 QueryRun::whole_query_snapshot(self)
841 }
842
843 pub fn subtract(&mut self, span: &PositionSpan) {
852 self.high_matchables.remove_span(span);
853 self.low_matchables.remove_span(span);
854 }
855
856 pub fn matched_text(&self, start_line: usize, end_line: usize) -> String {
870 if start_line == 0 || end_line == 0 || start_line > end_line {
871 return String::new();
872 }
873 let ranges = self.line_content_ranges();
874 let from = start_line - 1;
875 if from >= ranges.len() {
876 return String::new();
877 }
878 let to = end_line.min(ranges.len());
881 ranges[from..to]
882 .iter()
883 .map(|r| &self.text[r.clone()])
884 .collect::<Vec<_>>()
885 .join("\n")
886 }
887
888 pub(crate) fn line_range_has_oversized_line(
892 &self,
893 start_line: usize,
894 end_line: usize,
895 max_line_length: usize,
896 ) -> bool {
897 if start_line == 0 || end_line == 0 || start_line > end_line {
898 return false;
899 }
900 let ranges = self.line_content_ranges();
901 let from = start_line - 1;
902 if from >= ranges.len() {
903 return false;
904 }
905 let to = end_line.min(ranges.len());
906 ranges[from..to].iter().any(|r| r.len() > max_line_length)
907 }
908
909 fn line_content_ranges(&self) -> &[std::ops::Range<usize>] {
915 self.line_content_ranges.get_or_init(|| {
916 let base = self.text.as_ptr() as usize;
917 self.text
918 .lines()
919 .map(|line| {
920 let start = line.as_ptr() as usize - base;
921 start..start + line.len()
922 })
923 .collect()
924 })
925 }
926}
927
928#[derive(Debug, Clone)]
929struct WholeQueryRunSnapshot<'a> {
930 index: &'a LicenseIndex,
931 tokens: Vec<TokenId>,
932 line_by_pos: Vec<usize>,
933 high_matchables: PositionSet,
934 low_matchables: PositionSet,
935}
936
937#[derive(Debug, Clone)]
945pub struct QueryRun<'a> {
946 query: Option<&'a Query<'a>>,
947 whole_query_snapshot: Option<WholeQueryRunSnapshot<'a>>,
948 pub start: usize,
949 pub end: Option<usize>,
950 cached_high_matchables: OnceCell<PositionSet>,
951 cached_low_matchables: OnceCell<PositionSet>,
952 combined_matchables: RefCell<Option<PositionSet>>,
953}
954
955impl<'a> QueryRun<'a> {
956 pub fn new(query: &'a Query<'a>, start: usize, end: Option<usize>) -> Self {
965 Self {
966 query: Some(query),
967 whole_query_snapshot: None,
968 start,
969 end,
970 cached_high_matchables: OnceCell::new(),
971 cached_low_matchables: OnceCell::new(),
972 combined_matchables: RefCell::new(None),
973 }
974 }
975
976 fn whole_query_snapshot(query: &Query<'a>) -> Self {
977 let end = if query.is_empty() {
978 None
979 } else {
980 Some(query.tokens.len() - 1)
981 };
982
983 Self {
984 query: None,
985 whole_query_snapshot: Some(WholeQueryRunSnapshot {
986 index: query.index,
987 tokens: query.tokens.clone(),
988 line_by_pos: query.line_by_pos.clone(),
989 high_matchables: query.high_matchables.clone(),
990 low_matchables: query.low_matchables.clone(),
991 }),
992 start: 0,
993 end,
994 cached_high_matchables: OnceCell::new(),
995 cached_low_matchables: OnceCell::new(),
996 combined_matchables: RefCell::new(None),
997 }
998 }
999
1000 fn source_tokens(&self) -> &[TokenId] {
1001 if let Some(query) = self.query {
1002 &query.tokens
1003 } else {
1004 &self
1005 .whole_query_snapshot
1006 .as_ref()
1007 .expect("snapshot-backed whole query run should have snapshot data")
1008 .tokens
1009 }
1010 }
1011
1012 fn source_line_by_pos(&self) -> &[usize] {
1013 if let Some(query) = self.query {
1014 &query.line_by_pos
1015 } else {
1016 &self
1017 .whole_query_snapshot
1018 .as_ref()
1019 .expect("snapshot-backed whole query run should have snapshot data")
1020 .line_by_pos
1021 }
1022 }
1023
1024 fn source_high_matchables(&self) -> &PositionSet {
1025 if let Some(query) = self.query {
1026 &query.high_matchables
1027 } else {
1028 &self
1029 .whole_query_snapshot
1030 .as_ref()
1031 .expect("snapshot-backed whole query run should have snapshot data")
1032 .high_matchables
1033 }
1034 }
1035
1036 fn source_low_matchables(&self) -> &PositionSet {
1037 if let Some(query) = self.query {
1038 &query.low_matchables
1039 } else {
1040 &self
1041 .whole_query_snapshot
1042 .as_ref()
1043 .expect("snapshot-backed whole query run should have snapshot data")
1044 .low_matchables
1045 }
1046 }
1047
1048 pub fn get_index(&self) -> &LicenseIndex {
1050 if let Some(query) = self.query {
1051 query.index
1052 } else {
1053 self.whole_query_snapshot
1054 .as_ref()
1055 .expect("snapshot-backed whole query run should have snapshot data")
1056 .index
1057 }
1058 }
1059
1060 pub fn line_for_pos(&self, pos: usize) -> Option<usize> {
1068 self.source_line_by_pos().get(pos).copied()
1069 }
1070
1071 pub fn tokens(&self) -> &[TokenId] {
1077 match self.end {
1078 Some(end) => &self.source_tokens()[self.start..=end],
1079 None => &[],
1080 }
1081 }
1082
1083 pub fn tokens_with_pos(&self) -> impl Iterator<Item = (usize, TokenId)> + '_ {
1087 self.tokens()
1088 .iter()
1089 .copied()
1090 .enumerate()
1091 .map(|(i, tid)| (self.start + i, tid))
1092 }
1093
1094 pub fn is_digits_only(&self) -> bool {
1098 self.tokens()
1099 .iter()
1100 .all(|&tid| self.get_index().dictionary.is_digit_only_token(tid))
1101 }
1102
1103 pub fn is_matchable(&self, include_low: bool, exclude_positions: &[PositionSpan]) -> bool {
1113 if self.is_digits_only() {
1114 return false;
1115 }
1116
1117 let matchables = self.matchables(include_low);
1118
1119 if exclude_positions.is_empty() {
1120 return !matchables.is_empty();
1121 }
1122
1123 let mut matchable_set = matchables;
1124 for span in exclude_positions {
1125 matchable_set.remove_span(span);
1126 }
1127
1128 !matchable_set.is_empty()
1129 }
1130
1131 pub fn matchables(&self, include_low: bool) -> PositionSet {
1132 if include_low {
1133 if let Some(ref cached) = *self.combined_matchables.borrow() {
1134 return cached.clone();
1135 }
1136 let combined = self.low_matchables().union(&self.high_matchables());
1137 *self.combined_matchables.borrow_mut() = Some(combined.clone());
1138 combined
1139 } else {
1140 self.high_matchables()
1141 }
1142 }
1143
1144 pub fn matchable_tokens(&self) -> Vec<Option<TokenId>> {
1145 let high_matchables = self.high_matchables();
1146 if high_matchables.is_empty() {
1147 return Vec::new();
1148 }
1149
1150 let matchables = self.matchables(true);
1151 self.tokens_with_pos()
1152 .map(|(pos, tid)| {
1153 if matchables.contains(pos) {
1154 Some(tid)
1155 } else {
1156 None
1157 }
1158 })
1159 .collect()
1160 }
1161
1162 pub fn high_matchables(&self) -> PositionSet {
1163 self.cached_high_matchables
1164 .get_or_init(|| {
1165 let start = self.start;
1166 let end = self.end.map(|e| e + 1).unwrap_or(usize::MAX);
1167 let source = self.source_high_matchables();
1168 let live_span = PositionSpan::new(start, end);
1169 source
1170 .iter()
1171 .filter(|&pos| live_span.contains(pos))
1172 .collect()
1173 })
1174 .clone()
1175 }
1176
1177 pub fn low_matchables(&self) -> PositionSet {
1178 self.cached_low_matchables
1179 .get_or_init(|| {
1180 let start = self.start;
1181 let end = self.end.map(|e| e + 1).unwrap_or(usize::MAX);
1182 let source = self.source_low_matchables();
1183 let live_span = PositionSpan::new(start, end);
1184 source
1185 .iter()
1186 .filter(|&pos| live_span.contains(pos))
1187 .collect()
1188 })
1189 .clone()
1190 }
1191}
1192
1193#[cfg(test)]
1194mod test;