1use std::{ops::Range, sync::Arc};
2
3use anyhow::{Context, Result, ensure};
4
5use super::{
6 domain::{Domain, Lease},
7 literal::{self, Direction},
8 search::{Hit, Position},
9};
10use crate::{
11 engine::{ReviewId, Side, SourceLocation},
12 model::Kind,
13 text,
14 view::{LineId, SourceOrder},
15};
16
17pub(crate) enum SearchStart {
18 At(Position),
19 AfterRow { display: SourceOrder, row: usize },
20}
21
22pub struct SearchRead {
25 domain: Domain,
26 query: Arc<str>,
27 start: SearchStart,
28 preferred: Side,
29}
30
31#[derive(Clone, Debug)]
33pub struct SearchMatch {
34 pub(crate) hit: Hit,
35 pub(crate) display: Range<usize>,
36 location: SourceLocation,
37 lease: Lease,
38 query: Arc<str>,
39}
40
41impl SearchMatch {
42 pub fn location(&self) -> SourceLocation {
43 self.location
44 }
45 pub fn source_bytes(&self) -> Range<u64> {
46 self.hit.bytes.clone()
47 }
48 pub fn wrapped(&self) -> bool {
49 self.hit.wrapped
50 }
51
52 pub(crate) fn check(&self, review: ReviewId) -> Result<()> {
53 ensure!(
54 self.hit.review == review,
55 "Search match belongs to another review"
56 );
57 self.lease.check()
58 }
59}
60
61impl SearchRead {
62 pub(crate) fn new(
63 domain: Domain,
64 query: String,
65 start: SearchStart,
66 preferred: Side,
67 ) -> Result<Self> {
68 literal::validate(&query)?;
69 domain.check()?;
70 Ok(Self {
71 domain,
72 query: query.into(),
73 start,
74 preferred,
75 })
76 }
77
78 pub fn query(&self) -> &str {
79 &self.query
80 }
81
82 pub fn check(&self) -> Result<()> {
84 self.domain.check()
85 }
86
87 fn origin(&self, check: &impl Fn() -> Result<()>) -> Result<Position> {
88 match &self.start {
89 SearchStart::At(position) => Ok(*position),
90 SearchStart::AfterRow { display, row } => {
91 let file = display.file.id;
92 for row in row.saturating_add(1)..display.len() {
93 check()?;
94 if let Some(source) = display.source(row) {
95 let row = self.domain.files[file]
96 .order
97 .row_for_source(source)
98 .context("Search origin is outside the frozen source order")?;
99 return Ok(Position { file, row, byte: 0 });
100 }
101 }
102 Ok(Position {
103 file: file + 1,
104 ..Position::default()
105 })
106 }
107 }
108 }
109
110 pub fn find(
113 &self,
114 previous: Option<&SearchMatch>,
115 direction: Direction,
116 cancel: &impl Fn() -> Result<()>,
117 ) -> Result<Option<SearchMatch>> {
118 let check = || {
119 self.domain.check()?;
120 cancel()
121 };
122 check()?;
123 let origin = if let Some(previous) = previous {
124 previous.check(self.domain.review)?;
125 ensure!(
126 Arc::ptr_eq(&self.query, &previous.query),
127 "Match belongs to another search"
128 );
129 Position {
130 byte: previous
131 .hit
132 .bytes
133 .start
134 .checked_add(u64::from(direction == Direction::Forward))
135 .context("Search byte offset overflow")?,
136 ..previous.hit.position
137 }
138 } else {
139 self.origin(&check)?
140 };
141 let Some(hit) = self.domain.find(&self.query, origin, direction, cancel)? else {
142 return Ok(None);
143 };
144 let file = &self.domain.files[hit.position.file];
145 let (display, old, new, kind) = match hit.source {
146 LineId::Patch(id) => {
147 let info = file.order.file.info(id);
148 let mut reader = file.order.file.uncached_lines();
149 (
150 text::source_offsets(reader.line(id, 0)?, hit.bytes.clone(), &check)?,
151 info.old,
152 info.new,
153 info.kind,
154 )
155 }
156 LineId::Context { old, new, .. } => {
157 let mut reader = file
158 .context
159 .as_ref()
160 .context("Search context is no longer available")?
161 .uncached_lines();
162 (
163 text::source_offsets(reader.line(new.get(), 0)?, hit.bytes.clone(), &check)?,
164 Some(old.get()),
165 Some(new.get()),
166 Kind::Context,
167 )
168 }
169 };
170 check()?;
171 let side = match kind {
172 Kind::Deletion => Side::Old,
173 Kind::Addition => Side::New,
174 Kind::Context => self.preferred,
175 };
176 Ok(Some(SearchMatch {
177 location: SourceLocation {
178 review: self.domain.review,
179 file: hit.position.file,
180 side,
181 old_line: old,
182 new_line: new,
183 },
184 hit,
185 display,
186 lease: self.domain.lease.clone(),
187 query: self.query.clone(),
188 }))
189 }
190}