dsh-ops 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +417 -0
  5. package/README.en.md +121 -0
  6. package/README.md +113 -2
  7. package/README.zh.md +114 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/schema-baseline.json +64 -0
  12. package/docs/schema-current.json +84 -0
  13. package/docs/schema-measurement.md +17 -0
  14. package/dsh-plugin.json +88 -0
  15. package/icon.svg +12 -0
  16. package/lib/binary.js +409 -0
  17. package/lib/config.js +198 -0
  18. package/lib/handshake.js +252 -0
  19. package/lib/index.js +108 -0
  20. package/lib/jobs.js +42 -0
  21. package/lib/policy.js +64 -0
  22. package/lib/presentation.js +63 -0
  23. package/lib/profile-install.js +61 -0
  24. package/lib/rust.js +194 -0
  25. package/lib/session-shells.js +78 -0
  26. package/lib/shells.js +993 -0
  27. package/lib/tools.js +657 -0
  28. package/locale/en.json +6 -0
  29. package/locale/zh.json +6 -0
  30. package/package.json +114 -4
  31. package/vendor/fastctx/Cargo.lock +3210 -0
  32. package/vendor/fastctx/Cargo.toml +94 -0
  33. package/vendor/fastctx/FORK.md +119 -0
  34. package/vendor/fastctx/LICENSE-APACHE +201 -0
  35. package/vendor/fastctx/NOTICE +40 -0
  36. package/vendor/fastctx/README.md +439 -0
  37. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  39. package/vendor/fastctx/UPSTREAM.md +49 -0
  40. package/vendor/fastctx/build.rs +413 -0
  41. package/vendor/fastctx/src/background_status.rs +403 -0
  42. package/vendor/fastctx/src/binary.rs +75 -0
  43. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  44. package/vendor/fastctx/src/budget.rs +781 -0
  45. package/vendor/fastctx/src/cli/mod.rs +110 -0
  46. package/vendor/fastctx/src/context_guard.rs +289 -0
  47. package/vendor/fastctx/src/control/mod.rs +6 -0
  48. package/vendor/fastctx/src/control/paths.rs +49 -0
  49. package/vendor/fastctx/src/control/settings.rs +753 -0
  50. package/vendor/fastctx/src/control/transaction.rs +531 -0
  51. package/vendor/fastctx/src/edit/document.rs +535 -0
  52. package/vendor/fastctx/src/edit/locks.rs +371 -0
  53. package/vendor/fastctx/src/edit/mod.rs +213 -0
  54. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  55. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  56. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  57. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  58. package/vendor/fastctx/src/edit_server.rs +53 -0
  59. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  60. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  61. package/vendor/fastctx/src/encoding.rs +1118 -0
  62. package/vendor/fastctx/src/file_executor.rs +1151 -0
  63. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  64. package/vendor/fastctx/src/glob_filter.rs +98 -0
  65. package/vendor/fastctx/src/glob_tool.rs +653 -0
  66. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  67. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  68. package/vendor/fastctx/src/lib.rs +45 -0
  69. package/vendor/fastctx/src/main.rs +15 -0
  70. package/vendor/fastctx/src/model.rs +51 -0
  71. package/vendor/fastctx/src/model_guidance.rs +62 -0
  72. package/vendor/fastctx/src/operation.rs +356 -0
  73. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  74. package/vendor/fastctx/src/os_environment.rs +414 -0
  75. package/vendor/fastctx/src/path_codec.rs +850 -0
  76. package/vendor/fastctx/src/paths.rs +244 -0
  77. package/vendor/fastctx/src/process_identity.rs +763 -0
  78. package/vendor/fastctx/src/process_policy.rs +74 -0
  79. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  80. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  81. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  82. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  83. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  84. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  85. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  86. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  87. package/vendor/fastctx/src/render_plan.rs +468 -0
  88. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  89. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  90. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  91. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  92. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  93. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  94. package/vendor/fastctx/src/runtime/session.rs +536 -0
  95. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  96. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  97. package/vendor/fastctx/src/search_text.rs +227 -0
  98. package/vendor/fastctx/src/server.rs +359 -0
  99. package/vendor/fastctx/src/server_manifest.rs +468 -0
  100. package/vendor/fastctx/src/server_support.rs +826 -0
  101. package/vendor/fastctx/src/session.rs +629 -0
  102. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  103. package/vendor/fastctx/src/shell/bash.rs +263 -0
  104. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  105. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  106. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  107. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  108. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  109. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  110. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  111. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  112. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  113. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  114. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  115. package/vendor/fastctx/src/shell/mod.rs +345 -0
  116. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  117. package/vendor/fastctx/src/shell/output.rs +406 -0
  118. package/vendor/fastctx/src/shell/process.rs +493 -0
  119. package/vendor/fastctx/src/shell_server.rs +156 -0
  120. package/vendor/fastctx/src/skip_report.rs +83 -0
  121. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  122. package/vendor/fastctx/src/tool_schema.rs +204 -0
  123. package/vendor/fastctx/src/traversal.rs +846 -0
  124. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
