Remove duplication when assigning diagnostics and hardcode provider names

Antonio Scandurra created

Change summary

crates/diagnostics/src/diagnostics.rs |  24 ++--
crates/language/src/buffer.rs         |  72 ++++++++------
crates/language/src/diagnostic_set.rs |   4 
crates/language/src/language.rs       |  16 +-
crates/project/src/project.rs         |   8 -
crates/project/src/worktree.rs        | 141 +++++++++-------------------
crates/text/src/rope.rs               |  35 +++++++
crates/text/src/text.rs               |  32 ++++++
8 files changed, 176 insertions(+), 156 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -579,7 +579,7 @@ mod tests {
     use super::*;
     use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
     use gpui::TestAppContext;
-    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
+    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry};
     use project::FakeFs;
     use serde_json::json;
     use std::sync::Arc;
@@ -637,13 +637,11 @@ mod tests {
 
         worktree.update(&mut cx, |worktree, cx| {
             worktree
-                .update_point_utf16_diagnostics(
-                    "lsp".into(),
+                .update_diagnostics_from_provider(
                     Arc::from("/test/main.rs".as_ref()),
-                    None,
                     vec![
                         DiagnosticEntry {
-                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+                            range: 20..21,
                             diagnostic: Diagnostic {
                                 message:
                                     "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -655,7 +653,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
+                            range: 40..41,
                             diagnostic: Diagnostic {
                                 message:
                                     "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -667,7 +665,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
+                            range: 58..59,
                             diagnostic: Diagnostic {
                                 message: "value moved here".to_string(),
                                 severity: DiagnosticSeverity::INFORMATION,
@@ -677,7 +675,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
+                            range: 68..69,
                             diagnostic: Diagnostic {
                                 message: "value moved here".to_string(),
                                 severity: DiagnosticSeverity::INFORMATION,
@@ -687,7 +685,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
+                            range: 112..113,
                             diagnostic: Diagnostic {
                                 message: "use of moved value\nvalue used here after move".to_string(),
                                 severity: DiagnosticSeverity::ERROR,
@@ -697,7 +695,7 @@ mod tests {
                             },
                         },
                         DiagnosticEntry {
-                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
+                            range: 122..123,
                             diagnostic: Diagnostic {
                                 message: "use of moved value\nvalue used here after move".to_string(),
                                 severity: DiagnosticSeverity::ERROR,
@@ -764,12 +762,10 @@ mod tests {
 
         worktree.update(&mut cx, |worktree, cx| {
             worktree
-                .update_point_utf16_diagnostics(
-                    "lsp".into(),
+                .update_diagnostics_from_provider(
                     Arc::from("/test/a.rs".as_ref()),
-                    None,
                     vec![DiagnosticEntry {
-                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
+                        range: 15..15,
                         diagnostic: Diagnostic {
                             message: "mismatched types\nexpected `usize`, found `char`".to_string(),
                             severity: DiagnosticSeverity::ERROR,

crates/language/src/buffer.rs 🔗

@@ -23,7 +23,7 @@ use std::{
     ffi::OsString,
     future::Future,
     iter::{Iterator, Peekable},
-    ops::{Deref, DerefMut, Range},
+    ops::{Add, Deref, DerefMut, Range, Sub},
     path::{Path, PathBuf},
     str,
     sync::Arc,
@@ -31,7 +31,7 @@ use std::{
     vec,
 };
 use sum_tree::TreeMap;
-use text::operation_queue::OperationQueue;
+use text::{operation_queue::OperationQueue, rope::TextDimension};
 pub use text::{Buffer as TextBuffer, Operation as _, *};
 use theme::SyntaxTheme;
 use tree_sitter::{InputEdit, Parser, QueryCursor, Tree};
@@ -85,6 +85,12 @@ pub struct BufferSnapshot {
     parse_count: usize,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct GroupId {
+    source: Arc<str>,
+    id: usize,
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Diagnostic {
     pub code: Option<String>,
@@ -740,13 +746,16 @@ impl Buffer {
         self.diagnostic_sets.iter().flat_map(|set| set.iter())
     }
 
-    pub fn update_diagnostics(
+    pub fn update_diagnostics<T>(
         &mut self,
         provider_name: Arc<str>,
         version: Option<i32>,
-        mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        mut diagnostics: Vec<DiagnosticEntry<T>>,
         cx: &mut ModelContext<Self>,
-    ) -> Result<Operation> {
+    ) -> Result<Operation>
+    where
+        T: ToPoint + Ord + Clip + TextDimension + Add<Output = T> + Sub<Output = T> + Copy,
+    {
         fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering {
             Ordering::Equal
                 .then_with(|| b.is_primary.cmp(&a.is_primary))
@@ -755,13 +764,6 @@ impl Buffer {
                 .then_with(|| a.message.cmp(&b.message))
         }
 
-        diagnostics.sort_unstable_by(|a, b| {
-            Ordering::Equal
-                .then_with(|| a.range.start.cmp(&b.range.start))
-                .then_with(|| b.range.end.cmp(&a.range.end))
-                .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic))
-        });
-
         let version = version.map(|version| version as usize);
         let content = if let Some(version) = version {
             let language_server = self.language_server.as_mut().unwrap();
@@ -777,14 +779,18 @@ impl Buffer {
             self.deref()
         };
 
-        let mut edits_since_save = content
-            .edits_since::<PointUtf16>(&self.saved_version)
-            .peekable();
-        let mut last_edit_old_end = PointUtf16::zero();
-        let mut last_edit_new_end = PointUtf16::zero();
-        let mut ix = 0;
-        'outer: while ix < diagnostics.len() {
-            let entry = &mut diagnostics[ix];
+        diagnostics.sort_unstable_by(|a, b| {
+            Ordering::Equal
+                .then_with(|| a.range.start.cmp(&b.range.start))
+                .then_with(|| b.range.end.cmp(&a.range.end))
+                .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic))
+        });
+
+        let mut sanitized_diagnostics = Vec::new();
+        let mut edits_since_save = content.edits_since::<T>(&self.saved_version).peekable();
+        let mut last_edit_old_end = T::default();
+        let mut last_edit_new_end = T::default();
+        'outer: for entry in diagnostics {
             let mut start = entry.range.start;
             let mut end = entry.range.end;
 
@@ -798,7 +804,6 @@ impl Buffer {
                         last_edit_new_end = edit.new.end;
                         edits_since_save.next();
                     } else if edit.old.start <= end && edit.old.end >= start {
-                        diagnostics.remove(ix);
                         continue 'outer;
                     } else {
                         break;
@@ -809,23 +814,26 @@ impl Buffer {
                 end = last_edit_new_end + (end - last_edit_old_end);
             }
 
-            entry.range = content.clip_point_utf16(start, Bias::Left)
-                ..content.clip_point_utf16(end, Bias::Right);
-
+            let range = start.clip(Bias::Left, content)..end.clip(Bias::Right, content);
+            let mut range = range.start.to_point(content)..range.end.to_point(content);
             // Expand empty ranges by one character
-            if entry.range.start == entry.range.end {
-                entry.range.end.column += 1;
-                entry.range.end = content.clip_point_utf16(entry.range.end, Bias::Right);
-                if entry.range.start == entry.range.end && entry.range.end.column > 0 {
-                    entry.range.start.column -= 1;
-                    entry.range.start = content.clip_point_utf16(entry.range.start, Bias::Left);
+            if range.start == range.end {
+                range.end.column += 1;
+                range.end = content.clip_point(range.end, Bias::Right);
+                if range.start == range.end && range.end.column > 0 {
+                    range.start.column -= 1;
+                    range.start = content.clip_point(range.start, Bias::Left);
                 }
             }
-            ix += 1;
+
+            sanitized_diagnostics.push(DiagnosticEntry {
+                range,
+                diagnostic: entry.diagnostic,
+            });
         }
         drop(edits_since_save);
 
-        let set = DiagnosticSet::new(provider_name, diagnostics, content);
+        let set = DiagnosticSet::new(provider_name, sanitized_diagnostics, content);
         self.apply_diagnostic_update(set.clone(), cx);
         Ok(Operation::UpdateDiagnostics {
             provider_name: set.provider_name().to_string(),

crates/language/src/diagnostic_set.rs 🔗

@@ -7,7 +7,7 @@ use std::{
     sync::Arc,
 };
 use sum_tree::{self, Bias, SumTree};
-use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
+use text::{Anchor, FromAnchor, Point, ToOffset};
 
 #[derive(Clone, Debug)]
 pub struct DiagnosticSet {
@@ -56,7 +56,7 @@ impl DiagnosticSet {
 
     pub fn new<I>(provider_name: Arc<str>, iter: I, buffer: &text::BufferSnapshot) -> Self
     where
-        I: IntoIterator<Item = DiagnosticEntry<PointUtf16>>,
+        I: IntoIterator<Item = DiagnosticEntry<Point>>,
     {
         let mut entries = iter.into_iter().collect::<Vec<_>>();
         entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));

crates/language/src/language.rs 🔗

@@ -65,9 +65,7 @@ pub struct BracketPair {
 }
 
 #[async_trait]
-pub trait DiagnosticSource: 'static + Send + Sync {
-    fn name(&self) -> Arc<str>;
-
+pub trait DiagnosticProvider: 'static + Send + Sync {
     async fn diagnose(
         &self,
         path: Arc<Path>,
@@ -77,7 +75,7 @@ pub trait DiagnosticSource: 'static + Send + Sync {
 pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
-    pub(crate) diagnostic_source: Option<Arc<dyn DiagnosticSource>>,
+    pub(crate) diagnostic_provider: Option<Arc<dyn DiagnosticProvider>>,
 }
 
 pub struct Grammar {
@@ -142,7 +140,7 @@ impl Language {
                     highlight_map: Default::default(),
                 })
             }),
-            diagnostic_source: None,
+            diagnostic_provider: None,
         }
     }
 
@@ -176,8 +174,8 @@ impl Language {
         Ok(self)
     }
 
-    pub fn with_diagnostic_source(mut self, source: impl DiagnosticSource) -> Self {
-        self.diagnostic_source = Some(Arc::new(source));
+    pub fn with_diagnostic_provider(mut self, source: impl DiagnosticProvider) -> Self {
+        self.diagnostic_provider = Some(Arc::new(source));
         self
     }
 
@@ -214,8 +212,8 @@ impl Language {
         }
     }
 
-    pub fn diagnostic_source(&self) -> Option<&Arc<dyn DiagnosticSource>> {
-        self.diagnostic_source.as_ref()
+    pub fn diagnostic_provider(&self) -> Option<&Arc<dyn DiagnosticProvider>> {
+        self.diagnostic_provider.as_ref()
     }
 
     pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {

crates/project/src/project.rs 🔗

@@ -507,18 +507,16 @@ impl Project {
         for worktree_handle in &self.worktrees {
             if let Some(worktree) = worktree_handle.read(cx).as_local() {
                 for language in worktree.languages() {
-                    if let Some(diagnostic_source) = language.diagnostic_source().cloned() {
+                    if let Some(provider) = language.diagnostic_provider().cloned() {
                         let worktree_path = worktree.abs_path().clone();
                         let worktree_handle = worktree_handle.downgrade();
                         cx.spawn_weak(|_, mut cx| async move {
-                            let diagnostics =
-                                diagnostic_source.diagnose(worktree_path).await.log_err()?;
+                            let diagnostics = provider.diagnose(worktree_path).await.log_err()?;
                             let worktree_handle = worktree_handle.upgrade(&cx)?;
                             worktree_handle.update(&mut cx, |worktree, cx| {
                                 for (path, diagnostics) in diagnostics {
                                     worktree
-                                        .update_offset_diagnostics(
-                                            diagnostic_source.name(),
+                                        .update_diagnostics_from_provider(
                                             path.into(),
                                             diagnostics,
                                             cx,

crates/project/src/worktree.rs 🔗

@@ -50,6 +50,8 @@ use util::{post_inc, ResultExt, TryFutureExt};
 
 lazy_static! {
     static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
+    static ref DIAGNOSTIC_PROVIDER_NAME: Arc<str> = Arc::from("diagnostic_source");
+    static ref LSP_PROVIDER_NAME: Arc<str> = Arc::from("lsp");
 }
 
 #[derive(Clone, Debug)]
@@ -672,9 +674,8 @@ impl Worktree {
         }
     }
 
-    pub fn update_lsp_diagnostics(
+    pub fn update_diagnostics_from_lsp(
         &mut self,
-        provider_name: Arc<str>,
         mut params: lsp::PublishDiagnosticsParams,
         disk_based_sources: &HashSet<String>,
         cx: &mut ModelContext<Worktree>,
@@ -743,44 +744,21 @@ impl Worktree {
             })
             .collect::<Vec<_>>();
 
-        self.update_point_utf16_diagnostics(
-            provider_name,
-            worktree_path,
-            params.version,
-            diagnostics,
-            cx,
-        )
-    }
-
-    pub fn update_offset_diagnostics(
-        &mut self,
-        provider_name: Arc<str>,
-        path: Arc<Path>,
-        diagnostics: Vec<DiagnosticEntry<usize>>,
-        cx: &mut ModelContext<Worktree>,
-    ) -> Result<()> {
         let this = self.as_local_mut().unwrap();
         for buffer in this.open_buffers.values() {
             if let Some(buffer) = buffer.upgrade(cx) {
                 if buffer
                     .read(cx)
                     .file()
-                    .map_or(false, |file| *file.path() == path)
+                    .map_or(false, |file| *file.path() == worktree_path)
                 {
                     let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
                         (
                             buffer.remote_id(),
                             buffer.update_diagnostics(
-                                provider_name,
-                                None,
-                                diagnostics
-                                    .iter()
-                                    .map(|entry| DiagnosticEntry {
-                                        range: buffer.offset_to_point_utf16(entry.range.start)
-                                            ..buffer.offset_to_point_utf16(entry.range.end),
-                                        diagnostic: entry.diagnostic.clone(),
-                                    })
-                                    .collect(),
+                                LSP_PROVIDER_NAME.clone(),
+                                params.version,
+                                diagnostics.clone(),
                                 cx,
                             ),
                         )
@@ -793,18 +771,17 @@ impl Worktree {
 
         let this = self.as_local_mut().unwrap();
         this.diagnostic_summaries
-            .insert(path.clone(), DiagnosticSummary::new(&diagnostics));
-        this.offset_diagnostics.insert(path.clone(), diagnostics);
-        cx.emit(Event::DiagnosticsUpdated(path.clone()));
+            .insert(worktree_path.clone(), DiagnosticSummary::new(&diagnostics));
+        this.lsp_diagnostics
+            .insert(worktree_path.clone(), diagnostics);
+        cx.emit(Event::DiagnosticsUpdated(worktree_path.clone()));
         Ok(())
     }
 
-    pub fn update_point_utf16_diagnostics(
+    pub fn update_diagnostics_from_provider(
         &mut self,
-        provider_name: Arc<str>,
         path: Arc<Path>,
-        version: Option<i32>,
-        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
+        diagnostics: Vec<DiagnosticEntry<usize>>,
         cx: &mut ModelContext<Worktree>,
     ) -> Result<()> {
         let this = self.as_local_mut().unwrap();
@@ -819,8 +796,8 @@ impl Worktree {
                         (
                             buffer.remote_id(),
                             buffer.update_diagnostics(
-                                provider_name,
-                                version,
+                                DIAGNOSTIC_PROVIDER_NAME.clone(),
+                                None,
                                 diagnostics.clone(),
                                 cx,
                             ),
@@ -835,26 +812,11 @@ impl Worktree {
         let this = self.as_local_mut().unwrap();
         this.diagnostic_summaries
             .insert(path.clone(), DiagnosticSummary::new(&diagnostics));
-        this.point_utf16_diagnostics
-            .insert(path.clone(), diagnostics);
+        this.provider_diagnostics.insert(path.clone(), diagnostics);
         cx.emit(Event::DiagnosticsUpdated(path.clone()));
         Ok(())
     }
 
-    fn convert_diagnostics(
-        diagnostics: &[DiagnosticEntry<usize>],
-        buffer: &Buffer,
-    ) -> Vec<DiagnosticEntry<PointUtf16>> {
-        diagnostics
-            .iter()
-            .map(|entry| DiagnosticEntry {
-                range: buffer.offset_to_point_utf16(entry.range.start)
-                    ..buffer.offset_to_point_utf16(entry.range.end),
-                diagnostic: entry.diagnostic.clone(),
-            })
-            .collect()
-    }
-
     fn send_buffer_update(
         &mut self,
         buffer_id: u64,
@@ -925,8 +887,8 @@ pub struct LocalWorktree {
     loading_buffers: LoadingBuffers,
     open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
     shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
-    point_utf16_diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
-    offset_diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>,
+    lsp_diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
+    provider_diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>,
     diagnostic_summaries: BTreeMap<Arc<Path>, DiagnosticSummary>,
     queued_operations: Vec<(u64, Operation)>,
     language_registry: Arc<LanguageRegistry>,
@@ -1034,8 +996,8 @@ impl LocalWorktree {
                 loading_buffers: Default::default(),
                 open_buffers: Default::default(),
                 shared_buffers: Default::default(),
-                point_utf16_diagnostics: Default::default(),
-                offset_diagnostics: Default::default(),
+                lsp_diagnostics: Default::default(),
+                provider_diagnostics: Default::default(),
                 diagnostic_summaries: Default::default(),
                 queued_operations: Default::default(),
                 language_registry: languages,
@@ -1104,8 +1066,6 @@ impl LocalWorktree {
             return Some(server.clone());
         }
 
-        let name: Arc<str> = language.name().into();
-
         if let Some(language_server) = language
             .start_server(self.abs_path(), cx)
             .log_err()
@@ -1121,23 +1081,15 @@ impl LocalWorktree {
                     smol::block_on(diagnostics_tx.send(params)).ok();
                 })
                 .detach();
-            cx.spawn_weak(|this, mut cx| {
-                let provider_name = name.clone();
-                async move {
-                    while let Ok(diagnostics) = diagnostics_rx.recv().await {
-                        if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
-                            handle.update(&mut cx, |this, cx| {
-                                this.update_lsp_diagnostics(
-                                    provider_name.clone(),
-                                    diagnostics,
-                                    &disk_based_sources,
-                                    cx,
-                                )
+            cx.spawn_weak(|this, mut cx| async move {
+                while let Ok(diagnostics) = diagnostics_rx.recv().await {
+                    if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
+                        handle.update(&mut cx, |this, cx| {
+                            this.update_diagnostics_from_lsp(diagnostics, &disk_based_sources, cx)
                                 .log_err();
-                            });
-                        } else {
-                            break;
-                        }
+                        });
+                    } else {
+                        break;
                     }
                 }
             })
@@ -1184,11 +1136,11 @@ impl LocalWorktree {
                 .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
                 .await?;
 
-            let (point_utf16_diagnostics, offset_diagnostics, language, language_server) = this
-                .update(&mut cx, |this, cx| {
+            let (lsp_diagnostics, provider_diagnostics, language, language_server) =
+                this.update(&mut cx, |this, cx| {
                     let this = this.as_local_mut().unwrap();
-                    let point_utf16_diagnostics = this.point_utf16_diagnostics.remove(&path);
-                    let offset_diagnostics = this.offset_diagnostics.remove(&path);
+                    let lsp_diagnostics = this.lsp_diagnostics.remove(&path);
+                    let provider_diagnostics = this.provider_diagnostics.remove(&path);
                     let language = this
                         .language_registry
                         .select_language(file.full_path())
@@ -1196,31 +1148,32 @@ impl LocalWorktree {
                     let server = language
                         .as_ref()
                         .and_then(|language| this.register_language(language, cx));
-                    (
-                        point_utf16_diagnostics,
-                        offset_diagnostics,
-                        language,
-                        server,
-                    )
+                    (lsp_diagnostics, provider_diagnostics, language, server)
                 });
 
+            let mut buffer_operations = Vec::new();
             let buffer = cx.add_model(|cx| {
                 let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx);
                 buffer.set_language(language, language_server, cx);
-                if let Some(diagnostics) = point_utf16_diagnostics {
-                    buffer
-                        .update_diagnostics(todo!(), None, diagnostics, cx)
+                if let Some(diagnostics) = lsp_diagnostics {
+                    let op = buffer
+                        .update_diagnostics(LSP_PROVIDER_NAME.clone(), None, diagnostics, cx)
                         .unwrap();
+                    buffer_operations.push(op);
                 }
-                if let Some(diagnostics) = offset_diagnostics {
-                    buffer
-                        .update_offset_diagnostics(todo!(), None, diagnostics, cx)
+                if let Some(diagnostics) = provider_diagnostics {
+                    let op = buffer
+                        .update_diagnostics(DIAGNOSTIC_PROVIDER_NAME.clone(), None, diagnostics, cx)
                         .unwrap();
+                    buffer_operations.push(op);
                 }
                 buffer
             });
 
-            this.update(&mut cx, |this, _| {
+            this.update(&mut cx, |this, cx| {
+                for op in buffer_operations {
+                    this.send_buffer_update(buffer.read(cx).remote_id(), op, cx);
+                }
                 let this = this.as_local_mut().unwrap();
                 this.open_buffers.insert(buffer.id(), buffer.downgrade());
             });
@@ -3936,7 +3889,7 @@ mod tests {
 
         worktree
             .update(&mut cx, |tree, cx| {
-                tree.update_lsp_diagnostics("lsp".into(), message, &Default::default(), cx)
+                tree.update_diagnostics_from_lsp(message, &Default::default(), cx)
             })
             .unwrap();
         let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot());

crates/text/src/rope.rs 🔗

@@ -205,6 +205,19 @@ impl Rope {
                 .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot))
     }
 
+    pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
+        if point >= self.summary().lines_utf16 {
+            return self.summary().lines;
+        }
+        let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>();
+        cursor.seek(&point, Bias::Left, &());
+        let overshoot = point - cursor.start().0;
+        cursor.start().1
+            + cursor
+                .item()
+                .map_or(Point::zero(), |chunk| chunk.point_utf16_to_point(overshoot))
+    }
+
     pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
         let mut cursor = self.chunks.cursor::<usize>();
         cursor.seek(&offset, Bias::Left, &());
@@ -583,6 +596,28 @@ impl Chunk {
         offset
     }
 
+    fn point_utf16_to_point(&self, target: PointUtf16) -> Point {
+        let mut point = Point::zero();
+        let mut point_utf16 = PointUtf16::zero();
+        for ch in self.0.chars() {
+            if point_utf16 >= target {
+                if point_utf16 > target {
+                    panic!("point {:?} is inside of character {:?}", target, ch);
+                }
+                break;
+            }
+
+            if ch == '\n' {
+                point_utf16 += PointUtf16::new(1, 0);
+                point += Point::new(1, 0);
+            } else {
+                point_utf16 += PointUtf16::new(0, ch.len_utf16() as u32);
+                point += Point::new(0, ch.len_utf8() as u32);
+            }
+        }
+        point
+    }
+
     fn clip_point(&self, target: Point, bias: Bias) -> Point {
         for (row, line) in self.0.split('\n').enumerate() {
             if row == target.row as usize {

crates/text/src/text.rs 🔗

@@ -1307,6 +1307,10 @@ impl BufferSnapshot {
         self.visible_text.point_utf16_to_offset(point)
     }
 
+    pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
+        self.visible_text.point_utf16_to_point(point)
+    }
+
     pub fn offset_to_point(&self, offset: usize) -> Point {
         self.visible_text.offset_to_point(offset)
     }
@@ -2045,12 +2049,40 @@ impl ToPoint for usize {
     }
 }
 
+impl ToPoint for PointUtf16 {
+    fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
+        snapshot.point_utf16_to_point(*self)
+    }
+}
+
 impl ToPoint for Point {
     fn to_point<'a>(&self, _: &BufferSnapshot) -> Point {
         *self
     }
 }
 
+pub trait Clip {
+    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
+}
+
+impl Clip for usize {
+    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
+        snapshot.clip_offset(*self, bias)
+    }
+}
+
+impl Clip for Point {
+    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
+        snapshot.clip_point(*self, bias)
+    }
+}
+
+impl Clip for PointUtf16 {
+    fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
+        snapshot.clip_point_utf16(*self, bias)
+    }
+}
+
 pub trait FromAnchor {
     fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self;
 }