diff --git a/Cargo.lock b/Cargo.lock
index f5dbc470f5..0fd0c8a212 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4878,6 +4878,8 @@ name = "terminal"
version = "0.1.0"
dependencies = [
"alacritty_terminal",
+ "client",
+ "dirs 4.0.0",
"editor",
"futures",
"gpui",
@@ -4910,6 +4912,7 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"rand 0.8.5",
+ "regex",
"smallvec",
"sum_tree",
"util",
@@ -6158,7 +6161,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.43.0"
+version = "0.45.0"
dependencies = [
"activity_indicator",
"anyhow",
diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg
new file mode 100644
index 0000000000..904fdaa1a7
--- /dev/null
+++ b/assets/icons/arrow-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg
new file mode 100644
index 0000000000..b7e1bec6d8
--- /dev/null
+++ b/assets/icons/arrow-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs
index 538b0fa4b0..0e9ec4076a 100644
--- a/crates/client/src/client.rs
+++ b/crates/client/src/client.rs
@@ -549,7 +549,7 @@ impl Client {
client.respond_with_error(
receipt,
proto::Error {
- message: error.to_string(),
+ message: format!("{:?}", error),
},
)?;
Err(error)
diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs
index a4c8386b13..7767b361c1 100644
--- a/crates/collab/src/integration_tests.rs
+++ b/crates/collab/src/integration_tests.rs
@@ -35,7 +35,7 @@ use project::{
use rand::prelude::*;
use rpc::PeerId;
use serde_json::json;
-use settings::Settings;
+use settings::{FormatOnSave, Settings};
use sqlx::types::time::OffsetDateTime;
use std::{
cell::RefCell,
@@ -1912,7 +1912,6 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
#[gpui::test(iterations = 10)]
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
- cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -1932,11 +1931,15 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
client_a.language_registry.add(Arc::new(language));
+ // Here we insert a fake tree with a directory that exists on disk. This is needed
+ // because later we'll invoke a command, which requires passing a working directory
+ // that points to a valid location on disk.
+ let directory = env::current_dir().unwrap();
client_a
.fs
- .insert_tree("/a", json!({ "a.rs": "let one = two" }))
+ .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
.await;
- let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
let buffer_b = cx_b
@@ -1967,7 +1970,28 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
.unwrap();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
- "let honey = two"
+ "let honey = \"two\""
+ );
+
+ // Ensure buffer can be formatted using an external command. Notice how the
+ // host's configuration is honored as opposed to using the guest's settings.
+ cx_a.update(|cx| {
+ cx.update_global(|settings: &mut Settings, _| {
+ settings.language_settings.format_on_save = Some(FormatOnSave::External {
+ command: "awk".to_string(),
+ arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
+ });
+ });
+ });
+ project_b
+ .update(cx_b, |project, cx| {
+ project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
+ format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
);
}
diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs
index 94eda67c39..ecc1b2df68 100644
--- a/crates/diagnostics/src/diagnostics.rs
+++ b/crates/diagnostics/src/diagnostics.rs
@@ -568,10 +568,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
}
fn should_update_tab_on_event(event: &Event) -> bool {
- matches!(
- event,
- Event::Saved | Event::DirtyChanged | Event::TitleChanged
- )
+ Editor::should_update_tab_on_event(event)
+ }
+
+ fn is_edit_event(event: &Self::Event) -> bool {
+ Editor::is_edit_event(event)
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) {
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 31a636fd61..c1e2557555 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -18,7 +18,6 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
-use futures::{channel::oneshot, FutureExt};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
@@ -51,7 +50,7 @@ use ordered_float::OrderedFloat;
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
-use settings::{Autosave, Settings};
+use settings::Settings;
use smallvec::SmallVec;
use smol::Timer;
use snippet::Snippet;
@@ -439,8 +438,6 @@ pub struct Editor {
leader_replica_id: Option,
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
- pending_autosave: Option>>,
- cancel_pending_autosave: Option>,
_subscriptions: Vec,
}
@@ -1028,13 +1025,10 @@ impl Editor {
leader_replica_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
- pending_autosave: Default::default(),
- cancel_pending_autosave: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
- cx.observe_window_activation(Self::on_window_activation_changed),
],
};
this.end_selection(cx);
@@ -4071,13 +4065,16 @@ impl Editor {
}
}
- nav_history.push(Some(NavigationData {
- cursor_anchor: position,
- cursor_position: point,
- scroll_position: self.scroll_position,
- scroll_top_anchor: self.scroll_top_anchor.clone(),
- scroll_top_row,
- }));
+ nav_history.push(
+ Some(NavigationData {
+ cursor_anchor: position,
+ cursor_position: point,
+ scroll_position: self.scroll_position,
+ scroll_top_anchor: self.scroll_top_anchor.clone(),
+ scroll_top_row,
+ }),
+ cx,
+ );
}
}
@@ -4675,7 +4672,7 @@ impl Editor {
definitions: Vec,
cx: &mut ViewContext,
) {
- let nav_history = workspace.active_pane().read(cx).nav_history().clone();
+ let pane = workspace.active_pane().clone();
for definition in definitions {
let range = definition
.target
@@ -4687,13 +4684,13 @@ impl Editor {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
if editor_handle != target_editor_handle {
- nav_history.borrow_mut().disable();
+ pane.update(cx, |pane, _| pane.disable_history());
}
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([range]);
});
- nav_history.borrow_mut().enable();
+ pane.update(cx, |pane, _| pane.enable_history());
});
}
}
@@ -5584,33 +5581,6 @@ impl Editor {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited);
- if let Autosave::AfterDelay { milliseconds } = cx.global::().autosave {
- let pending_autosave =
- self.pending_autosave.take().unwrap_or(Task::ready(None));
- if let Some(cancel_pending_autosave) = self.cancel_pending_autosave.take() {
- let _ = cancel_pending_autosave.send(());
- }
-
- let (cancel_tx, mut cancel_rx) = oneshot::channel();
- self.cancel_pending_autosave = Some(cancel_tx);
- self.pending_autosave = Some(cx.spawn_weak(|this, mut cx| async move {
- let mut timer = cx
- .background()
- .timer(Duration::from_millis(milliseconds))
- .fuse();
- pending_autosave.await;
- futures::select_biased! {
- _ = cancel_rx => return None,
- _ = timer => {}
- }
-
- this.upgrade(&cx)?
- .update(&mut cx, |this, cx| this.autosave(cx))
- .await
- .log_err();
- None
- }));
- }
}
language::Event::Reparsed => cx.emit(Event::Reparsed),
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
@@ -5629,25 +5599,6 @@ impl Editor {
cx.notify();
}
- fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) {
- if !active && cx.global::().autosave == Autosave::OnWindowChange {
- self.autosave(cx).detach_and_log_err(cx);
- }
- }
-
- fn autosave(&mut self, cx: &mut ViewContext) -> Task> {
- if let Some(project) = self.project.clone() {
- if self.buffer.read(cx).is_dirty(cx)
- && !self.buffer.read(cx).has_conflict(cx)
- && workspace::Item::can_save(self, cx)
- {
- return workspace::Item::save(self, project, cx);
- }
- }
-
- Task::ready(Ok(()))
- }
-
pub fn set_searchable(&mut self, searchable: bool) {
self.searchable = searchable;
}
@@ -5693,8 +5644,8 @@ impl Editor {
editor_handle.update(cx, |editor, cx| {
editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
});
- let nav_history = workspace.active_pane().read(cx).nav_history().clone();
- nav_history.borrow_mut().disable();
+ let pane = workspace.active_pane().clone();
+ pane.update(cx, |pane, _| pane.disable_history());
// We defer the pane interaction because we ourselves are a workspace item
// and activating a new item causes the pane to call a method on us reentrantly,
@@ -5709,7 +5660,7 @@ impl Editor {
});
}
- nav_history.borrow_mut().enable();
+ pane.update(cx, |pane, _| pane.enable_history());
});
}
@@ -5865,10 +5816,6 @@ impl View for Editor {
hide_hover(self, cx);
cx.emit(Event::Blurred);
cx.notify();
-
- if cx.global::().autosave == Autosave::OnFocusChange {
- self.autosave(cx).detach_and_log_err(cx);
- }
}
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
@@ -6282,23 +6229,22 @@ mod tests {
use super::*;
use futures::StreamExt;
use gpui::{
- executor::Deterministic,
geometry::rect::RectF,
platform::{WindowBounds, WindowOptions},
};
use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig};
use lsp::FakeLanguageServer;
- use project::{FakeFs, Fs};
+ use project::FakeFs;
use settings::LanguageSettings;
- use std::{cell::RefCell, path::Path, rc::Rc, time::Instant};
+ use std::{cell::RefCell, rc::Rc, time::Instant};
use text::Point;
use unindent::Unindent;
use util::{
assert_set_eq,
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
};
- use workspace::{FollowableItem, Item, ItemHandle};
+ use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
@@ -6646,12 +6592,20 @@ mod tests {
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
use workspace::Item;
- let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
+ let pane = cx.add_view(Default::default(), |cx| Pane::new(cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
cx.add_window(Default::default(), |cx| {
let mut editor = build_editor(buffer.clone(), cx);
- editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle()));
+ let handle = cx.handle();
+ editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
+
+ fn pop_history(
+ editor: &mut Editor,
+ cx: &mut MutableAppContext,
+ ) -> Option {
+ editor.nav_history.as_mut().unwrap().pop_backward(cx)
+ }
// Move the cursor a small distance.
// Nothing is added to the navigation history.
@@ -6661,21 +6615,21 @@ mod tests {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
});
- assert!(nav_history.borrow_mut().pop_backward().is_none());
+ assert!(pop_history(&mut editor, cx).is_none());
// Move the cursor a large distance.
// The history can jump back to the previous position.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
});
- let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
editor.navigate(nav_entry.data.unwrap(), cx);
assert_eq!(nav_entry.item.id(), cx.view_id());
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
);
- assert!(nav_history.borrow_mut().pop_backward().is_none());
+ assert!(pop_history(&mut editor, cx).is_none());
// Move the cursor a small distance via the mouse.
// Nothing is added to the navigation history.
@@ -6685,7 +6639,7 @@ mod tests {
editor.selections.display_ranges(cx),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
);
- assert!(nav_history.borrow_mut().pop_backward().is_none());
+ assert!(pop_history(&mut editor, cx).is_none());
// Move the cursor a large distance via the mouse.
// The history can jump back to the previous position.
@@ -6695,14 +6649,14 @@ mod tests {
editor.selections.display_ranges(cx),
&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
);
- let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
editor.navigate(nav_entry.data.unwrap(), cx);
assert_eq!(nav_entry.item.id(), cx.view_id());
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
);
- assert!(nav_history.borrow_mut().pop_backward().is_none());
+ assert!(pop_history(&mut editor, cx).is_none());
// Set scroll position to check later
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
@@ -6715,7 +6669,7 @@ mod tests {
assert_ne!(editor.scroll_position, original_scroll_position);
assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
- let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
editor.navigate(nav_entry.data.unwrap(), cx);
assert_eq!(editor.scroll_position, original_scroll_position);
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
@@ -9562,72 +9516,6 @@ mod tests {
save.await.unwrap();
}
- #[gpui::test]
- async fn test_autosave(deterministic: Arc, cx: &mut gpui::TestAppContext) {
- deterministic.forbid_parking();
-
- let fs = FakeFs::new(cx.background().clone());
- fs.insert_file("/file.rs", Default::default()).await;
-
- let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
- let buffer = project
- .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
- .await
- .unwrap();
-
- let (_, editor) = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
-
- // Autosave on window change.
- editor.update(cx, |editor, cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::OnWindowChange;
- });
- editor.insert("X", cx);
- assert!(editor.is_dirty(cx))
- });
-
- // Deactivating the window saves the file.
- cx.simulate_window_activation(None);
- deterministic.run_until_parked();
- assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "X");
- editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
-
- // Autosave on focus change.
- editor.update(cx, |editor, cx| {
- cx.focus_self();
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::OnFocusChange;
- });
- editor.insert("X", cx);
- assert!(editor.is_dirty(cx))
- });
-
- // Blurring the editor saves the file.
- editor.update(cx, |_, cx| cx.blur());
- deterministic.run_until_parked();
- assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XX");
- editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
-
- // Autosave after delay.
- editor.update(cx, |editor, cx| {
- cx.update_global(|settings: &mut Settings, _| {
- settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
- });
- editor.insert("X", cx);
- assert!(editor.is_dirty(cx))
- });
-
- // Delay hasn't fully expired, so the file is still dirty and unsaved.
- deterministic.advance_clock(Duration::from_millis(250));
- assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XX");
- editor.read_with(cx, |editor, cx| assert!(editor.is_dirty(cx)));
-
- // After delay expires, the file is saved.
- deterministic.advance_clock(Duration::from_millis(250));
- assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XXX");
- editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
- }
-
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
let mut language = Language::new(
diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs
index 8e15dce83c..0e3aca1447 100644
--- a/crates/editor/src/items.rs
+++ b/crates/editor/src/items.rs
@@ -352,13 +352,8 @@ impl Item for Editor {
project: ModelHandle,
cx: &mut ViewContext,
) -> Task> {
- let settings = cx.global::();
let buffer = self.buffer().clone();
- let mut buffers = buffer.read(cx).all_buffers();
- buffers.retain(|buffer| {
- let language_name = buffer.read(cx).language().map(|l| l.name());
- settings.format_on_save(language_name.as_deref())
- });
+ let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
cx.spawn(|this, mut cx| async move {
@@ -445,6 +440,10 @@ impl Item for Editor {
Event::Saved | Event::DirtyChanged | Event::TitleChanged
)
}
+
+ fn is_edit_event(event: &Self::Event) -> bool {
+ matches!(event, Event::BufferEdited)
+ }
}
impl ProjectItem for Editor {
diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs
index 505f609f57..b81714e0bc 100644
--- a/crates/gpui/src/app.rs
+++ b/crates/gpui/src/app.rs
@@ -151,6 +151,7 @@ pub struct AsyncAppContext(Rc>);
pub struct TestAppContext {
cx: Rc>,
foreground_platform: Rc,
+ condition_duration: Option,
}
impl App {
@@ -337,6 +338,7 @@ impl TestAppContext {
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
+ condition_duration: None,
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
@@ -612,6 +614,19 @@ impl TestAppContext {
test_window
})
}
+
+ pub fn set_condition_duration(&mut self, duration: Duration) {
+ self.condition_duration = Some(duration);
+ }
+ pub fn condition_duration(&self) -> Duration {
+ self.condition_duration.unwrap_or_else(|| {
+ if std::env::var("CI").is_ok() {
+ Duration::from_secs(2)
+ } else {
+ Duration::from_millis(500)
+ }
+ })
+ }
}
impl AsyncAppContext {
@@ -811,7 +826,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
type SubscriptionCallback = Box bool>;
type GlobalSubscriptionCallback = Box;
type ObservationCallback = Box bool>;
-type FocusObservationCallback = Box bool>;
+type FocusObservationCallback = Box bool>;
type GlobalObservationCallback = Box;
type ReleaseObservationCallback = Box;
type ActionObservationCallback = Box;
@@ -1305,7 +1320,7 @@ impl MutableAppContext {
fn observe_focus(&mut self, handle: &ViewHandle, mut callback: F) -> Subscription
where
- F: 'static + FnMut(ViewHandle, &mut MutableAppContext) -> bool,
+ F: 'static + FnMut(ViewHandle, bool, &mut MutableAppContext) -> bool,
V: View,
{
let subscription_id = post_inc(&mut self.next_subscription_id);
@@ -1314,9 +1329,9 @@ impl MutableAppContext {
self.pending_effects.push_back(Effect::FocusObservation {
view_id,
subscription_id,
- callback: Box::new(move |cx| {
+ callback: Box::new(move |focused, cx| {
if let Some(observed) = observed.upgrade(cx) {
- callback(observed, cx)
+ callback(observed, focused, cx)
} else {
false
}
@@ -2525,6 +2540,31 @@ impl MutableAppContext {
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
blurred_view.on_blur(this, window_id, blurred_id);
this.cx.views.insert((window_id, blurred_id), blurred_view);
+
+ let callbacks = this.focus_observations.lock().remove(&blurred_id);
+ if let Some(callbacks) = callbacks {
+ for (id, callback) in callbacks {
+ if let Some(mut callback) = callback {
+ let alive = callback(false, this);
+ if alive {
+ match this
+ .focus_observations
+ .lock()
+ .entry(blurred_id)
+ .or_default()
+ .entry(id)
+ {
+ btree_map::Entry::Vacant(entry) => {
+ entry.insert(Some(callback));
+ }
+ btree_map::Entry::Occupied(entry) => {
+ entry.remove();
+ }
+ }
+ }
+ }
+ }
+ }
}
}
@@ -2537,7 +2577,7 @@ impl MutableAppContext {
if let Some(callbacks) = callbacks {
for (id, callback) in callbacks {
if let Some(mut callback) = callback {
- let alive = callback(this);
+ let alive = callback(true, this);
if alive {
match this
.focus_observations
@@ -3598,20 +3638,21 @@ impl<'a, T: View> ViewContext<'a, T> {
pub fn observe_focus(&mut self, handle: &ViewHandle, mut callback: F) -> Subscription
where
- F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext),
+ F: 'static + FnMut(&mut T, ViewHandle, bool, &mut ViewContext),
V: View,
{
let observer = self.weak_handle();
- self.app.observe_focus(handle, move |observed, cx| {
- if let Some(observer) = observer.upgrade(cx) {
- observer.update(cx, |observer, cx| {
- callback(observer, observed, cx);
- });
- true
- } else {
- false
- }
- })
+ self.app
+ .observe_focus(handle, move |observed, focused, cx| {
+ if let Some(observer) = observer.upgrade(cx) {
+ observer.update(cx, |observer, cx| {
+ callback(observer, observed, focused, cx);
+ });
+ true
+ } else {
+ false
+ }
+ })
}
pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -4398,6 +4439,7 @@ impl ViewHandle {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
+ let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = self.update(&mut *cx, |_, cx| {
@@ -4419,14 +4461,9 @@ impl ViewHandle {
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
- let duration = if std::env::var("CI").is_ok() {
- Duration::from_secs(2)
- } else {
- Duration::from_millis(500)
- };
async move {
- crate::util::timeout(duration, async move {
+ crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
@@ -6448,11 +6485,13 @@ mod tests {
view_1.update(cx, |_, cx| {
cx.observe_focus(&view_2, {
let observed_events = observed_events.clone();
- move |this, view, cx| {
+ move |this, view, focused, cx| {
+ let label = if focused { "focus" } else { "blur" };
observed_events.lock().push(format!(
- "{} observed {}'s focus",
+ "{} observed {}'s {}",
this.name,
- view.read(cx).name
+ view.read(cx).name,
+ label
))
}
})
@@ -6461,16 +6500,20 @@ mod tests {
view_2.update(cx, |_, cx| {
cx.observe_focus(&view_1, {
let observed_events = observed_events.clone();
- move |this, view, cx| {
+ move |this, view, focused, cx| {
+ let label = if focused { "focus" } else { "blur" };
observed_events.lock().push(format!(
- "{} observed {}'s focus",
+ "{} observed {}'s {}",
this.name,
- view.read(cx).name
+ view.read(cx).name,
+ label
))
}
})
.detach();
});
+ assert_eq!(mem::take(&mut *view_events.lock()), ["view 1 focused"]);
+ assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
view_1.update(cx, |_, cx| {
// Ensure only the latest focus is honored.
@@ -6478,31 +6521,47 @@ mod tests {
cx.focus(&view_1);
cx.focus(&view_2);
});
- view_1.update(cx, |_, cx| cx.focus(&view_1));
- view_1.update(cx, |_, cx| cx.focus(&view_2));
- view_1.update(cx, |_, _| drop(view_2));
-
assert_eq!(
- *view_events.lock(),
- [
- "view 1 focused".to_string(),
- "view 1 blurred".to_string(),
- "view 2 focused".to_string(),
- "view 2 blurred".to_string(),
- "view 1 focused".to_string(),
- "view 1 blurred".to_string(),
- "view 2 focused".to_string(),
- "view 1 focused".to_string(),
- ],
+ mem::take(&mut *view_events.lock()),
+ ["view 1 blurred", "view 2 focused"],
);
assert_eq!(
- *observed_events.lock(),
+ mem::take(&mut *observed_events.lock()),
[
- "view 1 observed view 2's focus".to_string(),
- "view 2 observed view 1's focus".to_string(),
- "view 1 observed view 2's focus".to_string(),
+ "view 2 observed view 1's blur",
+ "view 1 observed view 2's focus"
]
);
+
+ view_1.update(cx, |_, cx| cx.focus(&view_1));
+ assert_eq!(
+ mem::take(&mut *view_events.lock()),
+ ["view 2 blurred", "view 1 focused"],
+ );
+ assert_eq!(
+ mem::take(&mut *observed_events.lock()),
+ [
+ "view 1 observed view 2's blur",
+ "view 2 observed view 1's focus"
+ ]
+ );
+
+ view_1.update(cx, |_, cx| cx.focus(&view_2));
+ assert_eq!(
+ mem::take(&mut *view_events.lock()),
+ ["view 1 blurred", "view 2 focused"],
+ );
+ assert_eq!(
+ mem::take(&mut *observed_events.lock()),
+ [
+ "view 2 observed view 1's blur",
+ "view 1 observed view 2's focus"
+ ]
+ );
+
+ view_1.update(cx, |_, _| drop(view_2));
+ assert_eq!(mem::take(&mut *view_events.lock()), ["view 1 focused"]);
+ assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
}
#[crate::test(self)]
diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs
index 4d50affdd5..d5ed1c1620 100644
--- a/crates/language/src/buffer.rs
+++ b/crates/language/src/buffer.rs
@@ -53,7 +53,6 @@ pub struct Buffer {
saved_version: clock::Global,
saved_version_fingerprint: String,
saved_mtime: SystemTime,
- line_ending: LineEnding,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option,
language: Option>,
@@ -98,12 +97,6 @@ pub enum IndentKind {
Tab,
}
-#[derive(Copy, Debug, Clone, PartialEq, Eq)]
-pub enum LineEnding {
- Unix,
- Windows,
-}
-
#[derive(Clone, Debug)]
struct SelectionSet {
line_mode: bool,
@@ -280,7 +273,7 @@ pub struct Chunk<'a> {
pub is_unnecessary: bool,
}
-pub(crate) struct Diff {
+pub struct Diff {
base_version: clock::Global,
new_text: Arc,
changes: Vec<(ChangeTag, usize)>,
@@ -314,32 +307,26 @@ impl CharKind {
}
impl Buffer {
- pub fn new>>(
+ pub fn new>(
replica_id: ReplicaId,
base_text: T,
cx: &mut ModelContext,
) -> Self {
- let history = History::new(base_text.into());
- let line_ending = LineEnding::detect(&history.base_text);
Self::build(
- TextBuffer::new(replica_id, cx.model_id() as u64, history),
+ TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
None,
- line_ending,
)
}
- pub fn from_file>>(
+ pub fn from_file>(
replica_id: ReplicaId,
base_text: T,
file: Arc,
cx: &mut ModelContext,
) -> Self {
- let history = History::new(base_text.into());
- let line_ending = LineEnding::detect(&history.base_text);
Self::build(
- TextBuffer::new(replica_id, cx.model_id() as u64, history),
+ TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
Some(file),
- line_ending,
)
}
@@ -349,14 +336,12 @@ impl Buffer {
file: Option>,
cx: &mut ModelContext,
) -> Result {
- let buffer = TextBuffer::new(
- replica_id,
- message.id,
- History::new(Arc::from(message.base_text)),
- );
- let line_ending = proto::LineEnding::from_i32(message.line_ending)
- .ok_or_else(|| anyhow!("missing line_ending"))?;
- let mut this = Self::build(buffer, file, LineEnding::from_proto(line_ending));
+ let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
+ let mut this = Self::build(buffer, file);
+ this.text.set_line_ending(proto::deserialize_line_ending(
+ proto::LineEnding::from_i32(message.line_ending)
+ .ok_or_else(|| anyhow!("missing line_ending"))?,
+ ));
let ops = message
.operations
.into_iter()
@@ -421,7 +406,7 @@ impl Buffer {
diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
diagnostics_timestamp: self.diagnostics_timestamp.value,
completion_triggers: self.completion_triggers.clone(),
- line_ending: self.line_ending.to_proto() as i32,
+ line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
}
}
@@ -430,7 +415,7 @@ impl Buffer {
self
}
- fn build(buffer: TextBuffer, file: Option>, line_ending: LineEnding) -> Self {
+ fn build(buffer: TextBuffer, file: Option>) -> Self {
let saved_mtime;
if let Some(file) = file.as_ref() {
saved_mtime = file.mtime();
@@ -446,7 +431,6 @@ impl Buffer {
was_dirty_before_starting_transaction: None,
text: buffer,
file,
- line_ending,
syntax_tree: Mutex::new(None),
parsing_in_background: false,
parse_count: 0,
@@ -507,7 +491,7 @@ impl Buffer {
self.remote_id(),
text,
version,
- self.line_ending,
+ self.line_ending(),
cx.as_mut(),
);
cx.spawn(|this, mut cx| async move {
@@ -563,7 +547,7 @@ impl Buffer {
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
- this.line_ending,
+ this.line_ending(),
new_mtime,
cx,
);
@@ -588,14 +572,14 @@ impl Buffer {
) {
self.saved_version = version;
self.saved_version_fingerprint = fingerprint;
- self.line_ending = line_ending;
+ self.text.set_line_ending(line_ending);
self.saved_mtime = mtime;
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
file.buffer_reloaded(
self.remote_id(),
&self.saved_version,
self.saved_version_fingerprint.clone(),
- self.line_ending,
+ self.line_ending(),
self.saved_mtime,
cx,
);
@@ -974,13 +958,13 @@ impl Buffer {
}
}
- pub(crate) fn diff(&self, new_text: String, cx: &AppContext) -> Task {
+ pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task {
let old_text = self.as_rope().clone();
let base_version = self.version();
cx.background().spawn(async move {
let old_text = old_text.to_string();
let line_ending = LineEnding::detect(&new_text);
- let new_text = new_text.replace("\r\n", "\n").replace('\r', "\n");
+ LineEnding::normalize(&mut new_text);
let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str())
.iter_all_changes()
.map(|c| (c.tag(), c.value().len()))
@@ -995,15 +979,11 @@ impl Buffer {
})
}
- pub(crate) fn apply_diff(
- &mut self,
- diff: Diff,
- cx: &mut ModelContext,
- ) -> Option<&Transaction> {
+ pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext) -> Option<&Transaction> {
if self.version == diff.base_version {
self.finalize_last_transaction();
self.start_transaction();
- self.line_ending = diff.line_ending;
+ self.text.set_line_ending(diff.line_ending);
let mut offset = diff.start_offset;
for (tag, len) in diff.changes {
let range = offset..(offset + len);
@@ -1249,7 +1229,8 @@ impl Buffer {
let inserted_ranges = edits
.into_iter()
- .filter_map(|(range, new_text)| {
+ .zip(&edit_operation.as_edit().unwrap().new_text)
+ .filter_map(|((range, _), new_text)| {
let first_newline_ix = new_text.find('\n')?;
let new_text_len = new_text.len();
let start = (delta + range.start as isize) as usize + first_newline_ix + 1;
@@ -1518,10 +1499,6 @@ impl Buffer {
pub fn completion_triggers(&self) -> &[String] {
&self.completion_triggers
}
-
- pub fn line_ending(&self) -> LineEnding {
- self.line_ending
- }
}
#[cfg(any(test, feature = "test-support"))]
@@ -2542,52 +2519,6 @@ impl std::ops::SubAssign for IndentSize {
}
}
-impl LineEnding {
- pub fn from_proto(style: proto::LineEnding) -> Self {
- match style {
- proto::LineEnding::Unix => Self::Unix,
- proto::LineEnding::Windows => Self::Windows,
- }
- }
-
- fn detect(text: &str) -> Self {
- let text = &text[..cmp::min(text.len(), 1000)];
- if let Some(ix) = text.find('\n') {
- if ix == 0 || text.as_bytes()[ix - 1] != b'\r' {
- Self::Unix
- } else {
- Self::Windows
- }
- } else {
- Default::default()
- }
- }
-
- pub fn as_str(self) -> &'static str {
- match self {
- LineEnding::Unix => "\n",
- LineEnding::Windows => "\r\n",
- }
- }
-
- pub fn to_proto(self) -> proto::LineEnding {
- match self {
- LineEnding::Unix => proto::LineEnding::Unix,
- LineEnding::Windows => proto::LineEnding::Windows,
- }
- }
-}
-
-impl Default for LineEnding {
- fn default() -> Self {
- #[cfg(unix)]
- return Self::Unix;
-
- #[cfg(not(unix))]
- return Self::Windows;
- }
-}
-
impl Completion {
pub fn sort_key(&self) -> (usize, &str) {
let kind_key = match self.lsp_completion.kind {
diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs
index 0e876d14df..7c7ec65fd8 100644
--- a/crates/language/src/proto.rs
+++ b/crates/language/src/proto.rs
@@ -11,6 +11,20 @@ use text::*;
pub use proto::{Buffer, BufferState, LineEnding, SelectionSet};
+pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
+ match message {
+ LineEnding::Unix => text::LineEnding::Unix,
+ LineEnding::Windows => text::LineEnding::Windows,
+ }
+}
+
+pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding {
+ match message {
+ text::LineEnding::Unix => proto::LineEnding::Unix,
+ text::LineEnding::Windows => proto::LineEnding::Windows,
+ }
+}
+
pub fn serialize_operation(operation: &Operation) -> proto::Operation {
proto::Operation {
variant: Some(match operation {
diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs
index 723e57ded4..ba8744624d 100644
--- a/crates/language/src/tests.rs
+++ b/crates/language/src/tests.rs
@@ -22,6 +22,29 @@ fn init_logger() {
}
}
+#[gpui::test]
+fn test_line_endings(cx: &mut gpui::MutableAppContext) {
+ cx.add_model(|cx| {
+ let mut buffer =
+ Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
+ assert_eq!(buffer.text(), "one\ntwo\nthree");
+ assert_eq!(buffer.line_ending(), LineEnding::Windows);
+
+ buffer.check_invariants();
+ buffer.edit_with_autoindent(
+ [(buffer.len()..buffer.len(), "\r\nfour")],
+ IndentSize::spaces(2),
+ cx,
+ );
+ buffer.edit([(0..0, "zero\r\n")], cx);
+ assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour");
+ assert_eq!(buffer.line_ending(), LineEnding::Windows);
+ buffer.check_invariants();
+
+ buffer
+ });
+}
+
#[gpui::test]
fn test_select_language() {
let registry = LanguageRegistry::test();
@@ -421,7 +444,7 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
async fn search<'a>(
outline: &'a Outline,
query: &str,
- cx: &gpui::TestAppContext,
+ cx: &'a gpui::TestAppContext,
) -> Vec<(&'a str, Vec)> {
let matches = cx
.read(|cx| outline.search(query, cx.background().clone()))
diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs
index 17d7264f1d..5c52801611 100644
--- a/crates/project/src/fs.rs
+++ b/crates/project/src/fs.rs
@@ -334,28 +334,6 @@ impl FakeFs {
})
}
- pub async fn insert_dir(&self, path: impl AsRef) {
- let mut state = self.state.lock().await;
- let path = path.as_ref();
- state.validate_path(path).unwrap();
-
- let inode = state.next_inode;
- state.next_inode += 1;
- state.entries.insert(
- path.to_path_buf(),
- FakeFsEntry {
- metadata: Metadata {
- inode,
- mtime: SystemTime::now(),
- is_dir: true,
- is_symlink: false,
- },
- content: None,
- },
- );
- state.emit_event(&[path]).await;
- }
-
pub async fn insert_file(&self, path: impl AsRef, content: String) {
let mut state = self.state.lock().await;
let path = path.as_ref();
@@ -392,7 +370,7 @@ impl FakeFs {
match tree {
Object(map) => {
- self.insert_dir(path).await;
+ self.create_dir(path).await.unwrap();
for (name, contents) in map {
let mut path = PathBuf::from(path);
path.push(name);
@@ -400,7 +378,7 @@ impl FakeFs {
}
}
Null => {
- self.insert_dir(&path).await;
+ self.create_dir(&path).await.unwrap();
}
String(contents) => {
self.insert_file(&path, contents).await;
diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs
index 677a7afa9a..0ac3064e56 100644
--- a/crates/project/src/project.rs
+++ b/crates/project/src/project.rs
@@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
-use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
+use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -20,7 +20,10 @@ use gpui::{
};
use language::{
point_to_lsp,
- proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
+ proto::{
+ deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor,
+ serialize_version,
+ },
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CharKind, CodeAction, CodeLabel,
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _,
Language, LanguageRegistry, LanguageServerName, LineEnding, LocalFile, LspAdapter,
@@ -48,10 +51,12 @@ use std::{
ffi::OsString,
hash::Hash,
mem,
+ num::NonZeroU32,
ops::Range,
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
path::{Component, Path, PathBuf},
rc::Rc,
+ str,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
@@ -3022,78 +3027,50 @@ impl Project {
}
for (buffer, buffer_abs_path, language_server) in local_buffers {
- let text_document = lsp::TextDocumentIdentifier::new(
- lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
- );
- let capabilities = &language_server.capabilities();
- let tab_size = cx.update(|cx| {
- let language_name = buffer.read(cx).language().map(|language| language.name());
- cx.global::().tab_size(language_name.as_deref())
+ let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| {
+ let settings = cx.global::();
+ let language_name = buffer.language().map(|language| language.name());
+ (
+ settings.format_on_save(language_name.as_deref()),
+ settings.tab_size(language_name.as_deref()),
+ )
});
- let lsp_edits = if capabilities
- .document_formatting_provider
- .as_ref()
- .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
- {
- language_server
- .request::(lsp::DocumentFormattingParams {
- text_document,
- options: lsp::FormattingOptions {
- tab_size: tab_size.into(),
- insert_spaces: true,
- insert_final_newline: Some(true),
- ..Default::default()
- },
- work_done_progress_params: Default::default(),
- })
- .await?
- } else if capabilities
- .document_range_formatting_provider
- .as_ref()
- .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
- {
- let buffer_start = lsp::Position::new(0, 0);
- let buffer_end =
- buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
- language_server
- .request::(
- lsp::DocumentRangeFormattingParams {
- text_document,
- range: lsp::Range::new(buffer_start, buffer_end),
- options: lsp::FormattingOptions {
- tab_size: tab_size.into(),
- insert_spaces: true,
- insert_final_newline: Some(true),
- ..Default::default()
- },
- work_done_progress_params: Default::default(),
- },
+
+ let transaction = match format_on_save {
+ settings::FormatOnSave::Off => continue,
+ settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
+ &this,
+ &buffer,
+ &buffer_abs_path,
+ &language_server,
+ tab_size,
+ &mut cx,
+ )
+ .await
+ .context("failed to format via language server")?,
+ settings::FormatOnSave::External { command, arguments } => {
+ Self::format_via_external_command(
+ &buffer,
+ &buffer_abs_path,
+ &command,
+ &arguments,
+ &mut cx,
)
- .await?
- } else {
- continue;
+ .await
+ .context(format!(
+ "failed to format via external command {:?}",
+ command
+ ))?
+ }
};
- if let Some(lsp_edits) = lsp_edits {
- let edits = this
- .update(&mut cx, |this, cx| {
- this.edits_from_lsp(&buffer, lsp_edits, None, cx)
- })
- .await?;
- buffer.update(&mut cx, |buffer, cx| {
- buffer.finalize_last_transaction();
- buffer.start_transaction();
- for (range, text) in edits {
- buffer.edit([(range, text)], cx);
- }
- if buffer.end_transaction(cx).is_some() {
- let transaction = buffer.finalize_last_transaction().unwrap().clone();
- if !push_to_history {
- buffer.forget_transaction(transaction.id);
- }
- project_transaction.0.insert(cx.handle(), transaction);
- }
- });
+ if let Some(transaction) = transaction {
+ if !push_to_history {
+ buffer.update(&mut cx, |buffer, _| {
+ buffer.forget_transaction(transaction.id)
+ });
+ }
+ project_transaction.0.insert(buffer, transaction);
}
}
@@ -3101,6 +3078,141 @@ impl Project {
})
}
+ async fn format_via_lsp(
+ this: &ModelHandle,
+ buffer: &ModelHandle,
+ abs_path: &Path,
+ language_server: &Arc,
+ tab_size: NonZeroU32,
+ cx: &mut AsyncAppContext,
+ ) -> Result