Implement `Buffer::format`

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/client/src/client.rs       |   8 ++
crates/editor/src/items.rs        |  17 +++-
crates/editor/src/multi_buffer.rs |  14 +++
crates/language/src/buffer.rs     |  67 ++++++++++++++++
crates/language/src/language.rs   |  18 ++++
crates/lsp/src/lsp.rs             |  30 ++++--
crates/project/src/project.rs     |  16 ++++
crates/project/src/worktree.rs    |  87 +++++++++++++++++----
crates/rpc/proto/zed.proto        |  39 +++++----
crates/rpc/src/peer.rs            |   8 +-
crates/rpc/src/proto.rs           |   3 
crates/server/src/rpc.rs          | 130 +++++++++++++++++++++++++++++++++
crates/text/src/rope.rs           |   6 +
13 files changed, 387 insertions(+), 56 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -691,6 +691,14 @@ impl Client {
     ) -> impl Future<Output = Result<()>> {
         self.peer.respond(receipt, response)
     }
+
+    pub fn respond_with_error<T: RequestMessage>(
+        &self,
+        receipt: Receipt<T>,
+        error: proto::Error,
+    ) -> impl Future<Output = Result<()>> {
+        self.peer.respond_with_error(receipt, error)
+    }
 }
 
 fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {

crates/editor/src/items.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Editor, Event};
+use crate::{Autoscroll, Editor, Event};
 use crate::{MultiBuffer, ToPoint as _};
 use anyhow::Result;
 use gpui::{
@@ -11,6 +11,7 @@ use project::{File, ProjectPath, Worktree};
 use std::fmt::Write;
 use std::path::Path;
 use text::{Point, Selection};
+use util::TryFutureExt;
 use workspace::{
     ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle,
     Workspace,
@@ -141,9 +142,17 @@ impl ItemView for Editor {
     }
 
     fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
-        let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
-        Ok(cx.spawn(|_, _| async move {
-            save.await?;
+        let buffer = self.buffer().clone();
+        Ok(cx.spawn(|editor, mut cx| async move {
+            buffer
+                .update(&mut cx, |buffer, cx| buffer.format(cx).log_err())
+                .await;
+            editor.update(&mut cx, |editor, cx| {
+                editor.request_autoscroll(Autoscroll::Fit, cx)
+            });
+            buffer
+                .update(&mut cx, |buffer, cx| buffer.save(cx))?
+                .await?;
             Ok(())
         }))
     }

crates/editor/src/multi_buffer.rs 🔗

@@ -798,6 +798,20 @@ impl MultiBuffer {
         cx.emit(event.clone());
     }
 
+    pub fn format(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let mut format_tasks = Vec::new();
+        for BufferState { buffer, .. } in self.buffers.borrow().values() {
+            format_tasks.push(buffer.update(cx, |buffer, cx| buffer.format(cx)));
+        }
+
+        cx.spawn(|_, _| async move {
+            for format in format_tasks {
+                format.await?;
+            }
+            Ok(())
+        })
+    }
+
     pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
         let mut save_tasks = Vec::new();
         for BufferState { buffer, .. } in self.buffers.borrow().values() {

crates/language/src/buffer.rs 🔗

@@ -1,10 +1,13 @@
-use crate::diagnostic_set::{DiagnosticEntry, DiagnosticGroup};
 pub use crate::{
     diagnostic_set::DiagnosticSet,
     highlight_map::{HighlightId, HighlightMap},
     proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig,
     PLAIN_TEXT,
 };
+use crate::{
+    diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
+    range_from_lsp, ToPointUtf16,
+};
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use futures::FutureExt as _;
@@ -180,6 +183,9 @@ pub trait File {
 
     fn load_local(&self, cx: &AppContext) -> Option<Task<Result<String>>>;
 
+    fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
+        -> Option<Task<Result<()>>>;
+
     fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext);
 
     fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext);
@@ -437,6 +443,65 @@ impl Buffer {
         self.file.as_deref()
     }
 
+    pub fn format(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let file = if let Some(file) = self.file.as_ref() {
+            file
+        } else {
+            return Task::ready(Err(anyhow!("buffer has no file")));
+        };
+
+        if let Some(LanguageServerState { server, .. }) = self.language_server.as_ref() {
+            let server = server.clone();
+            let abs_path = file.abs_path().unwrap();
+            let version = self.version();
+            cx.spawn(|this, mut cx| async move {
+                let edits = server
+                    .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
+                        text_document: lsp::TextDocumentIdentifier::new(
+                            lsp::Url::from_file_path(&abs_path).unwrap(),
+                        ),
+                        options: Default::default(),
+                        work_done_progress_params: Default::default(),
+                    })
+                    .await?;
+
+                if let Some(edits) = edits {
+                    this.update(&mut cx, |this, cx| {
+                        if this.version == version {
+                            for edit in &edits {
+                                let range = range_from_lsp(edit.range);
+                                if this.clip_point_utf16(range.start, Bias::Left) != range.start
+                                    || this.clip_point_utf16(range.end, Bias::Left) != range.end
+                                {
+                                    return Err(anyhow!(
+                                        "invalid formatting edits received from language server"
+                                    ));
+                                }
+                            }
+
+                            for edit in edits.into_iter().rev() {
+                                this.edit([range_from_lsp(edit.range)], edit.new_text, cx);
+                            }
+                            Ok(())
+                        } else {
+                            Err(anyhow!("buffer edited since starting to format"))
+                        }
+                    })
+                } else {
+                    Ok(())
+                }
+            })
+        } else {
+            let format = file.format_remote(self.remote_id(), cx.as_mut());
+            cx.spawn(|_, _| async move {
+                if let Some(format) = format {
+                    format.await?;
+                }
+                Ok(())
+            })
+        }
+    }
+
     pub fn save(
         &mut self,
         cx: &mut ModelContext<Self>,

crates/language/src/language.rs 🔗

@@ -15,7 +15,7 @@ use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Deserialize;
-use std::{path::Path, str, sync::Arc};
+use std::{ops::Range, path::Path, str, sync::Arc};
 use theme::SyntaxTheme;
 use tree_sitter::{self, Query};
 pub use tree_sitter::{Parser, Tree};
@@ -33,6 +33,10 @@ lazy_static! {
     ));
 }
 
+pub trait ToPointUtf16 {
+    fn to_point_utf16(self) -> PointUtf16;
+}
+
 #[derive(Default, Deserialize)]
 pub struct LanguageConfig {
     pub name: String,
@@ -244,3 +248,15 @@ impl LanguageServerConfig {
         )
     }
 }
