Merge branch 'main' into storybook

This commit is contained in:
Nathan Sobo 2023-09-11 09:13:46 -06:00
commit 15ea4af5e7
22 changed files with 734 additions and 96 deletions

10
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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:

View file

@ -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",

View file

@ -1528,7 +1528,12 @@ 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)
pane.close_active_item(
&workspace::CloseActiveItem {
save_behavior: None,
},
cx,
)
.unwrap()
})
.await

View file

@ -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"
}
}
]

View file

@ -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,12 +321,29 @@ 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 {
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;
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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);

View file

@ -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| {

View file

@ -474,7 +474,13 @@ 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))
pane.update(cx, |pane, cx| {
pane.close_item_by_id(
item.id(),
crate::SaveBehavior::PromptOnWrite,
cx,
)
})
.detach_and_log_err(cx);
return;
}

View file

@ -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,7 +895,14 @@ 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)
&& !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,6 +1019,9 @@ impl Pane {
});
if has_conflict && can_save {
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(
@ -984,6 +1035,7 @@ impl Pane {
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| {
matches!(
@ -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,7 +1185,11 @@ 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)
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,7 +2411,14 @@ 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))
pane.update(cx, |pane, cx| {
pane.close_active_item(
&CloseActiveItem {
save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
@ -2346,19 +2427,40 @@ mod tests {
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))
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))
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))
pane.update(cx, |pane, cx| {
pane.close_active_item(
&CloseActiveItem {
save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();

View file

@ -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,7 +4615,9 @@ mod tests {
item.is_dirty = true;
});
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
pane.update(cx, |pane, cx| {
pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
})
.await
.unwrap();
assert!(!window.has_pending_prompt(cx));
@ -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));

View file

@ -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"

View file

@ -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"))]

View file

@ -0,0 +1,4 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
(parameter_pipes "|" @open "|" @close)

View file

@ -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 },
]

View file

@ -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

View file

@ -0,0 +1,3 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View file

@ -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),
],
},

View file

@ -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,22 +1862,30 @@ 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))
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))
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))
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))
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);