Skip to main content

diffs_rs/reading/
search_read.rs

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
22/// One frozen diff-text query. Run searches off the terminal input thread.
23/// Drop the job after use to release its source owners.
24pub struct SearchRead {
25    domain: Domain,
26    query: Arc<str>,
27    start: SearchStart,
28    preferred: Side,
29}
30
31/// An opaque match in one open review. It retains validity tokens, not source files.
32#[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    /// Checks whether this captured scope still belongs to the open review.
83    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    /// Finds one occurrence, wrapping at most once. Previous must come from this query.
111    /// No match and cancellation leave the caller's selected match unchanged.
112    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}