Merge branch 'main' into copilot

Mikayla Maki created

Change summary

Cargo.lock                                                   |  1 
crates/copilot/src/copilot.rs                                | 10 
crates/editor/src/display_map/tab_map.rs                     | 54 ++---
crates/gpui/src/app.rs                                       | 17 +
crates/gpui/src/platform.rs                                  |  4 
crates/gpui/src/platform/mac/platform.rs                     | 37 +++-
crates/gpui/src/platform/test.rs                             |  5 
crates/language/src/language.rs                              | 16 +
crates/lsp/src/lsp.rs                                        | 11 +
crates/project/src/project.rs                                | 14 -
crates/recent_projects/Cargo.toml                            |  1 
crates/recent_projects/src/highlighted_workspace_location.rs |  1 
crates/util/Cargo.toml                                       |  2 
crates/util/src/paths.rs                                     | 48 +++++
crates/util/src/test/marked_text.rs                          |  6 
crates/zed/src/languages/typescript.rs                       | 10 +
crates/zed/src/main.rs                                       |  3 
17 files changed, 177 insertions(+), 63 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -5074,6 +5074,7 @@ dependencies = [
  "settings",
  "smol",
  "text",
+ "util",
  "workspace",
 ]
 

crates/copilot/src/copilot.rs ๐Ÿ”—

