Skip to repository content537 lines · 18.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:38:55.033Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
anchor.rs
1use crate::{
2 ExcerptSummary, MultiBufferDimension, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
3 PathKeyIndex, find_diff_state,
4};
5
6use super::{MultiBufferSnapshot, ToOffset, ToPoint};
7use language::{BufferSnapshot, Point};
8use std::{
9 cmp::Ordering,
10 ops::{Add, AddAssign, Range, Sub},
11};
12use sum_tree::Bias;
13use text::BufferId;
14
15/// A multibuffer anchor derived from an anchor into a specific excerpted buffer.
16#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
17pub struct ExcerptAnchor {
18 pub(crate) text_anchor: text::Anchor,
19 pub(crate) path: PathKeyIndex,
20 pub(crate) diff_base_anchor: Option<text::Anchor>,
21}
22
23/// A stable reference to a position within a [`MultiBuffer`](super::MultiBuffer).
24///
25/// Unlike simple offsets, anchors remain valid as the text is edited, automatically
26/// adjusting to reflect insertions and deletions around them.
27#[derive(Clone, Copy, Eq, PartialEq, Hash)]
28pub enum Anchor {
29 /// An anchor that always resolves to the start of the multibuffer.
30 Min,
31 /// An anchor that's attached to a specific excerpted buffer.
32 Excerpt(ExcerptAnchor),
33 /// An anchor that always resolves to the end of the multibuffer.
34 Max,
35}
36
37pub(crate) enum AnchorSeekTarget<'a> {
38 // buffer no longer exists at its original path key in the multibuffer
39 Missing {
40 path_key: &'a PathKey,
41 },
42 // we have excerpts for the buffer at the expected path key
43 Excerpt {
44 path_key: &'a PathKey,
45 path_key_index: PathKeyIndex,
46 anchor: text::Anchor,
47 snapshot: &'a BufferSnapshot,
48 },
49 // no excerpts and it's a min or max anchor
50 Empty,
51}
52
53impl std::fmt::Debug for AnchorSeekTarget<'_> {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Excerpt {
57 path_key,
58 path_key_index: _,
59 anchor,
60 snapshot: _,
61 } => f
62 .debug_struct("Excerpt")
63 .field("path_key", path_key)
64 .field("anchor", anchor)
65 .finish(),
66 Self::Missing { path_key } => f
67 .debug_struct("Missing")
68 .field("path_key", path_key)
69 .finish(),
70 Self::Empty => f.debug_struct("Empty").finish(),
71 }
72 }
73}
74
75impl std::fmt::Debug for Anchor {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Anchor::Min => write!(f, "Anchor::Min"),
79 Anchor::Max => write!(f, "Anchor::Max"),
80 Anchor::Excerpt(excerpt_anchor) => write!(f, "{excerpt_anchor:?}"),
81 }
82 }
83}
84
85impl From<ExcerptAnchor> for Anchor {
86 fn from(anchor: ExcerptAnchor) -> Self {
87 Anchor::Excerpt(anchor)
88 }
89}
90
91impl ExcerptAnchor {
92 pub(crate) fn buffer_id(&self) -> BufferId {
93 self.text_anchor.buffer_id
94 }
95
96 pub(crate) fn text_anchor(&self) -> text::Anchor {
97 self.text_anchor
98 }
99
100 pub(crate) fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
101 self.diff_base_anchor = Some(diff_base_anchor);
102 self
103 }
104
105 pub(crate) fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> Ordering {
106 let Some(self_path_key) = snapshot.path_keys.get_index(self.path.0 as usize) else {
107 panic!("anchor's path was never added to multibuffer")
108 };
109 let Some(other_path_key) = snapshot.path_keys.get_index(other.path.0 as usize) else {
110 panic!("anchor's path was never added to multibuffer")
111 };
112
113 match self_path_key.cmp(other_path_key) {
114 Ordering::Equal => (),
115 ordering => return ordering,
116 }
117
118 // in the case that you removed the buffer containing self,
119 // and added the buffer containing other with the same path key
120 // (ordering is arbitrary but consistent)
121 if self.text_anchor.buffer_id != other.text_anchor.buffer_id {
122 return self.text_anchor.buffer_id.cmp(&other.text_anchor.buffer_id);
123 }
124
125 // two anchors into the same buffer at the same path
126 let Some(buffer) = snapshot
127 .buffers
128 .get(&self.text_anchor.buffer_id)
129 .filter(|buffer_state| buffer_state.path_key == *self_path_key)
130 else {
131 // buffer no longer exists at the original path (which may have been reused for a different buffer),
132 // so no way to compare the anchors
133 return Ordering::Equal;
134 };
135 // two anchors into the same buffer at the same path that still exists at that path in the multibuffer
136 let text_cmp = self
137 .text_anchor()
138 .cmp(&other.text_anchor(), &buffer.buffer_snapshot);
139 if text_cmp != Ordering::Equal {
140 return text_cmp;
141 }
142
143 if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some())
144 && let Some(base_text) = find_diff_state(&snapshot.diffs, self.text_anchor.buffer_id)
145 .map(|diff| diff.base_text())
146 {
147 let self_anchor = self.diff_base_anchor.filter(|a| a.is_valid(base_text));
148 let other_anchor = other.diff_base_anchor.filter(|a| a.is_valid(base_text));
149 return match (self_anchor, other_anchor) {
150 (Some(a), Some(b)) => a.cmp(&b, base_text),
151 (Some(_), None) => match other.text_anchor().bias {
152 Bias::Left => Ordering::Greater,
153 Bias::Right => Ordering::Less,
154 },
155 (None, Some(_)) => match self.text_anchor().bias {
156 Bias::Left => Ordering::Less,
157 Bias::Right => Ordering::Greater,
158 },
159 (None, None) => Ordering::Equal,
160 };
161 }
162
163 Ordering::Equal
164 }
165
166 fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Self {
167 if self.text_anchor.bias == Bias::Left {
168 return *self;
169 }
170 let Some(buffer) = snapshot.buffer_for_id(self.text_anchor.buffer_id) else {
171 return *self;
172 };
173 let text_anchor = self.text_anchor().bias_left(&buffer);
174 let ret = Self::in_buffer(self.path, text_anchor);
175 if let Some(diff_base_anchor) = self.diff_base_anchor {
176 if let Some(diff) = find_diff_state(&snapshot.diffs, self.text_anchor.buffer_id)
177 && diff_base_anchor.is_valid(&diff.base_text())
178 {
179 ret.with_diff_base_anchor(diff_base_anchor.bias_left(diff.base_text()))
180 } else {
181 ret.with_diff_base_anchor(diff_base_anchor)
182 }
183 } else {
184 ret
185 }
186 }
187
188 fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Self {
189 if self.text_anchor.bias == Bias::Right {
190 return *self;
191 }
192 let Some(buffer) = snapshot.buffer_for_id(self.text_anchor.buffer_id) else {
193 return *self;
194 };
195 let text_anchor = self.text_anchor().bias_right(&buffer);
196 let ret = Self::in_buffer(self.path, text_anchor);
197 if let Some(diff_base_anchor) = self.diff_base_anchor {
198 if let Some(diff) = find_diff_state(&snapshot.diffs, self.text_anchor.buffer_id)
199 && diff_base_anchor.is_valid(&diff.base_text())
200 {
201 ret.with_diff_base_anchor(diff_base_anchor.bias_right(diff.base_text()))
202 } else {
203 ret.with_diff_base_anchor(diff_base_anchor)
204 }
205 } else {
206 ret
207 }
208 }
209
210 #[track_caller]
211 pub(crate) fn in_buffer(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
212 ExcerptAnchor {
213 path,
214 diff_base_anchor: None,
215 text_anchor,
216 }
217 }
218
219 fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
220 let Some(target) = self.try_seek_target(snapshot) else {
221 return false;
222 };
223 let Some(buffer_snapshot) = snapshot.buffer_for_id(self.buffer_id()) else {
224 return false;
225 };
226 // Early check to avoid invalid comparisons when seeking
227 if !buffer_snapshot.can_resolve(&self.text_anchor) {
228 return false;
229 }
230 let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
231 cursor.seek(&target, Bias::Left);
232 let Some(excerpt) = cursor.item() else {
233 return false;
234 };
235 if excerpt.buffer_id != buffer_snapshot.remote_id() {
236 return false;
237 }
238 let is_valid = self.text_anchor == excerpt.range.context.start
239 || self.text_anchor == excerpt.range.context.end
240 || self.text_anchor.is_valid(&buffer_snapshot);
241 is_valid
242 && excerpt
243 .range
244 .context
245 .start
246 .cmp(&self.text_anchor(), buffer_snapshot)
247 .is_le()
248 && excerpt
249 .range
250 .context
251 .end
252 .cmp(&self.text_anchor(), buffer_snapshot)
253 .is_ge()
254 }
255
256 pub(crate) fn seek_target<'a>(
257 &self,
258 snapshot: &'a MultiBufferSnapshot,
259 ) -> AnchorSeekTarget<'a> {
260 self.try_seek_target(snapshot)
261 .expect("anchor is from different multi-buffer")
262 }
263
264 pub(crate) fn try_seek_target<'a>(
265 &self,
266 snapshot: &'a MultiBufferSnapshot,
267 ) -> Option<AnchorSeekTarget<'a>> {
268 let path_key = snapshot.try_path_for_anchor(*self)?;
269
270 let Some(state) = snapshot
271 .buffers
272 .get(&self.buffer_id())
273 .filter(|state| &state.path_key == path_key)
274 else {
275 return Some(AnchorSeekTarget::Missing { path_key });
276 };
277
278 Some(AnchorSeekTarget::Excerpt {
279 path_key,
280 path_key_index: self.path,
281 anchor: self.text_anchor(),
282 snapshot: &state.buffer_snapshot,
283 })
284 }
285}
286
287impl ToOffset for ExcerptAnchor {
288 fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset {
289 Anchor::from(*self).to_offset(snapshot)
290 }
291
292 fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 {
293 Anchor::from(*self).to_offset_utf16(snapshot)
294 }
295}
296
297impl ToPoint for ExcerptAnchor {
298 fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point {
299 Anchor::from(*self).to_point(snapshot)
300 }
301
302 fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 {
303 Anchor::from(*self).to_point_utf16(snapshot)
304 }
305}
306
307impl Anchor {
308 pub fn is_min(&self) -> bool {
309 matches!(self, Self::Min)
310 }
311
312 pub fn is_max(&self) -> bool {
313 matches!(self, Self::Max)
314 }
315
316 pub(crate) fn in_buffer(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
317 Self::Excerpt(ExcerptAnchor::in_buffer(path, text_anchor))
318 }
319
320 pub(crate) fn range_in_buffer(path: PathKeyIndex, range: Range<text::Anchor>) -> Range<Self> {
321 Self::in_buffer(path, range.start)..Self::in_buffer(path, range.end)
322 }
323
324 pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
325 match (self, other) {
326 (Anchor::Min, Anchor::Min) => return Ordering::Equal,
327 (Anchor::Max, Anchor::Max) => return Ordering::Equal,
328 (Anchor::Min, _) => return Ordering::Less,
329 (Anchor::Max, _) => return Ordering::Greater,
330 (_, Anchor::Max) => return Ordering::Less,
331 (_, Anchor::Min) => return Ordering::Greater,
332 (Anchor::Excerpt(self_excerpt_anchor), Anchor::Excerpt(other_excerpt_anchor)) => {
333 self_excerpt_anchor.cmp(other_excerpt_anchor, snapshot)
334 }
335 }
336 }
337
338 pub fn bias(&self) -> Bias {
339 match self {
340 Anchor::Min => Bias::Left,
341 Anchor::Max => Bias::Right,
342 Anchor::Excerpt(anchor) => anchor.text_anchor.bias,
343 }
344 }
345
346 pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
347 match self {
348 Anchor::Min => *self,
349 Anchor::Max => snapshot.anchor_before(snapshot.max_point()),
350 Anchor::Excerpt(anchor) => Anchor::Excerpt(anchor.bias_left(snapshot)),
351 }
352 }
353
354 pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
355 match self {
356 Anchor::Max => *self,
357 Anchor::Min => snapshot.anchor_after(Point::zero()),
358 Anchor::Excerpt(anchor) => Anchor::Excerpt(anchor.bias_right(snapshot)),
359 }
360 }
361
362 pub fn summary<D>(&self, snapshot: &MultiBufferSnapshot) -> D
363 where
364 D: MultiBufferDimension
365 + Ord
366 + Sub<Output = D::TextDimension>
367 + Sub<D::TextDimension, Output = D>
368 + AddAssign<D::TextDimension>
369 + Add<D::TextDimension, Output = D>,
370 D::TextDimension: Sub<Output = D::TextDimension> + Ord,
371 {
372 snapshot.summary_for_anchor(self)
373 }
374
375 pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
376 match self {
377 Anchor::Min | Anchor::Max => true,
378 Anchor::Excerpt(excerpt_anchor) => excerpt_anchor.is_valid(snapshot),
379 }
380 }
381
382 fn to_excerpt_anchor(&self, snapshot: &MultiBufferSnapshot) -> Option<ExcerptAnchor> {
383 match self {
384 Anchor::Min => {
385 let excerpt = snapshot.excerpts.first()?;
386
387 Some(ExcerptAnchor {
388 text_anchor: excerpt.range.context.start,
389 path: excerpt.path_key_index,
390 diff_base_anchor: None,
391 })
392 }
393 Anchor::Excerpt(excerpt_anchor) => Some(*excerpt_anchor),
394 Anchor::Max => {
395 let excerpt = snapshot.excerpts.last()?;
396
397 Some(ExcerptAnchor {
398 text_anchor: excerpt.range.context.end,
399 path: excerpt.path_key_index,
400 diff_base_anchor: None,
401 })
402 }
403 }
404 }
405
406 pub(crate) fn seek_target<'a>(
407 &self,
408 snapshot: &'a MultiBufferSnapshot,
409 ) -> AnchorSeekTarget<'a> {
410 let Some(excerpt_anchor) = self.to_excerpt_anchor(snapshot) else {
411 return AnchorSeekTarget::Empty;
412 };
413
414 excerpt_anchor.seek_target(snapshot)
415 }
416
417 pub(crate) fn excerpt_anchor(&self) -> Option<ExcerptAnchor> {
418 match self {
419 Anchor::Min | Anchor::Max => None,
420 Anchor::Excerpt(excerpt_anchor) => Some(*excerpt_anchor),
421 }
422 }
423
424 pub(crate) fn text_anchor(&self) -> Option<text::Anchor> {
425 match self {
426 Anchor::Min | Anchor::Max => None,
427 Anchor::Excerpt(excerpt_anchor) => Some(excerpt_anchor.text_anchor()),
428 }
429 }
430
431 pub fn opaque_id(&self) -> Option<[u8; 20]> {
432 self.text_anchor().map(|a| a.opaque_id())
433 }
434
435 /// Note: anchor_to_buffer_anchor is probably what you want
436 pub fn raw_text_anchor(&self) -> Option<text::Anchor> {
437 match self {
438 Anchor::Min | Anchor::Max => None,
439 Anchor::Excerpt(excerpt_anchor) => Some(excerpt_anchor.text_anchor),
440 }
441 }
442
443 pub(crate) fn try_seek_target<'a>(
444 &self,
445 snapshot: &'a MultiBufferSnapshot,
446 ) -> Option<AnchorSeekTarget<'a>> {
447 let Some(excerpt_anchor) = self.to_excerpt_anchor(snapshot) else {
448 return Some(AnchorSeekTarget::Empty);
449 };
450 excerpt_anchor.try_seek_target(snapshot)
451 }
452
453 /// Returns the text anchor for this anchor.
454 /// Panics if the anchor is from a different buffer.
455 pub fn text_anchor_in(&self, buffer: &BufferSnapshot) -> text::Anchor {
456 match self {
457 Anchor::Min => text::Anchor::min_for_buffer(buffer.remote_id()),
458 Anchor::Excerpt(excerpt_anchor) => {
459 let text_anchor = excerpt_anchor.text_anchor;
460 assert_eq!(text_anchor.buffer_id, buffer.remote_id());
461 text_anchor
462 }
463 Anchor::Max => text::Anchor::max_for_buffer(buffer.remote_id()),
464 }
465 }
466
467 pub fn diff_base_anchor(&self) -> Option<text::Anchor> {
468 self.excerpt_anchor()?.diff_base_anchor
469 }
470
471 #[cfg(any(test, feature = "test-support"))]
472 pub fn expect_text_anchor(&self) -> text::Anchor {
473 self.excerpt_anchor().unwrap().text_anchor
474 }
475
476 pub fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
477 match &mut self {
478 Anchor::Min | Anchor::Max => {}
479 Anchor::Excerpt(excerpt_anchor) => {
480 excerpt_anchor.diff_base_anchor = Some(diff_base_anchor);
481 }
482 }
483 self
484 }
485}
486
487impl ToOffset for Anchor {
488 fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset {
489 self.summary(snapshot)
490 }
491 fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 {
492 self.summary(snapshot)
493 }
494}
495
496impl ToPoint for Anchor {
497 fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
498 self.summary(snapshot)
499 }
500 fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 {
501 self.summary(snapshot)
502 }
503}
504
505pub trait AnchorRangeExt {
506 fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
507 fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
508 fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
509 fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<MultiBufferOffset>;
510 fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
511}
512
513impl AnchorRangeExt for Range<Anchor> {
514 fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering {
515 match self.start.cmp(&other.start, buffer) {
516 Ordering::Equal => other.end.cmp(&self.end, buffer),
517 ord => ord,
518 }
519 }
520
521 fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
522 self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le()
523 }
524
525 fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
526 self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le()
527 }
528
529 fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<MultiBufferOffset> {
530 self.start.to_offset(content)..self.end.to_offset(content)
531 }
532
533 fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
534 self.start.to_point(content)..self.end.to_point(content)
535 }
536}
537