Merge branch 'main' into storybook

Nathan Sobo created

Change summary

Cargo.lock                                            |  10 
Cargo.toml                                            |   1 
README.md                                             |  37 +
assets/keymaps/vim.json                               |  12 
crates/file_finder/src/file_finder.rs                 |   9 
crates/live_kit_client/LiveKitBridge/Package.resolved |   4 
crates/search/src/project_search.rs                   |  45 +
crates/semantic_index/src/embedding.rs                |  70 ++
crates/semantic_index/src/semantic_index.rs           |  11 
crates/semantic_index/src/semantic_index_tests.rs     |   6 
crates/terminal_view/src/terminal_view.rs             |   7 
crates/workspace/src/item.rs                          |  10 
crates/workspace/src/pane.rs                          | 198 ++++++--
crates/workspace/src/workspace.rs                     |  31 
crates/zed/Cargo.toml                                 |   1 
crates/zed/src/languages.rs                           |   1 
crates/zed/src/languages/nu/brackets.scm              |   4 
crates/zed/src/languages/nu/config.toml               |   9 
crates/zed/src/languages/nu/highlights.scm            | 302 +++++++++++++
crates/zed/src/languages/nu/indents.scm               |   3 
crates/zed/src/menus.rs                               |   7 
crates/zed/src/zed.rs                                 |  52 +
22 files changed, 734 insertions(+), 96 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -8425,6 +8425,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-nu"
+version = "0.0.1"
+source = "git+https://github.com/nushell/tree-sitter-nu?rev=786689b0562b9799ce53e824cb45a1a2a04dc673#786689b0562b9799ce53e824cb45a1a2a04dc673"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-php"
 version = "0.19.1"
@@ -9887,6 +9896,7 @@ dependencies = [
  "tree-sitter-lua",
  "tree-sitter-markdown",
  "tree-sitter-nix",
+ "tree-sitter-nu",
  "tree-sitter-php",
  "tree-sitter-python",
  "tree-sitter-racket",

Cargo.toml ๐Ÿ”—

@@ -142,6 +142,7 @@ tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-rack
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"}
 tree-sitter-lua = "0.0.14"
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
+tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }

README.md ๐Ÿ”—

@@ -8,7 +8,31 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
 
 ### Dependencies
 