@@ -225,8 +225,14 @@ impl Copilot {
                 let server_path = get_copilot_lsp(http).await?;
                 let node_path = node_runtime.binary_path().await?;
                 let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
-                let server =
-                    LanguageServer::new(0, &node_path, arguments, Path::new("/"), cx.clone())?;
+                let server = LanguageServer::new(
+                    0,
+                    &node_path,
+                    arguments,
+                    Path::new("/"),
+                    None,
+                    cx.clone(),
+                )?;
 
                 let server = server.initialize(Default::default()).await?;
                 let status = server

crates/editor/src/display_map/tab_map.rs ๐Ÿ”—

@@ -48,7 +48,6 @@ impl TabMap {
             new_snapshot.version += 1;
         }
 
-        let old_max_offset = old_snapshot.suggestion_snapshot.len();
         let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
 
         if old_snapshot.tab_size == new_snapshot.tab_size {
@@ -56,50 +55,47 @@ impl TabMap {
             // and any subsequent tabs on that line that moved across the tab expansion
             // boundary.
             for suggestion_edit in &mut suggestion_edits {
-                let old_end_column = old_snapshot
+                let old_end = old_snapshot
                     .suggestion_snapshot
-                    .to_point(suggestion_edit.old.end)
-                    .column();
-                let new_end_column = new_snapshot
+                    .to_point(suggestion_edit.old.end);
+                let old_end_row_successor_offset =
+                    old_snapshot.suggestion_snapshot.to_offset(cmp::min(
+                        SuggestionPoint::new(old_end.row() + 1, 0),
+                        old_snapshot.suggestion_snapshot.max_point(),
+                    ));
+                let new_end = new_snapshot
                     .suggestion_snapshot
-                    .to_point(suggestion_edit.new.end)
-                    .column();
+                    .to_point(suggestion_edit.new.end);
 
                 let mut offset_from_edit = 0;
                 let mut first_tab_offset = None;
                 let mut last_tab_with_changed_expansion_offset = None;
                 'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
-                    suggestion_edit.old.end..old_max_offset,
+                    suggestion_edit.old.end..old_end_row_successor_offset,
                     false,
                     None,
                     None,
                 ) {
-                    for (ix, mat) in chunk.text.match_indices(&['\t', '\n']) {
+                    for (ix, _) in chunk.text.match_indices('\t') {
                         let offset_from_edit = offset_from_edit + (ix as u32);
-                        match mat {
-                            "\t" => {
-                                if first_tab_offset.is_none() {
-                                    first_tab_offset = Some(offset_from_edit);
-                                }
-
-                                let old_column = old_end_column + offset_from_edit;
-                                let new_column = new_end_column + offset_from_edit;
-                                let was_expanded = old_column < old_snapshot.max_expansion_column;
-                                let is_expanded = new_column < new_snapshot.max_expansion_column;
-                                if was_expanded != is_expanded {
-                                    last_tab_with_changed_expansion_offset = Some(offset_from_edit);
-                                } else if !was_expanded && !is_expanded {
-                                    break 'outer;
-                                }
-                            }
-                            "\n" => break 'outer,
-                            _ => unreachable!(),
+                        if first_tab_offset.is_none() {
+                            first_tab_offset = Some(offset_from_edit);
+                        }
+
+                        let old_column = old_end.column() + offset_from_edit;
+                        let new_column = new_end.column() + offset_from_edit;
+                        let was_expanded = old_column < old_snapshot.max_expansion_column;
+                        let is_expanded = new_column < new_snapshot.max_expansion_column;
+                        if was_expanded != is_expanded {
+                            last_tab_with_changed_expansion_offset = Some(offset_from_edit);
+                        } else if !was_expanded && !is_expanded {
+                            break 'outer;
                         }
                     }
 
                     offset_from_edit += chunk.text.len() as u32;
-                    if old_end_column + offset_from_edit >= old_snapshot.max_expansion_column
-                        && new_end_column | offset_from_edit >= new_snapshot.max_expansion_column
+                    if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
+                        && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
                     {
                         break;
                     }

crates/gpui/src/app.rs ๐Ÿ”—

@@ -254,6 +254,19 @@ impl App {
         self
     }
 
+    /// Handle the application being re-activated when no windows are open.
+    pub fn on_reopen<F>(&mut self, mut callback: F) -> &mut Self
+    where
+        F: 'static + FnMut(&mut MutableAppContext),
+    {
+        let cx = self.0.clone();
+        self.0
+            .borrow_mut()
+            .foreground_platform
+            .on_reopen(Box::new(move || callback(&mut *cx.borrow_mut())));
+        self
+    }
+
     pub fn on_event<F>(&mut self, mut callback: F) -> &mut Self
     where
         F: 'static + FnMut(Event, &mut MutableAppContext) -> bool,
@@ -276,9 +289,7 @@ impl App {
         self.0
             .borrow_mut()
             .foreground_platform
-            .on_open_urls(Box::new(move |paths| {
-                callback(paths, &mut *cx.borrow_mut())
-            }));
+            .on_open_urls(Box::new(move |urls| callback(urls, &mut *cx.borrow_mut())));
         self
     }
 

crates/gpui/src/platform.rs ๐Ÿ”—

@@ -90,6 +90,10 @@ pub(crate) trait ForegroundPlatform {
     fn on_become_active(&self, callback: Box<dyn FnMut()>);
     fn on_resign_active(&self, callback: Box<dyn FnMut()>);
     fn on_quit(&self, callback: Box<dyn FnMut()>);
+
+    /// Handle the application being re-activated with no windows open.
+    fn on_reopen(&self, callback: Box<dyn FnMut()>);
+
     fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
     fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
     fn run(&self, on_finish_launching: Box<dyn FnOnce()>);

crates/gpui/src/platform/mac/platform.rs ๐Ÿ”—

@@ -82,6 +82,10 @@ unsafe fn build_classes() {
             sel!(applicationDidFinishLaunching:),
             did_finish_launching as extern "C" fn(&mut Object, Sel, id),
         );
+        decl.add_method(
+            sel!(applicationShouldHandleReopen:hasVisibleWindows:),
+            should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool),
+        );
         decl.add_method(
             sel!(applicationDidBecomeActive:),
             did_become_active as extern "C" fn(&mut Object, Sel, id),
@@ -144,6 +148,7 @@ pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
 pub struct MacForegroundPlatformState {
     become_active: Option<Box<dyn FnMut()>>,
     resign_active: Option<Box<dyn FnMut()>>,
+    reopen: Option<Box<dyn FnMut()>>,
     quit: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
     menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
@@ -158,15 +163,16 @@ pub struct MacForegroundPlatformState {
 impl MacForegroundPlatform {
     pub fn new(foreground: Rc<executor::Foreground>) -> Self {
         Self(RefCell::new(MacForegroundPlatformState {
-            become_active: Default::default(),
-            resign_active: Default::default(),
-            quit: Default::default(),
-            event: Default::default(),
-            menu_command: Default::default(),
-            validate_menu_command: Default::default(),
-            will_open_menu: Default::default(),
-            open_urls: Default::default(),
-            finish_launching: Default::default(),
+            become_active: None,
+            resign_active: None,
+            reopen: None,
+            quit: None,
+            event: None,
+            menu_command: None,
+            validate_menu_command: None,
+            will_open_menu: None,
+            open_urls: None,
+            finish_launching: None,
             menu_actions: Default::default(),
             foreground,
         }))
@@ -332,6 +338,10 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().quit = Some(callback);
     }
 
+    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().reopen = Some(callback);
+    }
+
     fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
         self.0.borrow_mut().event = Some(callback);
     }
@@ -943,6 +953,15 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
     }
 }
 
+extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
+    if !has_open_windows {
+        let platform = unsafe { get_foreground_platform(this) };
+        if let Some(callback) = platform.0.borrow_mut().reopen.as_mut() {
+            callback();
+        }
+    }
+}
+
 extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
     let platform = unsafe { get_foreground_platform(this) };
     if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() {

crates/gpui/src/platform/test.rs ๐Ÿ”—

@@ -61,13 +61,10 @@ impl ForegroundPlatform {
 
 impl super::ForegroundPlatform for ForegroundPlatform {
     fn on_become_active(&self, _: Box<dyn FnMut()>) {}
-
     fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
-
     fn on_quit(&self, _: Box<dyn FnMut()>) {}
-
+    fn on_reopen(&self, _: Box<dyn FnMut()>) {}
     fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
-
     fn on_open_urls(&self, _: Box<dyn FnMut(Vec<String>)>) {}
 
     fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {

crates/language/src/language.rs ๐Ÿ”—

@@ -19,6 +19,7 @@ use futures::{
 use gpui::{executor::Background, MutableAppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
+use lsp::CodeActionKind;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use regex::Regex;
@@ -140,6 +141,10 @@ impl CachedLspAdapter {
         self.adapter.cached_server_binary(container_dir).await
     }
 
+    pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        self.adapter.code_action_kinds()
+    }
+
     pub fn workspace_configuration(
         &self,
         cx: &mut MutableAppContext,
@@ -225,6 +230,16 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::EMPTY,
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR,
+            CodeActionKind::REFACTOR_EXTRACT,
+            CodeActionKind::SOURCE,
+        ])
+    }
+
     async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
         Default::default()
     }
@@ -825,6 +840,7 @@ impl LanguageRegistry {
                 &binary.path,
                 &binary.arguments,
                 &root_path,
+                adapter.code_action_kinds(),
                 cx,
             )?;
 

crates/lsp/src/lsp.rs ๐Ÿ”—

@@ -40,6 +40,7 @@ pub struct LanguageServer {
     outbound_tx: channel::Sender<Vec<u8>>,
     name: String,
     capabilities: ServerCapabilities,
+    code_action_kinds: Option<Vec<CodeActionKind>>,
     notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
     executor: Arc<executor::Background>,
@@ -110,6 +111,7 @@ impl LanguageServer {
         binary_path: &Path,
         arguments: &[T],
         root_path: &Path,
+        code_action_kinds: Option<Vec<CodeActionKind>>,
         cx: AsyncAppContext,
     ) -> Result<Self> {
         let working_dir = if root_path.is_dir() {
@@ -135,6 +137,7 @@ impl LanguageServer {
             stout,
             Some(server),
             root_path,
+            code_action_kinds,
             cx,
             |notification| {
                 log::info!(
@@ -160,6 +163,7 @@ impl LanguageServer {
         stdout: Stdout,
         server: Option<Child>,
         root_path: &Path,
+        code_action_kinds: Option<Vec<CodeActionKind>>,
         cx: AsyncAppContext,
         on_unhandled_notification: F,
     ) -> Self
@@ -197,6 +201,7 @@ impl LanguageServer {
             response_handlers,
             name: Default::default(),
             capabilities: Default::default(),
+            code_action_kinds,
             next_id: Default::default(),
             outbound_tx,
             executor: cx.background(),
@@ -207,6 +212,10 @@ impl LanguageServer {
         }
     }
 
+    pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        self.code_action_kinds.clone()
+    }
+
     async fn handle_input<Stdout, F>(
         stdout: Stdout,
         mut on_unhandled_notification: F,
@@ -715,6 +724,7 @@ impl LanguageServer {
             stdout_reader,
             None,
             Path::new("/"),
+            None,
             cx.clone(),
             |_| {},
         );
@@ -725,6 +735,7 @@ impl LanguageServer {
                 stdin_reader,
                 None,
                 Path::new("/"),
+                None,
                 cx,
                 move |msg| {
                     notifications_tx

crates/project/src/project.rs ๐Ÿ”—

@@ -3773,7 +3773,7 @@ impl Project {
             worktree = file.worktree.clone();
             buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
         } else {
-            return Task::ready(Ok(Default::default()));
+            return Task::ready(Ok(Vec::new()));
         };
         let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
 
@@ -3783,13 +3783,13 @@ impl Project {
             {
                 server.clone()
             } else {
-                return Task::ready(Ok(Default::default()));
+                return Task::ready(Ok(Vec::new()));
             };
 
             let lsp_range = range_to_lsp(range.to_point_utf16(buffer));
             cx.foreground().spawn(async move {
                 if lang_server.capabilities().code_action_provider.is_none() {
-                    return Ok(Default::default());
+                    return Ok(Vec::new());
                 }
 
                 Ok(lang_server
@@ -3802,13 +3802,7 @@ impl Project {
                         partial_result_params: Default::default(),
                         context: lsp::CodeActionContext {
                             diagnostics: relevant_diagnostics,
-                            only: Some(vec![
-                                lsp::CodeActionKind::EMPTY,
-                                lsp::CodeActionKind::QUICKFIX,
-                                lsp::CodeActionKind::REFACTOR,
-                                lsp::CodeActionKind::REFACTOR_EXTRACT,
-                                lsp::CodeActionKind::SOURCE,
-                            ]),
+                            only: lang_server.code_action_kinds(),
                         },
                     })
                     .await?

crates/util/Cargo.toml ๐Ÿ”—

@@ -6,7 +6,7 @@ publish = false
 
 [lib]
 path = "src/util.rs"
-doctest = false
+doctest = true
 
 [features]
 test-support = ["tempdir", "git2"]

crates/util/src/paths.rs ๐Ÿ”—

@@ -1,4 +1,4 @@
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 
 lazy_static::lazy_static! {
     pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
@@ -24,3 +24,49 @@ pub mod legacy {
         pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
     }
 }
+
+/// Compacts a given file path by replacing the user's home directory
+/// prefix with a tilde (`~`).
+///
+/// # Arguments
+///
+/// * `path` - A reference to a `Path` representing the file path to compact.
+///
+/// # Examples
+///
+/// ```
+/// use std::path::{Path, PathBuf};
+/// use util::paths::compact;
+/// let path: PathBuf = [
+///     util::paths::HOME.to_string_lossy().to_string(),
+///     "some_file.txt".to_string(),
+///  ]
+///  .iter()
+///  .collect();
+/// if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+///     assert_eq!(compact(&path).to_str(), Some("~/some_file.txt"));
+/// } else {
+///     assert_eq!(compact(&path).to_str(), path.to_str());
+/// }
+/// ```
+///
+/// # Returns
+///
+/// * A `PathBuf` containing the compacted file path. If the input path
+///   does not have the user's home directory prefix, or if we are not on
+///   Linux or macOS, the original path is returned unchanged.
+pub fn compact(path: &Path) -> PathBuf {
+    if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+        match path.strip_prefix(HOME.as_path()) {
+            Ok(relative_path) => {
+                let mut shortened_path = PathBuf::new();
+                shortened_path.push("~");
+                shortened_path.push(relative_path);
+                shortened_path
+            }
+            Err(_) => path.to_path_buf(),
+        }
+    } else {
+        path.to_path_buf()
+    }
+}

crates/util/src/test/marked_text.rs ๐Ÿ”—

@@ -87,21 +87,21 @@ pub fn marked_text_ranges_by(
 /// 1. To mark a range of text, surround it with the `ยซ` and `ยป` angle brackets,
 ///    which can be typed on a US keyboard with the `alt-|` and `alt-shift-|` keys.
 ///
-///    ```
+///    ```text
 ///    foo ยซselected textยป bar
 ///    ```
 ///
 /// 2. To mark a single position in the text, use the `ห‡` caron,
 ///    which can be typed on a US keyboard with the `alt-shift-t` key.
 ///
-///    ```
+///    ```text
 ///    the cursors are hereห‡ and hereห‡.
 ///    ```
 ///
 /// 3. To mark a range whose direction is meaningful (like a selection),
 ///    put a caron character beside one of its bounds, on the inside:
 ///
-///    ```
+///    ```text
 ///    one ยซห‡reversedยป selection and one ยซforwardห‡ยป selection
 ///    ```
 pub fn marked_text_ranges(

crates/zed/src/languages/typescript.rs ๐Ÿ”—

@@ -3,6 +3,7 @@ use async_trait::async_trait;
 use futures::StreamExt;
 use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
 use node_runtime::NodeRuntime;
+use lsp::CodeActionKind;
 use serde_json::json;
 use smol::fs;
 use std::{
@@ -134,6 +135,15 @@ impl LspAdapter for TypeScriptLspAdapter {
         .log_err()
     }
 
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR,
+            CodeActionKind::REFACTOR_EXTRACT,
+            CodeActionKind::SOURCE,
+        ])
+    }
+
     async fn label_for_completion(
         &self,
         item: &lsp::CompletionItem,

crates/zed/src/main.rs ๐Ÿ”—

@@ -103,7 +103,8 @@ fn main() {
                 .map_err(|_| anyhow!("no listener for open urls requests"))
                 .log_err();
         }
-    });
+    })
+    .on_reopen(move |cx| cx.dispatch_global_action(NewFile));
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);