+
+impl ToPointUtf16 for lsp::Position {
+    fn to_point_utf16(self) -> PointUtf16 {
+        PointUtf16::new(self.line, self.character)
+    }
+}
+
+pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
+    let start = PointUtf16::new(range.start.line, range.start.character);
+    let end = PointUtf16::new(range.end.line, range.end.character);
+    start..end
+}

crates/lsp/src/lsp.rs 🔗

@@ -494,17 +494,25 @@ impl FakeLanguageServer {
     }
 
     pub async fn receive_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
-        self.receive().await;
-        let request = serde_json::from_slice::<Request<T::Params>>(&self.buffer).unwrap();
-        assert_eq!(request.method, T::METHOD);
-        assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
-        (
-            RequestId {
-                id: request.id,
-                _type: std::marker::PhantomData,
-            },
-            request.params,
-        )
+        loop {
+            self.receive().await;
+            if let Ok(request) = serde_json::from_slice::<Request<T::Params>>(&self.buffer) {
+                assert_eq!(request.method, T::METHOD);
+                assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
+                return (
+                    RequestId {
+                        id: request.id,
+                        _type: std::marker::PhantomData,
+                    },
+                    request.params,
+                );
+            } else {
+                println!(
+                    "skipping message in fake language server {:?}",
+                    std::str::from_utf8(&self.buffer)
+                );
+            }
+        }
     }
 
     pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {

crates/project/src/project.rs 🔗

@@ -308,6 +308,7 @@ impl Project {
                 client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
                 client.subscribe_to_entity(remote_id, cx, Self::handle_save_buffer),
                 client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
+                client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffer),
             ]);
         }
     }
