Merge branch 'main' into disconnected-status

Nathan Sobo created

Change summary

crates/editor/src/editor.rs                                      |  12 
crates/lsp/src/lsp.rs                                            |   2 
crates/project/src/project.rs                                    | 104 +
crates/text/src/rope.rs                                          |   4 
crates/text/src/text.rs                                          |   4 
crates/zed/assets/fonts/zed-mono/zed-mono-bold.ttf               |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-bolditalic.ttf         |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf           |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf       |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf     |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-italic.ttf             |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-regular.ttf            |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-bold.ttf               |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-bolditalic.ttf         |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf           |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf       |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf     |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-italic.ttf             |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-regular.ttf            |   0 
crates/zed/src/zed.rs                                            |   2 
22 files changed, 115 insertions(+), 13 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -940,9 +940,15 @@ impl Editor {
         _: &workspace::OpenNew,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let buffer = cx
-            .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx));
-        workspace.open_item(BufferItemHandle(buffer), cx);
+        let project = workspace.project();
+        if project.read(cx).is_remote() {
+            cx.propagate_action();
+        } else if let Some(buffer) = project
+            .update(cx, |project, cx| project.create_buffer(cx))
+            .log_err()
+        {
+            workspace.open_item(BufferItemHandle(buffer), cx);
+        }
     }
 
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {

crates/lsp/src/lsp.rs 🔗

@@ -524,6 +524,8 @@ impl LanguageServer {
         ServerCapabilities {
             document_highlight_provider: Some(OneOf::Left(true)),
             code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
+            document_formatting_provider: Some(OneOf::Left(true)),
+            document_range_formatting_provider: Some(OneOf::Left(true)),
             ..Default::default()
         }
     }

crates/project/src/project.rs 🔗

@@ -715,6 +715,18 @@ impl Project {
         !self.is_local()
     }
 
+    pub fn create_buffer(&mut self, cx: &mut ModelContext<Self>) -> Result<ModelHandle<Buffer>> {
+        if self.is_remote() {
+            return Err(anyhow!("creating buffers as a guest is not supported yet"));
+        }
+
+        let buffer = cx.add_model(|cx| {
+            Buffer::new(self.replica_id(), "", cx).with_language(language::PLAIN_TEXT.clone(), cx)
+        });
+        self.register_buffer(&buffer, None, cx)?;
+        Ok(buffer)
+    }
+
     pub fn open_buffer(
         &mut self,
         path: impl Into<ProjectPath>,
@@ -1342,15 +1354,47 @@ impl Project {
             }
 
             for (buffer, buffer_abs_path, lang_server) in local_buffers {
-                let lsp_edits = lang_server
-                    .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
-                        text_document: lsp::TextDocumentIdentifier::new(
-                            lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
-                        ),
-                        options: Default::default(),
-                        work_done_progress_params: Default::default(),
-                    })
-                    .await?;
+                let capabilities = if let Some(capabilities) = lang_server.capabilities().await {
+                    capabilities
+                } else {
+                    continue;
+                };
+
+                let text_document = lsp::TextDocumentIdentifier::new(
+                    lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
+                );
+                let lsp_edits = if capabilities
+                    .document_formatting_provider
+                    .map_or(false, |provider| provider != lsp::OneOf::Left(false))
+                {
+                    lang_server
+                        .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
+                            text_document,
+                            options: Default::default(),
+                            work_done_progress_params: Default::default(),
+                        })
+                        .await?
+                } else if capabilities
+                    .document_range_formatting_provider
+                    .map_or(false, |provider| provider != lsp::OneOf::Left(false))
+                {
+                    let buffer_start = lsp::Position::new(0, 0);
+                    let buffer_end = buffer
+                        .read_with(&cx, |buffer, _| buffer.max_point_utf16())
+                        .to_lsp_position();
+                    lang_server
+                        .request::<lsp::request::RangeFormatting>(
+                            lsp::DocumentRangeFormattingParams {
+                                text_document,
+                                range: lsp::Range::new(buffer_start, buffer_end),
+                                options: Default::default(),
+                                work_done_progress_params: Default::default(),
+                            },
+                        )
+                        .await?
+                } else {
+                    continue;
+                };
 
                 if let Some(lsp_edits) = lsp_edits {
                     let edits = buffer
@@ -4128,6 +4172,48 @@ mod tests {
         assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
     }
 
+    #[gpui::test]
+    async fn test_save_as(cx: &mut gpui::TestAppContext) {
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree("/dir", json!({})).await;
+
+        let project = Project::test(fs.clone(), cx);
+        let (worktree, _) = project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/dir", true, cx)
+            })
+            .await
+            .unwrap();
+        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+
+        let buffer = project.update(cx, |project, cx| project.create_buffer(cx).unwrap());
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([0..0], "abc", cx);
+            assert!(buffer.is_dirty());
+            assert!(!buffer.has_conflict());
+        });
+        project
+            .update(cx, |project, cx| {
+                project.save_buffer_as(buffer.clone(), "/dir/file1".into(), cx)
+            })
+            .await
+            .unwrap();
+        assert_eq!(fs.load(Path::new("/dir/file1")).await.unwrap(), "abc");
+        buffer.read_with(cx, |buffer, cx| {
+            assert_eq!(buffer.file().unwrap().full_path(cx), Path::new("dir/file1"));
+            assert!(!buffer.is_dirty());
+            assert!(!buffer.has_conflict());
+        });
+
+        let opened_buffer = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "file1"), cx)
+            })
+            .await
+            .unwrap();
+        assert_eq!(opened_buffer, buffer);
+    }
+
     #[gpui::test(retries = 5)]
     async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
         let dir = temp_tree(json!({

crates/text/src/rope.rs 🔗

@@ -126,6 +126,10 @@ impl Rope {
         self.chunks.extent(&())
     }
 
+    pub fn max_point_utf16(&self) -> PointUtf16 {
+        self.chunks.extent(&())
+    }
+
     pub fn cursor(&self, offset: usize) -> Cursor {
         Cursor::new(self, offset)
     }

crates/text/src/text.rs 🔗

@@ -1526,6 +1526,10 @@ impl BufferSnapshot {
         self.visible_text.max_point()
     }
 
+    pub fn max_point_utf16(&self) -> PointUtf16 {
+        self.visible_text.max_point_utf16()
+    }
+
     pub fn point_to_offset(&self, point: Point) -> usize {
         self.visible_text.point_to_offset(point)
     }

crates/zed/src/zed.rs 🔗

@@ -849,7 +849,7 @@ mod tests {
         lazy_static::lazy_static! {
             static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
             static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
-                Assets.load("fonts/zed-sans/zed-sans-regular.ttf").unwrap().to_vec().into()
+                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into()
             ];
         }