Merge branch 'main' into v0.173.x

Joseph T. Lyons created

Change summary

assets/icons/file_icons/file_types.json | 15 ++++++
crates/buffer_diff/src/buffer_diff.rs   | 14 ++---
crates/editor/src/actions.rs            |  2 
crates/editor/src/editor.rs             | 59 +++++++++++++++++++++-----
crates/editor/src/editor_tests.rs       | 15 ++++++
crates/editor/src/element.rs            | 21 +++++++-
crates/language/src/buffer.rs           |  4 +
crates/multi_buffer/src/multi_buffer.rs |  2 
crates/project/src/buffer_store.rs      |  9 +--
crates/theme/src/icon_theme.rs          |  1 
crates/zeta/src/zeta.rs                 | 16 +++++--
11 files changed, 121 insertions(+), 37 deletions(-)

Detailed changes

assets/icons/file_icons/file_types.json 🔗

@@ -23,6 +23,7 @@
     "c++": "cpp",
     "cc": "cpp",
     "cjs": "javascript",
+    "cjsx": "react",
     "coffee": "coffeescript",
     "conf": "settings",
     "cpp": "cpp",
@@ -30,6 +31,7 @@
     "csv": "storage",
     "cxx": "cpp",
     "cts": "typescript",
+    "ctsx": "react",
     "dart": "dart",
     "dat": "storage",
     "db": "storage",
@@ -121,6 +123,7 @@
     "metadata": "code",
     "metal": "metal",
     "mjs": "javascript",
+    "mjsx": "react",
     "mka": "audio",
     "mkv": "video",
     "ml": "ocaml",
@@ -130,6 +133,7 @@
     "mp3": "audio",
     "mp4": "video",
     "mts": "typescript",
+    "mtsx": "react",
     "myd": "storage",
     "myi": "storage",
     "nim": "nim",
@@ -186,6 +190,17 @@
     "sh": "terminal",
     "sql": "storage",
     "sqlite": "storage",
+    "stylelint.config.cjs": "stylelint",
+    "stylelint.config.js": "stylelint",
+    "stylelint.config.mjs": "stylelint",
+    "stylelintignore": "stylelint",
+    "stylelintrc": "stylelint",
+    "stylelintrc.cjs": "stylelint",
+    "stylelintrc.js": "stylelint",
+    "stylelintrc.json": "stylelint",
+    "stylelintrc.mjs": "stylelint",
+    "stylelintrc.yaml": "stylelint",
+    "stylelintrc.yml": "stylelint",
     "svelte": "svelte",
     "svg": "image",
     "swift": "swift",

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -587,18 +587,16 @@ impl BufferDiff {
         range: Range<Anchor>,
         buffer: &text::BufferSnapshot,
         cx: &App,
-    ) -> Option<Range<Anchor>> {
+    ) -> Range<Anchor> {
         let start = self
             .hunks_intersecting_range(range.clone(), &buffer, cx)
-            .next()?
-            .buffer_range
-            .start;
+            .next()
+            .map_or(Anchor::MIN, |hunk| hunk.buffer_range.start);
         let end = self
             .hunks_intersecting_range_rev(range.clone(), &buffer)
-            .next()?
-            .buffer_range
-            .end;
-        Some(start..end)
+            .next()
+            .map_or(Anchor::MAX, |hunk| hunk.buffer_range.end);
+        start..end
     }
 
     #[allow(clippy::too_many_arguments)]

crates/editor/src/actions.rs 🔗