@@ -808,6 +809,21 @@ impl Project {
         Ok(())
     }
 
+    pub fn handle_format_buffer(
+        &mut self,
+        envelope: TypedEnvelope<proto::FormatBuffer>,
+        rpc: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
+            worktree.update(cx, |worktree, cx| {
+                worktree.handle_format_buffer(envelope, rpc, cx)
+            })?;
+        }
+        Ok(())
+    }
+
     pub fn handle_open_buffer(
         &mut self,
         envelope: TypedEnvelope<proto::OpenBuffer>,

crates/project/src/worktree.rs 🔗

@@ -15,8 +15,8 @@ use gpui::{
     Task, UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
-    Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language, LanguageRegistry,
-    Operation, PointUtf16, Rope,
+    range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language,
+    LanguageRegistry, Operation, PointUtf16, Rope,
 };
 use lazy_static::lazy_static;
 use lsp::LanguageServer;
@@ -34,7 +34,7 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     future::Future,
-    ops::{Deref, Range},
+    ops::Deref,
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
@@ -580,6 +580,49 @@ impl Worktree {
         Ok(())
     }
 
+    pub fn handle_format_buffer(
+        &mut self,
+        envelope: TypedEnvelope<proto::FormatBuffer>,
+        rpc: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let sender_id = envelope.original_sender_id()?;
+        let this = self.as_local().unwrap();
+        let buffer = this
+            .shared_buffers
+            .get(&sender_id)
+            .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned())
+            .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+
+        let receipt = envelope.receipt();
+        cx.spawn(|_, mut cx| async move {
+            let format = buffer.update(&mut cx, |buffer, cx| buffer.format(cx)).await;
+            // We spawn here in order to enqueue the sending of `Ack` *after* transmission of edits
+            // associated with formatting.
+            cx.spawn(|_| async move {
+                dbg!("responding");
+                match format {
+                    Ok(()) => rpc.respond(receipt, proto::Ack {}).await?,
+                    Err(error) => {
+                        rpc.respond_with_error(
+                            receipt,
+                            proto::Error {
+                                message: error.to_string(),
+                            },
+                        )
+                        .await?
+                    }
+                }
+                Ok::<_, anyhow::Error>(())
+            })
+            .await
+            .log_err();
+        })
+        .detach();
+
+        Ok(())
+    }
+
     fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
         match self {
             Self::Local(worktree) => {
@@ -880,6 +923,7 @@ impl Worktree {
             )),
         } {
             cx.spawn(|worktree, mut cx| async move {
+                dbg!(&operation);
                 if let Err(error) = rpc
                     .request(proto::UpdateBuffer {
                         project_id,
@@ -2259,6 +2303,27 @@ impl language::File for File {
         )
     }
 
+    fn format_remote(
+        &self,
+        buffer_id: u64,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<()>>> {
+        let worktree = self.worktree.read(cx);
+        let worktree_id = worktree.id().to_proto();
+        let worktree = worktree.as_remote()?;
+        let rpc = worktree.client.clone();
+        let project_id = worktree.project_id;
+        Some(cx.foreground().spawn(async move {
+            rpc.request(proto::FormatBuffer {
+                project_id,
+                worktree_id,
+                buffer_id,
+            })
+            .await?;
+            Ok(())
+        }))
+    }
+
     fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
         self.worktree.update(cx, |worktree, cx| {
             worktree.send_buffer_update(buffer_id, operation, cx);
@@ -3180,22 +3245,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
     }
 }
 
-trait ToPointUtf16 {
-    fn to_point_utf16(self) -> PointUtf16;
-}
-
-impl ToPointUtf16 for lsp::Position {
-    fn to_point_utf16(self) -> PointUtf16 {
-        PointUtf16::new(self.line, self.character)
-    }
-}
-
-fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
-    let start = PointUtf16::new(range.start.line, range.start.character);
-    let end = PointUtf16::new(range.end.line, range.end.character);
-    start..end
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/rpc/proto/zed.proto 🔗

@@ -35,22 +35,23 @@ message Envelope {
         UpdateBuffer update_buffer = 27;
         SaveBuffer save_buffer = 28;
         BufferSaved buffer_saved = 29;
-
-        GetChannels get_channels = 30;
-        GetChannelsResponse get_channels_response = 31;
-        JoinChannel join_channel = 32;
-        JoinChannelResponse join_channel_response = 33;
-        LeaveChannel leave_channel = 34;
-        SendChannelMessage send_channel_message = 35;
-        SendChannelMessageResponse send_channel_message_response = 36;
-        ChannelMessageSent channel_message_sent = 37;
-        GetChannelMessages get_channel_messages = 38;
-        GetChannelMessagesResponse get_channel_messages_response = 39;
-
-        UpdateContacts update_contacts = 40;
-
-        GetUsers get_users = 41;
-        GetUsersResponse get_users_response = 42;
+        FormatBuffer format_buffer = 30;
+
+        GetChannels get_channels = 31;
+        GetChannelsResponse get_channels_response = 32;
+        JoinChannel join_channel = 33;
+        JoinChannelResponse join_channel_response = 34;
+        LeaveChannel leave_channel = 35;
+        SendChannelMessage send_channel_message = 36;
+        SendChannelMessageResponse send_channel_message_response = 37;
+        ChannelMessageSent channel_message_sent = 38;
+        GetChannelMessages get_channel_messages = 39;
+        GetChannelMessagesResponse get_channel_messages_response = 40;
+
+        UpdateContacts update_contacts = 41;
+
+        GetUsers get_users = 42;
+        GetUsersResponse get_users_response = 43;
     }
 }
 
@@ -168,6 +169,12 @@ message BufferSaved {
     Timestamp mtime = 5;
 }
 
+message FormatBuffer {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    uint64 buffer_id = 3;
+}
+
 message UpdateDiagnosticSummary {
     uint64 project_id = 1;
     uint64 worktree_id = 2;

crates/rpc/src/peer.rs 🔗

@@ -398,7 +398,7 @@ mod tests {
                 proto::OpenBufferResponse {
                     buffer: Some(proto::Buffer {
                         id: 101,
-                        visible_text: "path/one content".to_string(),
+                        content: "path/one content".to_string(),
                         ..Default::default()
                     }),
                 }
@@ -419,7 +419,7 @@ mod tests {
                 proto::OpenBufferResponse {
                     buffer: Some(proto::Buffer {
                         id: 102,
-                        visible_text: "path/two content".to_string(),
+                        content: "path/two content".to_string(),
                         ..Default::default()
                     }),
                 }
@@ -448,7 +448,7 @@ mod tests {
                                 proto::OpenBufferResponse {
                                     buffer: Some(proto::Buffer {
                                         id: 101,
-                                        visible_text: "path/one content".to_string(),
+                                        content: "path/one content".to_string(),
                                         ..Default::default()
                                     }),
                                 }
@@ -458,7 +458,7 @@ mod tests {
                                 proto::OpenBufferResponse {
                                     buffer: Some(proto::Buffer {
                                         id: 102,
-                                        visible_text: "path/two content".to_string(),
+                                        content: "path/two content".to_string(),
                                         ..Default::default()
                                     }),
                                 }

crates/rpc/src/proto.rs 🔗

@@ -128,6 +128,7 @@ messages!(
     DiskBasedDiagnosticsUpdated,
     DiskBasedDiagnosticsUpdating,
     Error,
+    FormatBuffer,
     GetChannelMessages,
     GetChannelMessagesResponse,
     GetChannels,
@@ -162,6 +163,7 @@ messages!(
 );
 
 request_messages!(
+    (FormatBuffer, Ack),
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannels, GetChannelsResponse),
     (GetUsers, GetUsersResponse),
@@ -185,6 +187,7 @@ entity_messages!(
     CloseBuffer,
     DiskBasedDiagnosticsUpdated,
     DiskBasedDiagnosticsUpdating,
+    FormatBuffer,
     JoinProject,
     LeaveProject,
     OpenBuffer,

crates/server/src/rpc.rs 🔗

@@ -79,6 +79,7 @@ impl Server {
             .add_handler(Server::update_buffer)
             .add_handler(Server::buffer_saved)
             .add_handler(Server::save_buffer)
+            .add_handler(Server::format_buffer)
             .add_handler(Server::get_channels)
             .add_handler(Server::get_users)
             .add_handler(Server::join_channel)
@@ -660,6 +661,30 @@ impl Server {
         Ok(())
     }
 
+    async fn format_buffer(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::FormatBuffer>,
+    ) -> tide::Result<()> {
+        let host;
+        {
+            let state = self.state();
+            let project = state
+                .read_project(request.payload.project_id, request.sender_id)
+                .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+            host = project.host_connection_id;
+        }
+
+        let sender = request.sender_id;
+        let receipt = request.receipt();
+        let response = self
+            .peer
+            .forward_request(sender, host, request.payload.clone())
+            .await?;
+        self.peer.respond(receipt, response).await?;
+
+        Ok(())
+    }
+
     async fn update_buffer(
         self: Arc<Server>,
         request: TypedEnvelope<proto::UpdateBuffer>,
@@ -2001,6 +2026,111 @@ mod tests {
         });
     }
 
+    #[gpui::test(iterations = 1, seed = 2)]
+    async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let mut lang_registry = Arc::new(LanguageRegistry::new());
+        let fs = Arc::new(FakeFs::new());
+
+        // Set up a fake language server.
+        let (language_server_config, mut fake_language_server) =
+            LanguageServerConfig::fake(cx_a.background()).await;
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(Arc::new(Language::new(
+                LanguageConfig {
+                    name: "Rust".to_string(),
+                    path_suffixes: vec!["rs".to_string()],
+                    language_server: Some(language_server_config),
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )));
+
+        // Connect to a server as 2 clients.
+        let mut server = TestServer::start(cx_a.foreground()).await;
+        let client_a = server.create_client(&mut cx_a, "user_a").await;
+        let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+        // Share a project as client A
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "a.rs": "let one = two",
+            }),
+        )
+        .await;
+        let project_a = cx_a.update(|cx| {
+            Project::local(
+                client_a.clone(),
+                client_a.user_store.clone(),
+                lang_registry.clone(),
+                fs.clone(),
+                cx,
+            )
+        });
+        let worktree_a = project_a
+            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+            .await
+            .unwrap();
+        worktree_a
+            .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        let project_id = project_a
+            .update(&mut cx_a, |project, _| project.next_remote_id())
+            .await;
+        project_a
+            .update(&mut cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Join the worktree as client B.
+        let project_b = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+
+        // Open the file to be formatted on client B.
+        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let buffer_b = cx_b
+            .background()
+            .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
+            .await
+            .unwrap();
+
+        let format = buffer_b.update(&mut cx_b, |buffer, cx| buffer.format(cx));
+        let (request_id, _) = fake_language_server
+            .receive_request::<lsp::request::Formatting>()
+            .await;
+        fake_language_server
+            .respond(
+                request_id,
+                Some(vec![
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
+                        new_text: "h".to_string(),
+                    },
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
+                        new_text: "y".to_string(),
+                    },
+                ]),
+            )
+            .await;
+        format.await.unwrap();
+        assert_eq!(
+            buffer_b.read_with(&cx_b, |buffer, _| buffer.text()),
+            "let honey = two"
+        );
+    }
+
     #[gpui::test]
     async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();

crates/text/src/rope.rs 🔗

@@ -593,6 +593,12 @@ impl Chunk {
 
             if ch == '\n' {
                 point.row += 1;
+                if point.row > target.row {
+                    panic!(
+                        "point {:?} is beyond the end of a line with length {}",
+                        target, point.column
+                    );
+                }
                 point.column = 0;
             } else {
                 point.column += ch.len_utf16() as u32;