-* Install [Postgres.app](https://postgresapp.com) and start it.
+* Install Xcode from https://apps.apple.com/us/app/xcode/id497799835?mt=12, and accept the license:
+  ```
+  sudo xcodebuild -license
+  ```
+
+* Install homebrew, rust and node
+  ```
+  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+  brew install rust
+  brew install node
+  ```
+  
+* Ensure rust executables are in your $PATH
+  ```
+  echo $HOME/.cargo/bin | sudo tee /etc/paths.d/10-rust
+  ```
+  
+* Install postgres and configure the database
+  ```
+  brew install postgresql@15
+  brew services start postgresql@15
+  psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres
+  psql -U postgres -c "CREATE DATABASE zed"
+  ```
+  
 * Install the `LiveKit` server and the `foreman` process supervisor:
 
     ```
@@ -41,6 +65,17 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
     GITHUB_TOKEN=<$token> script/bootstrap
     ```
 
+* Now try running zed with collaboration disabled:
+  ```
+  cargo run
+  ```
+
+### Common errors
+
+* `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
+  * You need to install Xcode and then run: `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
+  * (see https://github.com/gfx-rs/gfx/issues/2309)
+
 ### Testing against locally-running servers
 
 Start the web and collab servers:

assets/keymaps/vim.json ๐Ÿ”—

@@ -198,6 +198,18 @@
       "z c": "editor::Fold",
       "z o": "editor::UnfoldLines",
       "z f": "editor::FoldSelectedRanges",
+      "shift-z shift-q": [
+        "pane::CloseActiveItem",
+        {
+          "saveBehavior": "dontSave"
+        }
+      ],
+      "shift-z shift-z": [
+        "pane::CloseActiveItem",
+        {
+          "saveBehavior": "promptOnConflict"
+        }
+      ],
       // Count support
       "1": [
         "vim::Number",

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

@@ -1528,8 +1528,13 @@ mod tests {
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         active_pane
             .update(cx, |pane, cx| {
-                pane.close_active_item(&workspace::CloseActiveItem, cx)
-                    .unwrap()
+                pane.close_active_item(
+                    &workspace::CloseActiveItem {
+                        save_behavior: None,
+                    },
+                    cx,
+                )
+                .unwrap()
             })
             .await
             .unwrap();

crates/live_kit_client/LiveKitBridge/Package.resolved ๐Ÿ”—

@@ -42,8 +42,8 @@
         "repositoryURL": "https://github.com/apple/swift-protobuf.git",
         "state": {
           "branch": null,
-          "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
-          "version": "1.21.0"
+          "revision": "ce20dc083ee485524b802669890291c0d8090170",
+          "version": "1.22.1"
         }
       }
     ]

crates/search/src/project_search.rs ๐Ÿ”—

@@ -34,6 +34,7 @@ use std::{
     ops::{Not, Range},
     path::PathBuf,
     sync::Arc,
+    time::{Duration, Instant},
 };
 use util::ResultExt as _;
 use workspace::{
@@ -130,6 +131,7 @@ pub struct ProjectSearchView {
 
 struct SemanticState {
     index_status: SemanticIndexStatus,
+    maintain_rate_limit: Option<Task<()>>,
     _subscription: Subscription,
 }
 
@@ -319,11 +321,28 @@ impl View for ProjectSearchView {
                 let status = semantic.index_status;
                 match status {
                     SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
-                    SemanticIndexStatus::Indexing { remaining_files } => {
+                    SemanticIndexStatus::Indexing {
+                        remaining_files,
+                        rate_limit_expiry,
+                    } => {
                         if remaining_files == 0 {
                             Some(format!("Indexing..."))
                         } else {
-                            Some(format!("Remaining files to index: {}", remaining_files))
+                            if let Some(rate_limit_expiry) = rate_limit_expiry {
+                                let remaining_seconds =
+                                    rate_limit_expiry.duration_since(Instant::now());
+                                if remaining_seconds > Duration::from_secs(0) {
+                                    Some(format!(
+                                        "Remaining files to index (rate limit resets in {}s): {}",
+                                        remaining_seconds.as_secs(),
+                                        remaining_files
+                                    ))
+                                } else {
+                                    Some(format!("Remaining files to index: {}", remaining_files))
+                                }
+                            } else {
+                                Some(format!("Remaining files to index: {}", remaining_files))
+                            }
                         }
                     }
                     SemanticIndexStatus::NotIndexed => None,
@@ -651,9 +670,10 @@ impl ProjectSearchView {
 
             self.semantic_state = Some(SemanticState {
                 index_status: semantic_index.read(cx).status(&project),
+                maintain_rate_limit: None,
                 _subscription: cx.observe(&semantic_index, Self::semantic_index_changed),
             });
-            cx.notify();
+            self.semantic_index_changed(semantic_index, cx);
         }
     }
 
@@ -664,8 +684,25 @@ impl ProjectSearchView {
     ) {
         let project = self.model.read(cx).project.clone();
         if let Some(semantic_state) = self.semantic_state.as_mut() {
-            semantic_state.index_status = semantic_index.read(cx).status(&project);
             cx.notify();
+            semantic_state.index_status = semantic_index.read(cx).status(&project);
+            if let SemanticIndexStatus::Indexing {
+                rate_limit_expiry: Some(_),
+                ..
+            } = &semantic_state.index_status
+            {
+                if semantic_state.maintain_rate_limit.is_none() {
+                    semantic_state.maintain_rate_limit =
+                        Some(cx.spawn(|this, mut cx| async move {
+                            loop {
+                                cx.background().timer(Duration::from_secs(1)).await;
+                                this.update(&mut cx, |_, cx| cx.notify()).log_err();
+                            }
+                        }));
+                    return;
+                }
+            }
+            semantic_state.maintain_rate_limit = None;
         }
     }
 

crates/semantic_index/src/embedding.rs ๐Ÿ”—

@@ -7,13 +7,16 @@ use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
 use lazy_static::lazy_static;
+use parking_lot::Mutex;
 use parse_duration::parse;
+use postage::watch;
 use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
 use rusqlite::ToSql;
 use serde::{Deserialize, Serialize};
 use std::env;
+use std::ops::Add;
 use std::sync::Arc;
-use std::time::Duration;
+use std::time::{Duration, Instant};
 use tiktoken_rs::{cl100k_base, CoreBPE};
 use util::http::{HttpClient, Request};
 
@@ -82,6 +85,8 @@ impl ToSql for Embedding {
 pub struct OpenAIEmbeddings {
     pub client: Arc<dyn HttpClient>,
     pub executor: Arc<Background>,
+    rate_limit_count_rx: watch::Receiver<Option<Instant>>,
+    rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
 }
 
 #[derive(Serialize)]
@@ -114,12 +119,16 @@ pub trait EmbeddingProvider: Sync + Send {
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
     fn max_tokens_per_batch(&self) -> usize;
     fn truncate(&self, span: &str) -> (String, usize);
+    fn rate_limit_expiration(&self) -> Option<Instant>;
 }
 
 pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        None
+    }
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
         // 1024 is the OpenAI Embeddings size for ada models.
         // the model we will likely be starting with.
@@ -149,6 +158,50 @@ impl EmbeddingProvider for DummyEmbeddings {
 const OPENAI_INPUT_LIMIT: usize = 8190;
 
 impl OpenAIEmbeddings {
+    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Background>) -> Self {
+        let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
+        let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
+
+        OpenAIEmbeddings {
+            client,
+            executor,
+            rate_limit_count_rx,
+            rate_limit_count_tx,
+        }
+    }
+
+    fn resolve_rate_limit(&self) {
+        let reset_time = *self.rate_limit_count_tx.lock().borrow();
+
+        if let Some(reset_time) = reset_time {
+            if Instant::now() >= reset_time {
+                *self.rate_limit_count_tx.lock().borrow_mut() = None
+            }
+        }
+
+        log::trace!(
+            "resolving reset time: {:?}",
+            *self.rate_limit_count_tx.lock().borrow()
+        );
+    }
+
+    fn update_reset_time(&self, reset_time: Instant) {
+        let original_time = *self.rate_limit_count_tx.lock().borrow();
+
+        let updated_time = if let Some(original_time) = original_time {
+            if reset_time < original_time {
+                Some(reset_time)
+            } else {
+                Some(original_time)
+            }
+        } else {
+            Some(reset_time)
+        };
+
+        log::trace!("updating rate limit time: {:?}", updated_time);
+
+        *self.rate_limit_count_tx.lock().borrow_mut() = updated_time;
+    }
     async fn send_request(
         &self,
         api_key: &str,
@@ -179,6 +232,9 @@ impl EmbeddingProvider for OpenAIEmbeddings {
         50000
     }
 
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        *self.rate_limit_count_rx.borrow()
+    }
     fn truncate(&self, span: &str) -> (String, usize) {
         let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span);
         let output = if tokens.len() > OPENAI_INPUT_LIMIT {
@@ -203,6 +259,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
             .ok_or_else(|| anyhow!("no api key"))?;
 
         let mut request_number = 0;
+        let mut rate_limiting = false;
         let mut request_timeout: u64 = 15;
         let mut response: Response<AsyncBody>;
         while request_number < MAX_RETRIES {
@@ -229,6 +286,12 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         response.usage.total_tokens
                     );
 
+                    // If we complete a request successfully that was previously rate_limited
+                    // resolve the rate limit
+                    if rate_limiting {
+                        self.resolve_rate_limit()
+                    }
+
                     return Ok(response
                         .data
                         .into_iter()
@@ -236,6 +299,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         .collect());
                 }
                 StatusCode::TOO_MANY_REQUESTS => {
+                    rate_limiting = true;
                     let mut body = String::new();
                     response.body_mut().read_to_string(&mut body).await?;
 
@@ -254,6 +318,10 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         }
                     };
 
+                    // If we've previously rate limited, increment the duration but not the count
+                    let reset_time = Instant::now().add(delay_duration);
+                    self.update_reset_time(reset_time);
+
                     log::trace!(
                         "openai rate limiting: waiting {:?} until lifted",
                         &delay_duration

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

@@ -91,10 +91,7 @@ pub fn init(
         let semantic_index = SemanticIndex::new(
             fs,
             db_file_path,
-            Arc::new(OpenAIEmbeddings {
-                client: http_client,
-                executor: cx.background(),
-            }),
+            Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
             language_registry,
             cx.clone(),
         )
@@ -113,7 +110,10 @@ pub fn init(
 pub enum SemanticIndexStatus {
     NotIndexed,
     Indexed,
-    Indexing { remaining_files: usize },
+    Indexing {
+        remaining_files: usize,
+        rate_limit_expiry: Option<Instant>,
+    },
 }
 
 pub struct SemanticIndex {
@@ -293,6 +293,7 @@ impl SemanticIndex {
             } else {
                 SemanticIndexStatus::Indexing {
                     remaining_files: project_state.pending_file_count_rx.borrow().clone(),
+                    rate_limit_expiry: self.embedding_provider.rate_limit_expiration(),
                 }
             }
         } else {

crates/semantic_index/src/semantic_index_tests.rs ๐Ÿ”—

@@ -21,7 +21,7 @@ use std::{
         atomic::{self, AtomicUsize},
         Arc,
     },
-    time::SystemTime,
+    time::{Instant, SystemTime},
 };
 use unindent::Unindent;
 use util::RandomCharIter;
@@ -1275,6 +1275,10 @@ impl EmbeddingProvider for FakeEmbeddingProvider {
         200
     }
 
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        None
+    }
+
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
         self.embedding_count
             .fetch_add(spans.len(), atomic::Ordering::SeqCst);

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

@@ -283,7 +283,12 @@ impl TerminalView {
     pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
         let menu_entries = vec![
             ContextMenuItem::action("Clear", Clear),
-            ContextMenuItem::action("Close", pane::CloseActiveItem),
+            ContextMenuItem::action(
+                "Close",
+                pane::CloseActiveItem {
+                    save_behavior: None,
+                },
+            ),
         ];
 
         self.context_menu.update(cx, |menu, cx| {

crates/workspace/src/item.rs ๐Ÿ”—

@@ -474,8 +474,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                     for item_event in T::to_item_events(event).into_iter() {
                         match item_event {
                             ItemEvent::CloseItem => {
-                                pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx))
-                                    .detach_and_log_err(cx);
+                                pane.update(cx, |pane, cx| {
+                                    pane.close_item_by_id(
+                                        item.id(),
+                                        crate::SaveBehavior::PromptOnWrite,
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
                                 return;
                             }
 

crates/workspace/src/pane.rs ๐Ÿ”—

@@ -43,6 +43,19 @@ use std::{
 };
 use theme::{Theme, ThemeSettings};
 
+#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub enum SaveBehavior {
+    /// ask before overwriting conflicting files (used by default with %s)
+    PromptOnConflict,
+    /// ask before writing any file that wouldn't be auto-saved (used by default with %w)
+    PromptOnWrite,
+    /// never prompt, write on conflict (used with vim's :w!)
+    SilentlyOverwrite,
+    /// skip all save-related behaviour (used with vim's :cq)
+    DontSave,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivateItem(pub usize);
 
@@ -64,13 +77,17 @@ pub struct CloseItemsToTheRightById {
     pub pane: WeakViewHandle<Pane>,
 }
 
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+pub struct CloseActiveItem {
+    pub save_behavior: Option<SaveBehavior>,
+}
+
 actions!(
     pane,
     [
         ActivatePrevItem,
         ActivateNextItem,
         ActivateLastItem,
-        CloseActiveItem,
         CloseInactiveItems,
         CloseCleanItems,
         CloseItemsToTheLeft,
@@ -86,7 +103,7 @@ actions!(
     ]
 );
 
-impl_actions!(pane, [ActivateItem]);
+impl_actions!(pane, [ActivateItem, CloseActiveItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -696,22 +713,29 @@ impl Pane {
 
     pub fn close_active_item(
         &mut self,
-        _: &CloseActiveItem,
+        action: &CloseActiveItem,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         if self.items.is_empty() {
             return None;
         }
         let active_item_id = self.items[self.active_item_index].id();
-        Some(self.close_item_by_id(active_item_id, cx))
+        Some(self.close_item_by_id(
+            active_item_id,
+            action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+            cx,
+        ))
     }
 
     pub fn close_item_by_id(
         &mut self,
         item_id_to_close: usize,
+        save_behavior: SaveBehavior,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        self.close_items(cx, move |view_id| view_id == item_id_to_close)
+        self.close_items(cx, save_behavior, move |view_id| {
+            view_id == item_id_to_close
+        })
     }
 
     pub fn close_inactive_items(
@@ -724,7 +748,11 @@ impl Pane {
         }
 
         let active_item_id = self.items[self.active_item_index].id();
-        Some(self.close_items(cx, move |item_id| item_id != active_item_id))
+        Some(
+            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+                item_id != active_item_id
+            }),
+        )
     }
 
     pub fn close_clean_items(
@@ -737,7 +765,11 @@ impl Pane {
             .filter(|item| !item.is_dirty(cx))
             .map(|item| item.id())
             .collect();
-        Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
+        Some(
+            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+                item_ids.contains(&item_id)
+            }),
+        )
     }
 
     pub fn close_items_to_the_left(
@@ -762,7 +794,9 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, move |item_id| item_ids.contains(&item_id))
+        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+            item_ids.contains(&item_id)
+        })
     }
 
     pub fn close_items_to_the_right(
@@ -788,7 +822,9 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, move |item_id| item_ids.contains(&item_id))
+        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+            item_ids.contains(&item_id)
+        })
     }
 
     pub fn close_all_items(
@@ -800,12 +836,13 @@ impl Pane {
             return None;
         }
 
-        Some(self.close_items(cx, move |_| true))
+        Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
     }
 
     pub fn close_items(
         &mut self,
         cx: &mut ViewContext<Pane>,
+        save_behavior: SaveBehavior,
         should_close: impl 'static + Fn(usize) -> bool,
     ) -> Task<Result<()>> {
         // Find the items to close.
@@ -858,8 +895,15 @@ impl Pane {
                     .any(|id| saved_project_items_ids.insert(*id));
 
                 if should_save
-                    && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
-                        .await?
+                    && !Self::save_item(
+                        project.clone(),
+                        &pane,
+                        item_ix,
+                        &*item,
+                        save_behavior,
+                        &mut cx,
+                    )
+                    .await?
                 {
                     break;
                 }
@@ -954,13 +998,17 @@ impl Pane {
         pane: &WeakViewHandle<Pane>,
         item_ix: usize,
         item: &dyn ItemHandle,
-        should_prompt_for_save: bool,
+        save_behavior: SaveBehavior,
         cx: &mut AsyncAppContext,
     ) -> Result<bool> {
         const CONFLICT_MESSAGE: &str =
             "This file has changed on disk since you started editing it. Do you want to overwrite it?";
         const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 
+        if save_behavior == SaveBehavior::DontSave {
+            return Ok(true);
+        }
+
         let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
             (
                 item.has_conflict(cx),
@@ -971,18 +1019,22 @@ impl Pane {
         });
 
         if has_conflict && can_save {
-            let mut answer = pane.update(cx, |pane, cx| {
-                pane.activate_item(item_ix, true, true, cx);
-                cx.prompt(
-                    PromptLevel::Warning,
-                    CONFLICT_MESSAGE,
-                    &["Overwrite", "Discard", "Cancel"],
-                )
-            })?;
-            match answer.next().await {
-                Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
-                Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
-                _ => return Ok(false),
+            if save_behavior == SaveBehavior::SilentlyOverwrite {
+                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
+            } else {
+                let mut answer = pane.update(cx, |pane, cx| {
+                    pane.activate_item(item_ix, true, true, cx);
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        CONFLICT_MESSAGE,
+                        &["Overwrite", "Discard", "Cancel"],
+                    )
+                })?;
+                match answer.next().await {
+                    Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
+                    Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
+                    _ => return Ok(false),
+                }
             }
         } else if is_dirty && (can_save || is_singleton) {
             let will_autosave = cx.read(|cx| {
@@ -991,7 +1043,7 @@ impl Pane {
                     AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
                 ) && Self::can_autosave_item(&*item, cx)
             });
-            let should_save = if should_prompt_for_save && !will_autosave {
+            let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
                 let mut answer = pane.update(cx, |pane, cx| {
                     pane.activate_item(item_ix, true, true, cx);
                     cx.prompt(
@@ -1113,7 +1165,12 @@ impl Pane {
                 AnchorCorner::TopLeft,
                 if is_active_item {
                     vec![
-                        ContextMenuItem::action("Close Active Item", CloseActiveItem),
+                        ContextMenuItem::action(
+                            "Close Active Item",
+                            CloseActiveItem {
+                                save_behavior: None,
+                            },
+                        ),
                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
                         ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
@@ -1128,8 +1185,12 @@ impl Pane {
                             move |cx| {
                                 if let Some(pane) = pane.upgrade(cx) {
                                     pane.update(cx, |pane, cx| {
-                                        pane.close_item_by_id(target_item_id, cx)
-                                            .detach_and_log_err(cx);
+                                        pane.close_item_by_id(
+                                            target_item_id,
+                                            SaveBehavior::PromptOnWrite,
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
                                     })
                                 }
                             }
@@ -1278,7 +1339,12 @@ impl Pane {
                                 .on_click(MouseButton::Middle, {
                                     let item_id = item.id();
                                     move |_, pane, cx| {
-                                        pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
+                                        pane.close_item_by_id(
+                                            item_id,
+                                            SaveBehavior::PromptOnWrite,
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
                                     }
                                 })
                                 .on_down(
@@ -1486,7 +1552,8 @@ impl Pane {
                     cx.window_context().defer(move |cx| {
                         if let Some(pane) = pane.upgrade(cx) {
                             pane.update(cx, |pane, cx| {
-                                pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
+                                pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
+                                    .detach_and_log_err(cx);
                             });
                         }
                     });
@@ -2087,7 +2154,14 @@ mod tests {
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         pane.update(cx, |pane, cx| {
-            assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
+            assert!(pane
+                .close_active_item(
+                    &CloseActiveItem {
+                        save_behavior: None
+                    },
+                    cx
+                )
+                .is_none())
         });
     }
 
@@ -2337,31 +2411,59 @@ mod tests {
         add_labeled_item(&pane, "1", false, cx);
         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
 
         pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A", "B*", "C"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A", "C*"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
-            .unwrap()
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(
+                &CloseActiveItem {
+                    save_behavior: None,
+                },
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
         assert_item_labels(&pane, ["A*"], cx);
     }
 

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

@@ -1308,13 +1308,15 @@ impl Workspace {
             }
 
             Ok(this
-                .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
+                .update(&mut cx, |this, cx| {
+                    this.save_all_internal(SaveBehavior::PromptOnWrite, cx)
+                })?
                 .await?)
         })
     }
 
     fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let save_all = self.save_all_internal(false, cx);
+        let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
         Some(cx.foreground().spawn(async move {
             save_all.await?;
             Ok(())
@@ -1323,7 +1325,7 @@ impl Workspace {
 
     fn save_all_internal(
         &mut self,
-        should_prompt_to_save: bool,
+        save_behaviour: SaveBehavior,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         if self.project.read(cx).is_read_only() {
@@ -1358,7 +1360,7 @@ impl Workspace {
                             &pane,
                             ix,
                             &*item,
-                            should_prompt_to_save,
+                            save_behaviour,
                             &mut cx,
                         )
                         .await?
@@ -4358,7 +4360,9 @@ mod tests {
             let item1_id = item1.id();
             let item3_id = item3.id();
             let item4_id = item4.id();
-            pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| {
+                [item1_id, item3_id, item4_id].contains(&id)
+            })
         });
         cx.foreground().run_until_parked();
 
@@ -4493,7 +4497,9 @@ mod tests {
         // once for project entry 0, and once for project entry 2. After those two
         // prompts, the task should complete.
 
-        let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
+        let close = left_pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true)
+        });
         cx.foreground().run_until_parked();
         left_pane.read_with(cx, |pane, cx| {
             assert_eq!(
@@ -4609,9 +4615,11 @@ mod tests {
             item.is_dirty = true;
         });
 
-        pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
+        })
+        .await
+        .unwrap();
         assert!(!window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
 
@@ -4630,8 +4638,9 @@ mod tests {
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
 
         // Ensure autosave is prevented for deleted files also when closing the buffer.
-        let _close_items =
-            pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
+        let _close_items = pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
+        });
         deterministic.run_until_parked();
         assert!(window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));

crates/zed/Cargo.toml ๐Ÿ”—

@@ -132,6 +132,7 @@ tree-sitter-racket.workspace = true
 tree-sitter-yaml.workspace = true
 tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
+tree-sitter-nu.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

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

@@ -170,6 +170,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
+    language("nu", tree_sitter_nu::language(), vec![]);
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/zed/src/languages/nu/config.toml ๐Ÿ”—

@@ -0,0 +1,9 @@
+name = "Nu"
+path_suffixes = ["nu"]
+line_comment = "# "
+autoclose_before = ";:.,=}])>` \n\t\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

crates/zed/src/languages/nu/highlights.scm ๐Ÿ”—

@@ -0,0 +1,302 @@
+;;; ---
+;;; keywords
+[
+    "def"
+    "def-env"
+    "alias"
+    "export-env"
+    "export"
+    "extern"
+    "module"
+
+    "let"
+    "let-env"
+    "mut"
+    "const"
+
+    "hide-env"
+
+    "source"
+    "source-env"
+
+    "overlay"
+    "register"
+
+    "loop"
+    "while"
+    "error"
+
+    "do"
+    "if"
+    "else"
+    "try"
+    "catch"
+    "match"
+
+    "break"
+    "continue"
+    "return"
+
+] @keyword
+
+(hide_mod "hide" @keyword)
+(decl_use "use" @keyword)
+
+(ctrl_for
+    "for" @keyword
+    "in" @keyword
+)
+(overlay_list "list" @keyword)
+(overlay_hide "hide" @keyword)
+(overlay_new "new" @keyword)
+(overlay_use
+    "use" @keyword
+    "as" @keyword
+)
+(ctrl_error "make" @keyword)
+
+;;; ---
+;;; literals
+(val_number) @constant
+(val_duration
+    unit: [
+        "ns" "ยตs" "us" "ms" "sec" "min" "hr" "day" "wk"
+    ] @variable
+)
+(val_filesize
+    unit: [
+        "b" "B"
+
+        "kb" "kB" "Kb" "KB"
+        "mb" "mB" "Mb" "MB"
+        "gb" "gB" "Gb" "GB"
+        "tb" "tB" "Tb" "TB"
+        "pb" "pB" "Pb" "PB"
+        "eb" "eB" "Eb" "EB"
+        "zb" "zB" "Zb" "ZB"
+
+        "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
+        "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
+        "gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB"
+        "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
+        "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
+        "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
+        "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB"
+    ] @variable
+)
+(val_binary
+    [
+       "0b"
+       "0o"
+       "0x"
+    ] @constant
+    "[" @punctuation.bracket
+    digit: [
+        "," @punctuation.delimiter
+        (hex_digit) @constant
+    ]
+    "]" @punctuation.bracket
+) @constant
+(val_bool) @constant.builtin
+(val_nothing) @constant.builtin
+(val_string) @string
+(val_date) @constant
+(inter_escape_sequence) @constant
+(escape_sequence) @constant
+(val_interpolated [
+    "$\""
+    "$\'"
+    "\""
+    "\'"
+] @string)
+(unescaped_interpolated_content) @string
+(escaped_interpolated_content) @string
+(expr_interpolated ["(" ")"] @variable)
+
+;;; ---
+;;; operators
+(expr_binary [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(expr_binary opr: ([
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+]) @keyword)
+
+(where_command [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(assignment [
+    "="
+    "+="
+    "-="
+    "*="
+    "/="
+    "++="
+] @operator)
+
+(expr_unary ["not" "-"] @operator)
+
+(val_range [
+    ".."
+    "..="
+    "..<"
+] @operator)
+
+["=>" "=" "|"] @operator
+
+[
+    "o>"   "out>"
+    "e>"   "err>"
+    "e+o>" "err+out>"
+    "o+e>" "out+err>"
+] @special
+
+;;; ---
+;;; punctuation
+[
+    ","
+    ";"
+] @punctuation.delimiter
+
+(param_short_flag "-" @punctuation.delimiter)
+(param_long_flag ["--"] @punctuation.delimiter)
+(long_flag ["--"] @punctuation.delimiter)
+(param_rest "..." @punctuation.delimiter)
+(param_type [":"] @punctuation.special)
+(param_value ["="] @punctuation.special)
+(param_cmd ["@"] @punctuation.special)
+(param_opt ["?"] @punctuation.special)
+
+[
+    "(" ")"
+    "{" "}"
+    "[" "]"
+] @punctuation.bracket
+
+(val_record
+  (record_entry ":" @punctuation.delimiter))
+;;; ---
+;;; identifiers
+(param_rest
+    name: (_) @variable)
+(param_opt
+    name: (_) @variable)
+(parameter
+    param_name: (_) @variable)
+(param_cmd
+    (cmd_identifier) @string)
+(param_long_flag) @variable
+(param_short_flag) @variable
+
+(short_flag) @variable
+(long_flag) @variable
+
+(scope_pattern [(wild_card) @function])
+
+(cmd_identifier) @function
+
+(command
+    "^" @punctuation.delimiter
+    head: (_) @function
+)
+
+"where" @function
+
+(path
+  ["." "?"] @punctuation.delimiter
+) @variable
+
+(val_variable
+  "$" @operator
+  [
+   (identifier) @variable
+   "in" @type.builtin
+   "nu" @type.builtin
+   "env" @type.builtin
+   "nothing" @type.builtin
+   ]  ; If we have a special styling, use it here
+)
+;;; ---
+;;; types
+(flat_type) @type.builtin
+(list_type
+    "list" @type
+    ["<" ">"] @punctuation.bracket
+)
+(collection_type
+    ["record" "table"] @type
+    "<" @punctuation.bracket
+    key: (_) @variable
+    ["," ":"] @punctuation.delimiter
+    ">" @punctuation.bracket
+)
+
+(shebang) @comment
+(comment) @comment

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

@@ -41,7 +41,12 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::action("Save", workspace::Save),
                 MenuItem::action("Save Asโ€ฆ", workspace::SaveAs),
                 MenuItem::action("Save All", workspace::SaveAll),
-                MenuItem::action("Close Editor", workspace::CloseActiveItem),
+                MenuItem::action(
+                    "Close Editor",
+                    workspace::CloseActiveItem {
+                        save_behavior: None,
+                    },
+                ),
                 MenuItem::action("Close Window", workspace::CloseWindow),
             ],
         },

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

@@ -733,7 +733,7 @@ mod tests {
     use theme::{ThemeRegistry, ThemeSettings};
     use workspace::{
         item::{Item, ItemHandle},
-        open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
+        open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
@@ -1495,7 +1495,12 @@ mod tests {
 
             pane2_item.downcast::<Editor>().unwrap().downgrade()
         });
-        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
+        cx.dispatch_action(
+            window.into(),
+            workspace::CloseActiveItem {
+                save_behavior: None,
+            },
+        );
 
         cx.foreground().run_until_parked();
         workspace.read_with(cx, |workspace, _| {
@@ -1503,7 +1508,12 @@ mod tests {
             assert_eq!(workspace.active_pane(), &pane_1);
         });
 
-        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
+        cx.dispatch_action(
+            window.into(),
+            workspace::CloseActiveItem {
+                save_behavior: None,
+            },
+        );
         cx.foreground().run_until_parked();
         window.simulate_prompt_answer(1, cx);
         cx.foreground().run_until_parked();
@@ -1661,7 +1671,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor3_id = editor3.id();
             drop(editor3);
-            pane.close_item_by_id(editor3_id, cx)
+            pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
         })
         .await
         .unwrap();
@@ -1696,7 +1706,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor2_id = editor2.id();
             drop(editor2);
-            pane.close_item_by_id(editor2_id, cx)
+            pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
         })
         .await
         .unwrap();
@@ -1852,24 +1862,32 @@ mod tests {
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         // Close all the pane items in some arbitrary order.
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
-        pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
-            .await
-            .unwrap();
+        pane.update(cx, |pane, cx| {
+            pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
+        })
+        .await
+        .unwrap();
         assert_eq!(active_path(&workspace, cx), None);
 
         // Reopen all the closed items, ensuring they are reopened in the same order