@@ -0,0 +1,1162 @@
1
+ //! Mode-specific ripgrep sinks and range-backed content capture.
2
+
3
+ use crate::operation::{WorkCheckpoint, WorkStop};
4
+ use crate::search_text::{RangeText, SearchText};
5
+ use grep_matcher::{Match, Matcher};
6
+ use grep_regex::RegexMatcher;
7
+ use grep_searcher::{Searcher, Sink, SinkContext, SinkContextKind, SinkError, SinkMatch};
8
+ use std::collections::{BTreeMap, VecDeque};
9
+ use std::fmt;
10
+ use std::io;
11
+ use std::ops::Range;
12
+ use std::sync::{Arc, OnceLock};
13
+
14
+ const OCCURRENCE_CHECK_INTERVAL: usize = 256;
15
+ const OCCURRENCE_BYTE_CHECK_INTERVAL: usize = 64 * 1024;
16
+
17
+ /// Content capture options shared by the line and occurrence plans.
18
+ #[derive(Clone, Copy, Debug)]
19
+ pub(crate) struct ContentSpec {
20
+ pub(crate) multiline: bool,
21
+ pub(crate) skip_entries: usize,
22
+ pub(crate) max_selected_entries: Option<usize>,
23
+ pub(crate) capture_match_text: bool,
24
+ pub(crate) before_context: usize,
25
+ pub(crate) after_context: usize,
26
+ pub(crate) capture_heap_limit_bytes: usize,
27
+ }
28
+
29
+ /// The minimum search work required by one grep output mode.
30
+ #[derive(Clone, Copy, Debug)]
31
+ pub(crate) enum GrepSearchPlan {
32
+ Exists,
33
+ Count,
34
+ ContentLine(ContentSpec),
35
+ ContentOccurrence(ContentSpec),
36
+ }
37
+
38
+ impl GrepSearchPlan {
39
+ pub(crate) fn content_multiline(self) -> Option<bool> {
40
+ match self {
41
+ Self::Exists | Self::Count => None,
42
+ Self::ContentLine(spec) | Self::ContentOccurrence(spec) => Some(spec.multiline),
43
+ }
44
+ }
45
+
46
+ pub(crate) fn before_context(self) -> usize {
47
+ match self {
48
+ Self::ContentLine(spec) | Self::ContentOccurrence(spec) => spec.before_context,
49
+ Self::Exists | Self::Count => 0,
50
+ }
51
+ }
52
+
53
+ pub(crate) fn after_context(self) -> usize {
54
+ match self {
55
+ Self::ContentLine(spec) | Self::ContentOccurrence(spec) => spec.after_context,
56
+ Self::Exists | Self::Count => 0,
57
+ }
58
+ }
59
+
60
+ pub(crate) fn with_content_window(
61
+ self,
62
+ skip_entries: usize,
63
+ max_selected_entries: Option<usize>,
64
+ ) -> Self {
65
+ match self {
66
+ Self::ContentLine(mut spec) => {
67
+ spec.skip_entries = skip_entries;
68
+ spec.max_selected_entries = max_selected_entries;
69
+ Self::ContentLine(spec)
70
+ }
71
+ Self::ContentOccurrence(mut spec) => {
72
+ spec.skip_entries = skip_entries;
73
+ spec.max_selected_entries = max_selected_entries;
74
+ Self::ContentOccurrence(spec)
75
+ }
76
+ Self::Exists | Self::Count => self,
77
+ }
78
+ }
79
+ }
80
+
81
+ #[derive(Clone, Copy, Debug)]
82
+ pub(crate) struct LineMatchSpan {
83
+ pub(crate) line_number: usize,
84
+ pub(crate) match_char_start: usize,
85
+ pub(crate) match_char_len: usize,
86
+ }
87
+
88
+ #[derive(Clone, Debug)]
89
+ struct TextRange {
90
+ backing: Arc<SearchText>,
91
+ range: Range<u64>,
92
+ cached: OnceLock<Arc<str>>,
93
+ }
94
+
95
+ impl TextRange {
96
+ fn new(backing: Arc<SearchText>, range: Range<u64>) -> Self {
97
+ Self {
98
+ backing,
99
+ range,
100
+ cached: OnceLock::new(),
101
+ }
102
+ }
103
+
104
+ fn as_str(&self) -> io::Result<&str> {
105
+ if let Some(text) = self.cached.get() {
106
+ return Ok(text);
107
+ }
108
+ match self.backing.range_str(self.range.clone())? {
109
+ RangeText::Borrowed(text) => Ok(text),
110
+ RangeText::Owned(text) => {
111
+ let _ = self.cached.set(text);
112
+ Ok(self
113
+ .cached
114
+ .get()
115
+ .expect("the captured temp range was initialized"))
116
+ }
117
+ }
118
+ }
119
+
120
+ fn len(&self) -> usize {
121
+ usize::try_from(self.range.end.saturating_sub(self.range.start)).unwrap_or(usize::MAX)
122
+ }
123
+ }
124
+
125
+ #[derive(Debug)]
126
+ pub(crate) struct CapturedLine {
127
+ text: TextRange,
128
+ chars: OnceLock<Arc<[char]>>,
129
+ }
130
+
131
+ impl CapturedLine {
132
+ fn new(backing: Arc<SearchText>, range: Range<u64>) -> Self {
133
+ Self {
134
+ text: TextRange::new(backing, range),
135
+ chars: OnceLock::new(),
136
+ }
137
+ }
138
+
139
+ pub(crate) fn as_str(&self) -> io::Result<&str> {
140
+ self.text.as_str()
141
+ }
142
+
143
+ pub(crate) fn byte_len(&self) -> usize {
144
+ self.text.len()
145
+ }
146
+
147
+ pub(crate) fn chars(&self) -> io::Result<&[char]> {
148
+ if self.chars.get().is_none() {
149
+ let chars = Arc::from(self.as_str()?.chars().collect::<Vec<_>>());
150
+ let _ = self.chars.set(chars);
151
+ }
152
+ Ok(self
153
+ .chars
154
+ .get()
155
+ .expect("the captured line character index was initialized"))
156
+ }
157
+
158
+ pub(crate) fn char_count(&self) -> io::Result<usize> {
159
+ Ok(self.chars()?.len())
160
+ }
161
+ }
162
+
163
+ #[derive(Clone, Debug)]
164
+ pub(crate) struct Occurrence {
165
+ matched_text: Option<TextRange>,
166
+ pub(crate) start_line: usize,
167
+ pub(crate) end_line: usize,
168
+ pub(crate) line_spans: Vec<LineMatchSpan>,
169
+ }
170
+
171
+ impl Occurrence {
172
+ pub(crate) fn matched_text(&self) -> io::Result<&str> {
173
+ self.matched_text
174
+ .as_ref()
175
+ .map(TextRange::as_str)
176
+ .unwrap_or(Ok(""))
177
+ }
178
+ }
179
+
180
+ #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
181
+ pub(crate) enum ContentEntry {
182
+ MatchingLine(usize),
183
+ Occurrence {
184
+ start_line: usize,
185
+ occurrence_index: usize,
186
+ },
187
+ }
188
+
189
+ #[derive(Debug)]
190
+ pub(crate) struct ContentPayload {
191
+ pub(crate) lines: BTreeMap<usize, CapturedLine>,
192
+ pub(crate) occurrences: BTreeMap<usize, Vec<Occurrence>>,
193
+ pub(crate) entries: Vec<ContentEntry>,
194
+ occurrence_total: usize,
195
+ pub(crate) total_lines: usize,
196
+ pub(crate) has_trailing_newline: bool,
197
+ }
198
+
199
+ #[derive(Debug)]
200
+ enum FilePayload {
201
+ Exists,
202
+ Count { occurrence_total: usize },
203
+ Content(ContentPayload),
204
+ }
205
+
206
+ #[derive(Debug)]
207
+ pub(crate) struct FileResult {
208
+ path: String,
209
+ payload: FilePayload,
210
+ }
211
+
212
+ impl FileResult {
213
+ fn exists(path: String) -> Self {
214
+ Self {
215
+ path,
216
+ payload: FilePayload::Exists,
217
+ }
218
+ }
219
+
220
+ pub(crate) fn count(path: String, occurrence_total: usize) -> Self {
221
+ Self {
222
+ path,
223
+ payload: FilePayload::Count { occurrence_total },
224
+ }
225
+ }
226
+
227
+ fn with_content(path: String, content: ContentPayload) -> Self {
228
+ Self {
229
+ path,
230
+ payload: FilePayload::Content(content),
231
+ }
232
+ }
233
+
234
+ pub(crate) fn path(&self) -> &str {
235
+ &self.path
236
+ }
237
+
238
+ pub(crate) fn occurrence_count(&self) -> usize {
239
+ match &self.payload {
240
+ FilePayload::Exists => 1,
241
+ FilePayload::Count { occurrence_total } => *occurrence_total,
242
+ FilePayload::Content(content) => content.occurrence_total,
243
+ }
244
+ }
245
+
246
+ pub(crate) fn content(&self) -> &ContentPayload {
247
+ match &self.payload {
248
+ FilePayload::Content(content) => content,
249
+ FilePayload::Exists | FilePayload::Count { .. } => {
250
+ unreachable!("only content results carry captured text")
251
+ }
252
+ }
253
+ }
254
+
255
+ pub(crate) fn content_mut(&mut self) -> &mut ContentPayload {
256
+ match &mut self.payload {
257
+ FilePayload::Content(content) => content,
258
+ FilePayload::Exists | FilePayload::Count { .. } => {
259
+ unreachable!("only content results carry captured text")
260
+ }
261
+ }
262
+ }
263
+
264
+ pub(crate) fn entry_count(&self) -> usize {
265
+ self.content().entries.len()
266
+ }
267
+
268
+ pub(crate) fn trim_entries(&mut self, start: usize, end: usize) {
269
+ let entries = &mut self.content_mut().entries;
270
+ entries.drain(..start);
271
+ entries.truncate(end - start);
272
+ }
273
+ }
274
+
275
+ pub(crate) struct SinkOutput {
276
+ pub(crate) result: Option<FileResult>,
277
+ pub(crate) entries_seen: usize,
278
+ }
279
+
280
+ #[derive(Debug)]
281
+ pub(crate) enum GrepSinkError {
282
+ Io(io::Error),
283
+ Search(String),
284
+ Stopped(WorkStop),
285
+ CaptureOverflow,
286
+ CountOverflow,
287
+ }
288
+
289
+ impl fmt::Display for GrepSinkError {
290
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
291
+ match self {
292
+ Self::Io(error) => error.fmt(formatter),
293
+ Self::Search(message) => formatter.write_str(message),
294
+ Self::Stopped(WorkStop::RequestCancelled) => formatter.write_str("request cancelled"),
295
+ Self::Stopped(WorkStop::EpochRetired) => formatter.write_str("search epoch retired"),
296
+ Self::CaptureOverflow => formatter.write_str("grep capture limit exceeded"),
297
+ Self::CountOverflow => formatter.write_str("grep occurrence count overflowed"),
298
+ }
299
+ }
300
+ }
301
+
302
+ impl std::error::Error for GrepSinkError {}
303
+
304
+ impl SinkError for GrepSinkError {
305
+ fn error_message<T: fmt::Display>(message: T) -> Self {
306
+ Self::Search(message.to_string())
307
+ }
308
+
309
+ fn error_io(error: io::Error) -> Self {
310
+ Self::Io(error)
311
+ }
312
+ }
313
+
314
+ struct SinkControl<'a> {
315
+ matcher: &'a RegexMatcher,
316
+ operation: Option<&'a dyn WorkCheckpoint>,
317
+ occurrences_since_check: usize,
318
+ bytes_since_check: usize,
319
+ }
320
+
321
+ impl<'a> SinkControl<'a> {
322
+ fn new(matcher: &'a RegexMatcher, operation: Option<&'a dyn WorkCheckpoint>) -> Self {
323
+ Self {
324
+ matcher,
325
+ operation,
326
+ occurrences_since_check: 0,
327
+ bytes_since_check: 0,
328
+ }
329
+ }
330
+
331
+ fn checkpoint(&self) -> Result<(), GrepSinkError> {
332
+ match self.operation.map(WorkCheckpoint::check_work) {
333
+ Some(Err(stop)) => Err(GrepSinkError::Stopped(stop)),
334
+ Some(Ok(())) | None => Ok(()),
335
+ }
336
+ }
337
+
338
+ fn sink_match_checkpoint(&mut self) -> Result<(), GrepSinkError> {
339
+ self.checkpoint()?;
340
+ #[cfg(test)]
341
+ if let Some(operation) = self.operation {
342
+ operation.stage(crate::operation::TestStage::SinkMatch);
343
+ }
344
+ self.checkpoint()?;
345
+ Ok(())
346
+ }
347
+
348
+ fn occurrence_checkpoint(&mut self, span_bytes: usize) -> Result<(), GrepSinkError> {
349
+ self.occurrences_since_check = self.occurrences_since_check.saturating_add(1);
350
+ self.bytes_since_check = self.bytes_since_check.saturating_add(span_bytes);
351
+ if self.occurrences_since_check < OCCURRENCE_CHECK_INTERVAL
352
+ && self.bytes_since_check < OCCURRENCE_BYTE_CHECK_INTERVAL
353
+ {
354
+ return Ok(());
355
+ }
356
+ self.occurrences_since_check = 0;
357
+ self.bytes_since_check = 0;
358
+ self.checkpoint()?;
359
+ #[cfg(test)]
360
+ if let Some(operation) = self.operation {
361
+ operation.stage(crate::operation::TestStage::OccurrenceBatch);
362
+ }
363
+ self.checkpoint()
364
+ }
365
+ }
366
+
367
+ fn impossible_matcher_error(error: grep_matcher::NoError) -> GrepSinkError {
368
+ let _ = error;
369
+ unreachable!("RegexMatcher's error type has no constructible value")
370
+ }
371
+
372
+ struct ExistsSink<'a> {
373
+ control: SinkControl<'a>,
374
+ matched: bool,
375
+ }
376
+
377
+ impl Sink for ExistsSink<'_> {
378
+ type Error = GrepSinkError;
379
+
380
+ fn matched(
381
+ &mut self,
382
+ _searcher: &Searcher,
383
+ matched: &SinkMatch<'_>,
384
+ ) -> Result<bool, Self::Error> {
385
+ self.control.sink_match_checkpoint()?;
386
+ let bytes = matched.bytes();
387
+ let matcher = self.control.matcher;
388
+ let iter = matcher.try_find_iter(bytes, |found| {
389
+ if is_synthetic_trailing_empty(bytes, found) {
390
+ return Ok(true);
391
+ }
392
+ self.control
393
+ .occurrence_checkpoint(found.end().saturating_sub(found.start()))?;
394
+ self.matched = true;
395
+ Ok(false)
396
+ });
397
+ match iter {
398
+ Err(error) => Err(impossible_matcher_error(error)),
399
+ Ok(Err(error)) => Err(error),
400
+ Ok(Ok(())) => Ok(!self.matched),
401
+ }
402
+ }
403
+ }
404
+
405
+ struct CountSink<'a> {
406
+ control: SinkControl<'a>,
407
+ occurrence_total: usize,
408
+ }
409
+
410
+ impl Sink for CountSink<'_> {
411
+ type Error = GrepSinkError;
412
+
413
+ fn matched(
414
+ &mut self,
415
+ _searcher: &Searcher,
416
+ matched: &SinkMatch<'_>,
417
+ ) -> Result<bool, Self::Error> {
418
+ self.control.sink_match_checkpoint()?;
419
+ let bytes = matched.bytes();
420
+ let matcher = self.control.matcher;
421
+ let iter = matcher.try_find_iter(bytes, |found| {
422
+ if is_synthetic_trailing_empty(bytes, found) {
423
+ return Ok(true);
424
+ }
425
+ self.control
426
+ .occurrence_checkpoint(found.end().saturating_sub(found.start()))?;
427
+ self.occurrence_total = self
428
+ .occurrence_total
429
+ .checked_add(1)
430
+ .ok_or(GrepSinkError::CountOverflow)?;
431
+ Ok(true)
432
+ });
433
+ match iter {
434
+ Err(error) => Err(impossible_matcher_error(error)),
435
+ Ok(Err(error)) => Err(error),
436
+ Ok(Ok(())) => Ok(true),
437
+ }
438
+ }
439
+ }
440
+
441
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
442
+ enum ContentMode {
443
+ Line,
444
+ Occurrence,
445
+ }
446
+
447
+ struct ContentSink<'a> {
448
+ control: SinkControl<'a>,
449
+ backing: Arc<SearchText>,
450
+ mode: ContentMode,
451
+ spec: ContentSpec,
452
+ occurrences: BTreeMap<usize, Vec<Occurrence>>,
453
+ entries: Vec<ContentEntry>,
454
+ lines: BTreeMap<usize, CapturedLine>,
455
+ occurrence_total: usize,
456
+ entries_seen: usize,
457
+ selected_entry_count: usize,
458
+ last_matching_line: Option<usize>,
459
+ current_line_selected: bool,
460
+ recent_lines: VecDeque<(usize, CapturedLine)>,
461
+ recent_bytes: usize,
462
+ stored_bytes: usize,
463
+ after_context_until: usize,
464
+ capture_overflow: bool,
465
+ }
466
+
467
+ impl<'a> ContentSink<'a> {
468
+ fn new(
469
+ matcher: &'a RegexMatcher,
470
+ operation: Option<&'a dyn WorkCheckpoint>,
471
+ mode: ContentMode,
472
+ spec: ContentSpec,
473
+ backing: Arc<SearchText>,
474
+ ) -> Self {
475
+ Self {
476
+ control: SinkControl::new(matcher, operation),
477
+ backing,
478
+ mode,
479
+ spec,
480
+ occurrences: BTreeMap::new(),
481
+ entries: Vec::new(),
482
+ lines: BTreeMap::new(),
483
+ occurrence_total: 0,
484
+ entries_seen: 0,
485
+ selected_entry_count: 0,
486
+ last_matching_line: None,
487
+ current_line_selected: false,
488
+ recent_lines: VecDeque::with_capacity(spec.before_context.min(1_024)),
489
+ recent_bytes: 0,
490
+ stored_bytes: 0,
491
+ after_context_until: 0,
492
+ capture_overflow: false,
493
+ }
494
+ }
495
+
496
+ fn reserve(&mut self, additional: usize) -> bool {
497
+ if self
498
+ .stored_bytes
499
+ .saturating_add(self.recent_bytes)
500
+ .saturating_add(additional)
501
+ > self.spec.capture_heap_limit_bytes
502
+ {
503
+ self.capture_overflow = true;
504
+ false
505
+ } else {
506
+ true
507
+ }
508
+ }
509
+
510
+ fn store_line(&mut self, line_number: usize, line: CapturedLine) {
511
+ if self.lines.contains_key(&line_number) {
512
+ return;
513
+ }
514
+ let len = line.byte_len();
515
+ if !self.reserve(len) {
516
+ return;
517
+ }
518
+ self.stored_bytes = self.stored_bytes.saturating_add(len);
519
+ self.lines.insert(line_number, line);
520
+ }
521
+
522
+ fn push_recent(&mut self, line_number: usize, line: CapturedLine) {
523
+ if self.spec.before_context == 0
524
+ || self
525
+ .recent_lines
526
+ .back()
527
+ .is_some_and(|(recent_line, _)| *recent_line == line_number)
528
+ {
529
+ return;
530
+ }
531
+ while self.recent_lines.len() >= self.spec.before_context {
532
+ if let Some((_, removed)) = self.recent_lines.pop_front() {
533
+ self.recent_bytes = self.recent_bytes.saturating_sub(removed.byte_len());
534
+ }
535
+ }
536
+ let len = line.byte_len();
537
+ if !self.reserve(len) {
538
+ return;
539
+ }
540
+ self.recent_bytes = self.recent_bytes.saturating_add(len);
541
+ self.recent_lines.push_back((line_number, line));
542
+ }
543
+
544
+ fn commit_recent(&mut self) {
545
+ let recent = std::mem::take(&mut self.recent_lines);
546
+ self.recent_bytes = 0;
547
+ for (line_number, line) in recent {
548
+ if self.lines.contains_key(&line_number) {
549
+ continue;
550
+ }
551
+ self.stored_bytes = self.stored_bytes.saturating_add(line.byte_len());
552
+ self.lines.insert(line_number, line);
553
+ }
554
+ }
555
+
556
+ fn increment_entries_seen(&mut self) -> Result<(), GrepSinkError> {
557
+ self.entries_seen = self
558
+ .entries_seen
559
+ .checked_add(1)
560
+ .ok_or(GrepSinkError::CountOverflow)?;
561
+ Ok(())
562
+ }
563
+
564
+ fn select_line(&mut self, line_number: usize) -> Result<bool, GrepSinkError> {
565
+ if self.last_matching_line == Some(line_number) {
566
+ return Ok(self.current_line_selected);
567
+ }
568
+ self.last_matching_line = Some(line_number);
569
+ self.increment_entries_seen()?;
570
+ self.current_line_selected = self.entries_seen > self.spec.skip_entries
571
+ && self
572
+ .spec
573
+ .max_selected_entries
574
+ .is_none_or(|limit| self.selected_entry_count < limit);
575
+ if self.current_line_selected {
576
+ self.selected_entry_count = self
577
+ .selected_entry_count
578
+ .checked_add(1)
579
+ .ok_or(GrepSinkError::CountOverflow)?;
580
+ self.entries.push(ContentEntry::MatchingLine(line_number));
581
+ self.commit_recent();
582
+ self.after_context_until = self
583
+ .after_context_until
584
+ .max(line_number.saturating_add(self.spec.after_context));
585
+ }
586
+ Ok(self.current_line_selected)
587
+ }
588
+
589
+ fn select_occurrence(&mut self, end_line: usize) -> Result<bool, GrepSinkError> {
590
+ self.increment_entries_seen()?;
591
+ let selected = self.entries_seen > self.spec.skip_entries
592
+ && self
593
+ .spec
594
+ .max_selected_entries
595
+ .is_none_or(|limit| self.selected_entry_count < limit);
596
+ if selected {
597
+ self.selected_entry_count = self
598
+ .selected_entry_count
599
+ .checked_add(1)
600
+ .ok_or(GrepSinkError::CountOverflow)?;
601
+ self.commit_recent();
602
+ self.after_context_until = self
603
+ .after_context_until
604
+ .max(end_line.saturating_add(self.spec.after_context));
605
+ }
606
+ Ok(selected)
607
+ }
608
+
609
+ fn limit_reached(&self) -> bool {
610
+ self.spec
611
+ .max_selected_entries
612
+ .is_some_and(|limit| self.selected_entry_count >= limit)
613
+ }
614
+
615
+ fn store_occurrence(&mut self, pending: PendingOccurrence) {
616
+ let metadata_bytes = std::mem::size_of::<Occurrence>().saturating_add(
617
+ pending
618
+ .line_spans
619
+ .len()
620
+ .saturating_mul(std::mem::size_of::<LineMatchSpan>()),
621
+ );
622
+ let text_bytes = if self.spec.capture_match_text {
623
+ usize::try_from(
624
+ pending
625
+ .match_range
626
+ .end
627
+ .saturating_sub(pending.match_range.start),
628
+ )
629
+ .unwrap_or(usize::MAX)
630
+ } else {
631
+ 0
632
+ };
633
+ if !self.reserve(metadata_bytes.saturating_add(text_bytes)) {
634
+ return;
635
+ }
636
+ self.stored_bytes = self
637
+ .stored_bytes
638
+ .saturating_add(metadata_bytes)
639
+ .saturating_add(text_bytes);
640
+ let occurrences = self.occurrences.entry(pending.start_line).or_default();
641
+ let occurrence_index = occurrences.len();
642
+ occurrences.push(Occurrence {
643
+ matched_text: self
644
+ .spec
645
+ .capture_match_text
646
+ .then(|| TextRange::new(Arc::clone(&self.backing), pending.match_range)),
647
+ start_line: pending.start_line,
648
+ end_line: pending.end_line,
649
+ line_spans: pending.line_spans,
650
+ });
651
+ if self.mode == ContentMode::Occurrence {
652
+ self.entries.push(ContentEntry::Occurrence {
653
+ start_line: pending.start_line,
654
+ occurrence_index,
655
+ });
656
+ }
657
+ }
658
+
659
+ fn capture_callback_lines(
660
+ &mut self,
661
+ layout: &LineLayout,
662
+ selected_lines: &LineIntervals,
663
+ absolute_start: u64,
664
+ ) -> Result<(), GrepSinkError> {
665
+ let mut interval_cursor = 0_usize;
666
+ for (line_delta, line) in layout.lines.iter().enumerate() {
667
+ let line_number = layout.first_line_number.saturating_add(line_delta);
668
+ let selected = selected_lines.contains(line_number, &mut interval_cursor);
669
+ if selected || line_number <= self.after_context_until {
670
+ self.store_line(
671
+ line_number,
672
+ CapturedLine::new(
673
+ Arc::clone(&self.backing),
674
+ absolute_range(absolute_start, line.content_start..line.content_end)?,
675
+ ),
676
+ );
677
+ }
678
+ if self.spec.before_context > 0 {
679
+ self.push_recent(
680
+ line_number,
681
+ CapturedLine::new(
682
+ Arc::clone(&self.backing),
683
+ absolute_range(absolute_start, line.content_start..line.content_end)?,
684
+ ),
685
+ );
686
+ }
687
+ if self.capture_overflow {
688
+ return Ok(());
689
+ }
690
+ }
691
+ Ok(())
692
+ }
693
+ }
694
+
695
+ impl Sink for ContentSink<'_> {
696
+ type Error = GrepSinkError;
697
+
698
+ fn matched(
699
+ &mut self,
700
+ _searcher: &Searcher,
701
+ matched: &SinkMatch<'_>,
702
+ ) -> Result<bool, Self::Error> {
703
+ self.control.sink_match_checkpoint()?;
704
+ let bytes = matched.bytes();
705
+ let absolute_start = matched.absolute_byte_offset();
706
+ let first_line_number = matched.line_number().unwrap_or(1) as usize;
707
+ let mut layout = None;
708
+ let mut cursor = OccurrenceCursor::default();
709
+ let mut selected_lines = LineIntervals::default();
710
+ let mut pending = Vec::new();
711
+ let mode = self.mode;
712
+ let matcher = self.control.matcher;
713
+ let iter = matcher.try_find_iter(bytes, |found| {
714
+ if is_synthetic_trailing_empty(bytes, found) {
715
+ return Ok(true);
716
+ }
717
+ self.control
718
+ .occurrence_checkpoint(found.end().saturating_sub(found.start()))?;
719
+ let mut located = None;
720
+ let selected = match mode {
721
+ ContentMode::Line => self.select_line(first_line_number)?,
722
+ ContentMode::Occurrence => {
723
+ if layout.is_none() {
724
+ layout = Some(LineLayout::new(bytes, first_line_number));
725
+ }
726
+ let indexes = layout
727
+ .as_ref()
728
+ .expect("the occurrence layout was initialized")
729
+ .locate(found, &mut cursor);
730
+ let end_line = first_line_number.saturating_add(indexes.1);
731
+ located = Some(indexes);
732
+ self.select_occurrence(end_line)?
733
+ }
734
+ };
735
+ self.occurrence_total = self
736
+ .occurrence_total
737
+ .checked_add(1)
738
+ .ok_or(GrepSinkError::CountOverflow)?;
739
+ if selected {
740
+ if layout.is_none() {
741
+ layout = Some(LineLayout::new(bytes, first_line_number));
742
+ }
743
+ let layout = layout
744
+ .as_mut()
745
+ .expect("a selected occurrence has one line layout");
746
+ let (start_index, end_index) =
747
+ located.unwrap_or_else(|| layout.locate(found, &mut cursor));
748
+ let start_line = first_line_number.saturating_add(start_index);
749
+ let end_line = first_line_number.saturating_add(end_index);
750
+ selected_lines.insert(start_line, end_line);
751
+ pending.push(PendingOccurrence {
752
+ match_range: absolute_range(absolute_start, found.start()..found.end())?,
753
+ start_line,
754
+ end_line,
755
+ line_spans: layout.line_spans(found, start_index, end_index),
756
+ });
757
+ }
758
+ Ok(match mode {
759
+ ContentMode::Line => selected,
760
+ ContentMode::Occurrence => !self.limit_reached(),
761
+ })
762
+ });
763
+ match iter {
764
+ Err(error) => return Err(impossible_matcher_error(error)),
765
+ Ok(Err(error)) => return Err(error),
766
+ Ok(Ok(())) => {}
767
+ }
768
+ if !pending.is_empty() {
769
+ for occurrence in pending {
770
+ self.store_occurrence(occurrence);
771
+ if self.capture_overflow {
772
+ break;
773
+ }
774
+ }
775
+ if !self.capture_overflow {
776
+ self.capture_callback_lines(
777
+ layout
778
+ .as_ref()
779
+ .expect("captured occurrences have one line layout"),
780
+ &selected_lines,
781
+ absolute_start,
782
+ )?;
783
+ }
784
+ }
785
+ if self.capture_overflow {
786
+ return Err(GrepSinkError::CaptureOverflow);
787
+ }
788
+ Ok(!self.limit_reached())
789
+ }
790
+
791
+ fn context(
792
+ &mut self,
793
+ _searcher: &Searcher,
794
+ context: &SinkContext<'_>,
795
+ ) -> Result<bool, Self::Error> {
796
+ self.control.checkpoint()?;
797
+ if let Some(line_number) = context.line_number() {
798
+ let line_number = line_number as usize;
799
+ let capture_after = line_number <= self.after_context_until;
800
+ let capture_recent = self.spec.before_context > 0
801
+ && matches!(
802
+ context.kind(),
803
+ SinkContextKind::Before | SinkContextKind::After
804
+ );
805
+ if capture_after || capture_recent {
806
+ let bytes = strip_line_terminator(context.bytes());
807
+ let range = absolute_range(context.absolute_byte_offset(), 0..bytes.len())?;
808
+ if capture_after {
809
+ self.store_line(
810
+ line_number,
811
+ CapturedLine::new(Arc::clone(&self.backing), range.clone()),
812
+ );
813
+ }
814
+ if capture_recent {
815
+ self.push_recent(
816
+ line_number,
817
+ CapturedLine::new(Arc::clone(&self.backing), range),
818
+ );
819
+ }
820
+ }
821
+ }
822
+ if self.capture_overflow {
823
+ Err(GrepSinkError::CaptureOverflow)
824
+ } else {
825
+ Ok(true)
826
+ }
827
+ }
828
+
829
+ fn context_break(&mut self, _searcher: &Searcher) -> Result<bool, Self::Error> {
830
+ self.control.checkpoint()?;
831
+ self.recent_lines.clear();
832
+ self.recent_bytes = 0;
833
+ self.after_context_until = 0;
834
+ Ok(true)
835
+ }
836
+ }
837
+
838
+ enum PlanSinkKind<'a> {
839
+ Exists(ExistsSink<'a>),
840
+ Count(CountSink<'a>),
841
+ Content(Box<ContentSink<'a>>),
842
+ }
843
+
844
+ /// One concrete sink dispatch point without mode booleans in hot callbacks.
845
+ pub(crate) struct PlanSink<'a> {
846
+ inner: PlanSinkKind<'a>,
847
+ }
848
+
849
+ impl<'a> PlanSink<'a> {
850
+ pub(crate) fn new(
851
+ matcher: &'a RegexMatcher,
852
+ plan: GrepSearchPlan,
853
+ operation: Option<&'a dyn WorkCheckpoint>,
854
+ content_backing: Option<Arc<SearchText>>,
855
+ ) -> Self {
856
+ let inner = match plan {
857
+ GrepSearchPlan::Exists => PlanSinkKind::Exists(ExistsSink {
858
+ control: SinkControl::new(matcher, operation),
859
+ matched: false,
860
+ }),
861
+ GrepSearchPlan::Count => PlanSinkKind::Count(CountSink {
862
+ control: SinkControl::new(matcher, operation),
863
+ occurrence_total: 0,
864
+ }),
865
+ GrepSearchPlan::ContentLine(spec) => PlanSinkKind::Content(Box::new(ContentSink::new(
866
+ matcher,
867
+ operation,
868
+ ContentMode::Line,
869
+ {
870
+ debug_assert!(!spec.multiline);
871
+ spec
872
+ },
873
+ content_backing
874
+ .clone()
875
+ .expect("content plans require one immutable UTF-8 backing"),
876
+ ))),
877
+ GrepSearchPlan::ContentOccurrence(spec) => {
878
+ PlanSinkKind::Content(Box::new(ContentSink::new(
879
+ matcher,
880
+ operation,
881
+ ContentMode::Occurrence,
882
+ spec,
883
+ content_backing.expect("content plans require one immutable UTF-8 backing"),
884
+ )))
885
+ }
886
+ };
887
+ Self { inner }
888
+ }
889
+
890
+ pub(crate) fn into_output(
891
+ self,
892
+ path: String,
893
+ total_lines: usize,
894
+ has_trailing_newline: bool,
895
+ ) -> SinkOutput {
896
+ match self.inner {
897
+ PlanSinkKind::Exists(sink) => SinkOutput {
898
+ result: sink.matched.then(|| FileResult::exists(path)),
899
+ entries_seen: usize::from(sink.matched),
900
+ },
901
+ PlanSinkKind::Count(sink) => SinkOutput {
902
+ result: (sink.occurrence_total > 0)
903
+ .then(|| FileResult::count(path, sink.occurrence_total)),
904
+ entries_seen: usize::from(sink.occurrence_total > 0),
905
+ },
906
+ PlanSinkKind::Content(sink) => {
907
+ let sink = *sink;
908
+ let entries_seen = sink.entries_seen;
909
+ let result = (!sink.entries.is_empty()).then(|| {
910
+ FileResult::with_content(
911
+ path,
912
+ ContentPayload {
913
+ lines: sink.lines,
914
+ occurrences: sink.occurrences,
915
+ entries: sink.entries,
916
+ occurrence_total: sink.occurrence_total,
917
+ total_lines,
918
+ has_trailing_newline,
919
+ },
920
+ )
921
+ });
922
+ SinkOutput {
923
+ result,
924
+ entries_seen,
925
+ }
926
+ }
927
+ }
928
+ }
929
+ }
930
+
931
+ impl Sink for PlanSink<'_> {
932
+ type Error = GrepSinkError;
933
+
934
+ fn matched(
935
+ &mut self,
936
+ searcher: &Searcher,
937
+ matched: &SinkMatch<'_>,
938
+ ) -> Result<bool, Self::Error> {
939
+ match &mut self.inner {
940
+ PlanSinkKind::Exists(sink) => sink.matched(searcher, matched),
941
+ PlanSinkKind::Count(sink) => sink.matched(searcher, matched),
942
+ PlanSinkKind::Content(sink) => sink.matched(searcher, matched),
943
+ }
944
+ }
945
+
946
+ fn context(
947
+ &mut self,
948
+ searcher: &Searcher,
949
+ context: &SinkContext<'_>,
950
+ ) -> Result<bool, Self::Error> {
951
+ match &mut self.inner {
952
+ PlanSinkKind::Content(sink) => sink.context(searcher, context),
953
+ PlanSinkKind::Exists(_) | PlanSinkKind::Count(_) => Ok(true),
954
+ }
955
+ }
956
+
957
+ fn context_break(&mut self, searcher: &Searcher) -> Result<bool, Self::Error> {
958
+ match &mut self.inner {
959
+ PlanSinkKind::Content(sink) => sink.context_break(searcher),
960
+ PlanSinkKind::Exists(_) | PlanSinkKind::Count(_) => Ok(true),
961
+ }
962
+ }
963
+ }
964
+
965
+ #[derive(Clone, Copy, Debug)]
966
+ struct LogicalLine {
967
+ content_start: usize,
968
+ content_end: usize,
969
+ full_end: usize,
970
+ }
971
+
972
+ struct LineLayout<'a> {
973
+ bytes: &'a [u8],
974
+ first_line_number: usize,
975
+ lines: Vec<LogicalLine>,
976
+ char_cursors: Vec<CharCursor>,
977
+ }
978
+
979
+ #[derive(Clone, Copy, Debug)]
980
+ struct CharCursor {
981
+ byte_offset: usize,
982
+ char_offset: usize,
983
+ }
984
+
985
+ impl<'a> LineLayout<'a> {
986
+ fn new(bytes: &'a [u8], first_line_number: usize) -> Self {
987
+ let mut lines = Vec::new();
988
+ let mut line_start = 0_usize;
989
+ for (index, byte) in bytes.iter().enumerate() {
990
+ if *byte != b'\n' {
991
+ continue;
992
+ }
993
+ let content_end = if index > line_start && bytes[index - 1] == b'\r' {
994
+ index - 1
995
+ } else {
996
+ index
997
+ };
998
+ lines.push(LogicalLine {
999
+ content_start: line_start,
1000
+ content_end,
1001
+ full_end: index + 1,
1002
+ });
1003
+ line_start = index + 1;
1004
+ }
1005
+ if line_start < bytes.len() || lines.is_empty() {
1006
+ lines.push(LogicalLine {
1007
+ content_start: line_start,
1008
+ content_end: bytes.len(),
1009
+ full_end: bytes.len(),
1010
+ });
1011
+ }
1012
+ let char_cursors = lines
1013
+ .iter()
1014
+ .map(|line| CharCursor {
1015
+ byte_offset: line.content_start,
1016
+ char_offset: 0,
1017
+ })
1018
+ .collect();
1019
+ Self {
1020
+ bytes,
1021
+ first_line_number,
1022
+ lines,
1023
+ char_cursors,
1024
+ }
1025
+ }
1026
+
1027
+ fn locate(&self, found: Match, cursor: &mut OccurrenceCursor) -> (usize, usize) {
1028
+ while cursor.start + 1 < self.lines.len()
1029
+ && !offset_is_in_line(&self.lines, cursor.start, found.start())
1030
+ {
1031
+ cursor.start += 1;
1032
+ }
1033
+ cursor.end = cursor.end.max(cursor.start);
1034
+ let end_offset = if found.end() > found.start() {
1035
+ found.end() - 1
1036
+ } else {
1037
+ found.start()
1038
+ };
1039
+ while cursor.end + 1 < self.lines.len()
1040
+ && !offset_is_in_line(&self.lines, cursor.end, end_offset)
1041
+ {
1042
+ cursor.end += 1;
1043
+ }
1044
+ (cursor.start, cursor.end)
1045
+ }
1046
+
1047
+ fn line_spans(
1048
+ &mut self,
1049
+ found: Match,
1050
+ start_index: usize,
1051
+ end_index: usize,
1052
+ ) -> Vec<LineMatchSpan> {
1053
+ let mut spans = Vec::with_capacity(end_index.saturating_sub(start_index) + 1);
1054
+ for line_index in start_index..=end_index {
1055
+ let line = self.lines[line_index];
1056
+ let overlap_start = found.start().max(line.content_start).min(line.content_end);
1057
+ let overlap_end = found.end().min(line.content_end).max(overlap_start);
1058
+ let anchor = if overlap_start < overlap_end {
1059
+ overlap_start
1060
+ } else if found.start() <= line.content_start {
1061
+ line.content_start
1062
+ } else {
1063
+ line.content_end
1064
+ };
1065
+ let match_char_start = self.char_offset(line_index, anchor);
1066
+ let match_char_len = if overlap_start < overlap_end {
1067
+ self.char_offset(line_index, overlap_end)
1068
+ .saturating_sub(match_char_start)
1069
+ } else {
1070
+ 0
1071
+ };
1072
+ spans.push(LineMatchSpan {
1073
+ line_number: self.first_line_number.saturating_add(line_index),
1074
+ match_char_start,
1075
+ match_char_len,
1076
+ });
1077
+ }
1078
+ spans
1079
+ }
1080
+
1081
+ fn char_offset(&mut self, line_index: usize, absolute: usize) -> usize {
1082
+ let line = self.lines[line_index];
1083
+ let target = absolute.clamp(line.content_start, line.content_end);
1084
+ let cursor = &mut self.char_cursors[line_index];
1085
+ debug_assert!(target >= cursor.byte_offset);
1086
+ if target > cursor.byte_offset {
1087
+ let text =
1088
+ unsafe { std::str::from_utf8_unchecked(&self.bytes[cursor.byte_offset..target]) };
1089
+ cursor.char_offset = cursor.char_offset.saturating_add(text.chars().count());
1090
+ cursor.byte_offset = target;
1091
+ }
1092
+ cursor.char_offset
1093
+ }
1094
+ }
1095
+
1096
+ #[derive(Default)]
1097
+ struct OccurrenceCursor {
1098
+ start: usize,
1099
+ end: usize,
1100
+ }
1101
+
1102
+ #[derive(Default)]
1103
+ struct LineIntervals {
1104
+ ranges: Vec<(usize, usize)>,
1105
+ }
1106
+
1107
+ impl LineIntervals {
1108
+ fn insert(&mut self, start: usize, end: usize) {
1109
+ if let Some(last) = self.ranges.last_mut()
1110
+ && start <= last.1.saturating_add(1)
1111
+ {
1112
+ last.1 = last.1.max(end);
1113
+ } else {
1114
+ self.ranges.push((start, end));
1115
+ }
1116
+ }
1117
+
1118
+ fn contains(&self, line: usize, cursor: &mut usize) -> bool {
1119
+ while *cursor < self.ranges.len() && self.ranges[*cursor].1 < line {
1120
+ *cursor += 1;
1121
+ }
1122
+ self.ranges
1123
+ .get(*cursor)
1124
+ .is_some_and(|(start, end)| *start <= line && line <= *end)
1125
+ }
1126
+ }
1127
+
1128
+ struct PendingOccurrence {
1129
+ match_range: Range<u64>,
1130
+ start_line: usize,
1131
+ end_line: usize,
1132
+ line_spans: Vec<LineMatchSpan>,
1133
+ }
1134
+
1135
+ fn offset_is_in_line(lines: &[LogicalLine], index: usize, offset: usize) -> bool {
1136
+ let line = lines[index];
1137
+ offset < line.full_end || (index + 1 == lines.len() && offset <= line.full_end)
1138
+ }
1139
+
1140
+ fn strip_line_terminator(bytes: &[u8]) -> &[u8] {
1141
+ let bytes = bytes.strip_suffix(b"\n").unwrap_or(bytes);
1142
+ bytes.strip_suffix(b"\r").unwrap_or(bytes)
1143
+ }
1144
+
1145
+ fn absolute_range(base: u64, local: Range<usize>) -> Result<Range<u64>, GrepSinkError> {
1146
+ let start = u64::try_from(local.start)
1147
+ .ok()
1148
+ .and_then(|offset| base.checked_add(offset));
1149
+ let end = u64::try_from(local.end)
1150
+ .ok()
1151
+ .and_then(|offset| base.checked_add(offset));
1152
+ match (start, end) {
1153
+ (Some(start), Some(end)) if start <= end => Ok(start..end),
1154
+ _ => Err(GrepSinkError::Search(
1155
+ "ripgrep reported a byte offset that overflowed the search source".to_string(),
1156
+ )),
1157
+ }
1158
+ }
1159
+
1160
+ fn is_synthetic_trailing_empty(bytes: &[u8], found: Match) -> bool {
1161
+ found.start() == found.end() && found.end() == bytes.len() && bytes.ends_with(b"\n")
1162
+ }