@@ -265,6 +265,8 @@ gpui::actions!(
         Copy,
         CopyFileLocation,
         CopyHighlightJson,
+        CopyFileName,
+        CopyFileNameWithoutExtension,
         CopyPath,
         CopyPermalinkToLine,
         CopyRelativePath,

crates/editor/src/editor.rs 🔗

@@ -5457,19 +5457,27 @@ impl Editor {
         };
 
         if &accept_keystroke.modifiers == modifiers {
-            if let Some(completion) = self.active_inline_completion.as_ref() {
-                if self.edit_prediction_preview.start(
-                    &completion.completion,
-                    &position_map.snapshot,
-                    self.selections
-                        .newest_anchor()
-                        .head()
-                        .to_display_point(&position_map.snapshot),
-                ) {
-                    self.request_autoscroll(Autoscroll::fit(), cx);
-                    self.update_visible_inline_completion(window, cx);
-                    cx.notify();
-                }
+            let Some(completion) = self.active_inline_completion.as_ref() else {
+                return;
+            };
+
+            if !self.edit_prediction_requires_modifier() && !self.has_visible_completions_menu() {
+                return;
+            }
+
+            let transitioned = self.edit_prediction_preview.start(
+                &completion.completion,
+                &position_map.snapshot,
+                self.selections
+                    .newest_anchor()
+                    .head()
+                    .to_display_point(&position_map.snapshot),
+            );
+
+            if transitioned {
+                self.request_autoscroll(Autoscroll::fit(), cx);
+                self.update_visible_inline_completion(window, cx);
+                cx.notify();
             }
         } else if self.edit_prediction_preview.end(
             self.selections
@@ -13116,6 +13124,31 @@ impl Editor {
         }
     }
 
+    pub fn copy_file_name_without_extension(
+        &mut self,
+        _: &CopyFileNameWithoutExtension,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(file) = self.target_file(cx) {
+            if let Some(file_stem) = file.path().file_stem() {
+                if let Some(name) = file_stem.to_str() {
+                    cx.write_to_clipboard(ClipboardItem::new_string(name.to_string()));
+                }
+            }
+        }
+    }
+
+    pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
+        if let Some(file) = self.target_file(cx) {
+            if let Some(file_name) = file.path().file_name() {
+                if let Some(name) = file_name.to_str() {
+                    cx.write_to_clipboard(ClipboardItem::new_string(name.to_string()));
+                }
+            }
+        }
+    }
+
     pub fn toggle_git_blame(
         &mut self,
         _: &ToggleGitBlame,

crates/editor/src/editor_tests.rs 🔗

@@ -5362,6 +5362,21 @@ async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
     cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
 }
 
+#[gpui::test]
+async fn test_select_previous_empty_buffer(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("aˇ");
+
+    cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
+        .unwrap();
+    cx.assert_editor_state("«aˇ»");
+    cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
+        .unwrap();
+    cx.assert_editor_state("«aˇ»");
+}
+
 #[gpui::test]
 async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/element.rs 🔗

@@ -408,6 +408,8 @@ impl EditorElement {
         register_action(editor, window, Editor::reveal_in_finder);
         register_action(editor, window, Editor::copy_path);
         register_action(editor, window, Editor::copy_relative_path);
+        register_action(editor, window, Editor::copy_file_name);
+        register_action(editor, window, Editor::copy_file_name_without_extension);
         register_action(editor, window, Editor::copy_highlight_json);
         register_action(editor, window, Editor::copy_permalink_to_line);
         register_action(editor, window, Editor::open_permalink_to_line);
@@ -3628,6 +3630,16 @@ impl EditorElement {
             return None;
         }
 
+        // Adjust text origin for horizontal scrolling (in some cases here)
+        let start_point =
+            text_bounds.origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
+
+        // Clamp left offset after extreme scrollings
+        let clamp_start = |point: gpui::Point<Pixels>| gpui::Point {
+            x: point.x.max(text_bounds.origin.x),
+            y: point.y,
+        };
+
         match &active_inline_completion.completion {
             InlineCompletion::Move { target, .. } => {
                 if editor.edit_prediction_requires_modifier() {
@@ -3684,6 +3696,7 @@ impl EditorElement {
                     )?;
                     let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
                     let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
+
                     element.prepaint_at(text_bounds.origin + offset, window, cx);
                     Some(element)
                 } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
@@ -3699,6 +3712,7 @@ impl EditorElement {
                         (text_bounds.size.width - size.width) / 2.,
                         text_bounds.size.height - size.height - PADDING_Y,
                     );
+
                     element.prepaint_at(text_bounds.origin + offset, window, cx);
                     Some(element)
                 } else {
@@ -3709,7 +3723,6 @@ impl EditorElement {
                         window,
                         cx,
                     )?;
-
                     let target_line_end = DisplayPoint::new(
                         target_display_point.row(),
                         editor_snapshot.line_len(target_display_point.row()),
@@ -3717,8 +3730,9 @@ impl EditorElement {
                     let origin = self.editor.update(cx, |editor, _cx| {
                         editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
                     })?;
+
                     element.prepaint_as_root(
-                        text_bounds.origin + origin + point(PADDING_X, px(0.)),
+                        clamp_start(start_point + origin + point(PADDING_X, px(0.))),
                         AvailableSpace::min_size(),
                         window,
                         cx,
@@ -3778,12 +3792,11 @@ impl EditorElement {
                         })?;
 
                         element.prepaint_as_root(
-                            text_bounds.origin + origin + point(PADDING_X, px(0.)),
+                            clamp_start(start_point + origin + point(PADDING_X, px(0.))),
                             AvailableSpace::min_size(),
                             window,
                             cx,
                         );
-
                         return Some(element);
                     }
                     EditDisplayMode::Inline => return None,

crates/language/src/buffer.rs 🔗

@@ -1102,6 +1102,10 @@ impl Buffer {
         let mut syntax_snapshot = self.syntax_map.lock().snapshot();
         cx.background_executor().spawn(async move {
             if !edits.is_empty() {
+                if let Some(language) = language.clone() {
+                    syntax_snapshot.reparse(&old_snapshot, registry.clone(), language);
+                }
+
                 branch_buffer.edit(edits.iter().cloned());
                 let snapshot = branch_buffer.snapshot();
                 syntax_snapshot.interpolate(&snapshot);

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -7121,7 +7121,7 @@ impl<'a> Iterator for ReversedMultiBufferChunks<'a> {
             self.offset -= 1;
             Some("\n")
         } else {
-            let chunk = self.current_chunks.as_mut().unwrap().next().unwrap();
+            let chunk = self.current_chunks.as_mut().unwrap().next()?;
             self.offset -= chunk.len();
             Some(chunk)
         }

crates/project/src/buffer_store.rs 🔗

@@ -260,15 +260,12 @@ impl BufferDiffState {
                     let changed_range = match (unstaged_changed_range, uncommitted_changed_range) {
                         (None, None) => None,
                         (Some(unstaged_range), None) => {
-                            uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
+                            Some(uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx))
                         }
                         (None, Some(uncommitted_range)) => Some(uncommitted_range),
                         (Some(unstaged_range), Some(uncommitted_range)) => maybe!({
-                            let expanded_range = uncommitted_diff.range_to_hunk_range(
-                                unstaged_range,
-                                &buffer,
-                                cx,
-                            )?;
+                            let expanded_range =
+                                uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx);
                             let start = expanded_range.start.min(&uncommitted_range.start, &buffer);
                             let end = expanded_range.end.max(&uncommitted_range.end, &buffer);
                             Some(start..end)

crates/theme/src/icon_theme.rs 🔗

@@ -113,6 +113,7 @@ const FILE_ICONS: &[(&str, &str)] = &[
     ("scala", "icons/file_icons/scala.svg"),
     ("settings", "icons/file_icons/settings.svg"),
     ("storage", "icons/file_icons/database.svg"),
+    ("stylelint", "icons/file_icons/javascript.svg"),
     ("svelte", "icons/file_icons/html.svg"),
     ("swift", "icons/file_icons/swift.svg"),
     ("tcl", "icons/file_icons/tcl.svg"),

crates/zeta/src/zeta.rs 🔗

@@ -664,11 +664,17 @@ and then another
             let mut did_retry = false;
 
             loop {
-                let request_builder = http_client::Request::builder().method(Method::POST).uri(
-                    http_client
-                        .build_zed_llm_url("/predict_edits/v2", &[])?
-                        .as_ref(),
-                );
+                let request_builder = http_client::Request::builder().method(Method::POST);
+                let request_builder =
+                    if let Ok(predict_edits_url) = std::env::var("ZED_PREDICT_EDITS_URL") {
+                        request_builder.uri(predict_edits_url)
+                    } else {
+                        request_builder.uri(
+                            http_client
+                                .build_zed_llm_url("/predict_edits/v2", &[])?
+                                .as_ref(),
+                        )
+                    };
                 let request = request_builder
                     .header("Content-Type", "application/json")
                     .header("Authorization", format!("Bearer {}", token))