Merge branch 'main' into terminal-selections
This commit is contained in:
commit
bc306ef8ed
32 changed files with 1327 additions and 553 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -4878,6 +4878,8 @@ name = "terminal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_terminal",
|
"alacritty_terminal",
|
||||||
|
"client",
|
||||||
|
"dirs 4.0.0",
|
||||||
"editor",
|
"editor",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -4910,6 +4912,7 @@ dependencies = [
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"postage",
|
"postage",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"regex",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"util",
|
"util",
|
||||||
|
@ -6158,7 +6161,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.43.0"
|
version = "0.45.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
3
assets/icons/arrow-left.svg
Normal file
3
assets/icons/arrow-left.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 3.99999C8 4.31671 7.76023 4.57258 7.44352 4.57258H1.95565L3.8416 6.45853C4.06527 6.6822 4.06527 7.04454 3.8416 7.2682C3.72887 7.38004 3.58215 7.43551 3.43542 7.43551C3.2887 7.43551 3.14233 7.37959 3.03068 7.26776L0.16775 4.40483C-0.0559165 4.18116 -0.0559165 3.81883 0.16775 3.59516L3.03068 0.732233C3.25434 0.508567 3.61668 0.508567 3.84035 0.732233C4.06401 0.955899 4.06401 1.31824 3.84035 1.5419L1.95565 3.42741H7.44352C7.76023 3.42741 8 3.68328 8 3.99999Z" fill="#839496"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 589 B |
3
assets/icons/arrow-right.svg
Normal file
3
assets/icons/arrow-right.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.83265 4.40382L4.97532 7.26115C4.8646 7.37365 4.71816 7.42901 4.57172 7.42901C4.42528 7.42901 4.2792 7.37321 4.16777 7.26159C3.94454 7.03836 3.94454 6.67673 4.16777 6.4535L6.05039 4.57169H0.571465C0.255909 4.57169 0 4.31631 0 4.00022C0 3.68413 0.255731 3.42876 0.571287 3.42876H6.05021L4.16795 1.54649C3.94472 1.32326 3.94472 0.961634 4.16795 0.738405C4.39117 0.515177 4.75281 0.515177 4.97603 0.738405L7.83336 3.59573C8.0557 3.81985 8.0557 4.18059 7.83247 4.40382H7.83265Z" fill="#FDF6E3"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 602 B |
|
@ -549,7 +549,7 @@ impl Client {
|
||||||
client.respond_with_error(
|
client.respond_with_error(
|
||||||
receipt,
|
receipt,
|
||||||
proto::Error {
|
proto::Error {
|
||||||
message: error.to_string(),
|
message: format!("{:?}", error),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
Err(error)
|
Err(error)
|
||||||
|
|
|
@ -35,7 +35,7 @@ use project::{
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::PeerId;
|
use rpc::PeerId;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::Settings;
|
use settings::{FormatOnSave, Settings};
|
||||||
use sqlx::types::time::OffsetDateTime;
|
use sqlx::types::time::OffsetDateTime;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
@ -1912,7 +1912,6 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
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 mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").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());
|
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
||||||
client_a.language_registry.add(Arc::new(language));
|
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
|
client_a
|
||||||
.fs
|
.fs
|
||||||
.insert_tree("/a", json!({ "a.rs": "let one = two" }))
|
.insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
|
||||||
.await;
|
.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 project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||||
|
|
||||||
let buffer_b = cx_b
|
let buffer_b = cx_b
|
||||||
|
@ -1967,7 +1970,28 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
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())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -568,10 +568,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_update_tab_on_event(event: &Event) -> bool {
|
fn should_update_tab_on_event(event: &Event) -> bool {
|
||||||
matches!(
|
Editor::should_update_tab_on_event(event)
|
||||||
event,
|
}
|
||||||
Event::Saved | Event::DirtyChanged | Event::TitleChanged
|
|
||||||
)
|
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<Self>) {
|
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
@ -18,7 +18,6 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
use display_map::*;
|
use display_map::*;
|
||||||
pub use element::*;
|
pub use element::*;
|
||||||
use futures::{channel::oneshot, FutureExt};
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
|
@ -51,7 +50,7 @@ use ordered_float::OrderedFloat;
|
||||||
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
|
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
|
||||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Autosave, Settings};
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
|
@ -439,8 +438,6 @@ pub struct Editor {
|
||||||
leader_replica_id: Option<u16>,
|
leader_replica_id: Option<u16>,
|
||||||
hover_state: HoverState,
|
hover_state: HoverState,
|
||||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||||
pending_autosave: Option<Task<Option<()>>>,
|
|
||||||
cancel_pending_autosave: Option<oneshot::Sender<()>>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1028,13 +1025,10 @@ impl Editor {
|
||||||
leader_replica_id: None,
|
leader_replica_id: None,
|
||||||
hover_state: Default::default(),
|
hover_state: Default::default(),
|
||||||
link_go_to_definition_state: Default::default(),
|
link_go_to_definition_state: Default::default(),
|
||||||
pending_autosave: Default::default(),
|
|
||||||
cancel_pending_autosave: Default::default(),
|
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.observe(&buffer, Self::on_buffer_changed),
|
cx.observe(&buffer, Self::on_buffer_changed),
|
||||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||||
cx.observe(&display_map, Self::on_display_map_changed),
|
cx.observe(&display_map, Self::on_display_map_changed),
|
||||||
cx.observe_window_activation(Self::on_window_activation_changed),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
@ -4071,13 +4065,16 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav_history.push(Some(NavigationData {
|
nav_history.push(
|
||||||
cursor_anchor: position,
|
Some(NavigationData {
|
||||||
cursor_position: point,
|
cursor_anchor: position,
|
||||||
scroll_position: self.scroll_position,
|
cursor_position: point,
|
||||||
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
scroll_position: self.scroll_position,
|
||||||
scroll_top_row,
|
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
||||||
}));
|
scroll_top_row,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4675,7 +4672,7 @@ impl Editor {
|
||||||
definitions: Vec<LocationLink>,
|
definitions: Vec<LocationLink>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
for definition in definitions {
|
for definition in definitions {
|
||||||
let range = definition
|
let range = definition
|
||||||
.target
|
.target
|
||||||
|
@ -4687,13 +4684,13 @@ impl Editor {
|
||||||
// When selecting a definition in a different buffer, disable the nav history
|
// When selecting a definition in a different buffer, disable the nav history
|
||||||
// to avoid creating a history entry at the previous cursor location.
|
// to avoid creating a history entry at the previous cursor location.
|
||||||
if editor_handle != target_editor_handle {
|
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| {
|
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||||
s.select_ranges([range]);
|
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_active_diagnostics(cx);
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
cx.emit(Event::BufferEdited);
|
cx.emit(Event::BufferEdited);
|
||||||
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().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::Reparsed => cx.emit(Event::Reparsed),
|
||||||
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
|
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
|
||||||
|
@ -5629,25 +5599,6 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
if !active && cx.global::<Settings>().autosave == Autosave::OnWindowChange {
|
|
||||||
self.autosave(cx).detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn autosave(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
|
||||||
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) {
|
pub fn set_searchable(&mut self, searchable: bool) {
|
||||||
self.searchable = searchable;
|
self.searchable = searchable;
|
||||||
}
|
}
|
||||||
|
@ -5693,8 +5644,8 @@ impl Editor {
|
||||||
editor_handle.update(cx, |editor, cx| {
|
editor_handle.update(cx, |editor, cx| {
|
||||||
editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
|
editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
|
||||||
});
|
});
|
||||||
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
nav_history.borrow_mut().disable();
|
pane.update(cx, |pane, _| pane.disable_history());
|
||||||
|
|
||||||
// We defer the pane interaction because we ourselves are a workspace item
|
// 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,
|
// 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);
|
hide_hover(self, cx);
|
||||||
cx.emit(Event::Blurred);
|
cx.emit(Event::Blurred);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
if cx.global::<Settings>().autosave == Autosave::OnFocusChange {
|
|
||||||
self.autosave(cx).detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
||||||
|
@ -6282,23 +6229,22 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
executor::Deterministic,
|
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
platform::{WindowBounds, WindowOptions},
|
platform::{WindowBounds, WindowOptions},
|
||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{FakeLspAdapter, LanguageConfig};
|
use language::{FakeLspAdapter, LanguageConfig};
|
||||||
use lsp::FakeLanguageServer;
|
use lsp::FakeLanguageServer;
|
||||||
use project::{FakeFs, Fs};
|
use project::FakeFs;
|
||||||
use settings::LanguageSettings;
|
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 text::Point;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::{
|
use util::{
|
||||||
assert_set_eq,
|
assert_set_eq,
|
||||||
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
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]
|
#[gpui::test]
|
||||||
fn test_edit_events(cx: &mut MutableAppContext) {
|
fn test_edit_events(cx: &mut MutableAppContext) {
|
||||||
|
@ -6646,12 +6592,20 @@ mod tests {
|
||||||
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
use workspace::Item;
|
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);
|
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||||
|
|
||||||
cx.add_window(Default::default(), |cx| {
|
cx.add_window(Default::default(), |cx| {
|
||||||
let mut editor = build_editor(buffer.clone(), 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<NavigationEntry> {
|
||||||
|
editor.nav_history.as_mut().unwrap().pop_backward(cx)
|
||||||
|
}
|
||||||
|
|
||||||
// Move the cursor a small distance.
|
// Move the cursor a small distance.
|
||||||
// Nothing is added to the navigation history.
|
// Nothing is added to the navigation history.
|
||||||
|
@ -6661,21 +6615,21 @@ mod tests {
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
|
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.
|
// Move the cursor a large distance.
|
||||||
// The history can jump back to the previous position.
|
// The history can jump back to the previous position.
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
|
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);
|
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||||
assert_eq!(nav_entry.item.id(), cx.view_id());
|
assert_eq!(nav_entry.item.id(), cx.view_id());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
|
&[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.
|
// Move the cursor a small distance via the mouse.
|
||||||
// Nothing is added to the navigation history.
|
// Nothing is added to the navigation history.
|
||||||
|
@ -6685,7 +6639,7 @@ mod tests {
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
|
&[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.
|
// Move the cursor a large distance via the mouse.
|
||||||
// The history can jump back to the previous position.
|
// The history can jump back to the previous position.
|
||||||
|
@ -6695,14 +6649,14 @@ mod tests {
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
|
&[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);
|
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||||
assert_eq!(nav_entry.item.id(), cx.view_id());
|
assert_eq!(nav_entry.item.id(), cx.view_id());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.selections.display_ranges(cx),
|
editor.selections.display_ranges(cx),
|
||||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
|
&[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
|
// Set scroll position to check later
|
||||||
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
|
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_position, original_scroll_position);
|
||||||
assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
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);
|
editor.navigate(nav_entry.data.unwrap(), cx);
|
||||||
assert_eq!(editor.scroll_position, original_scroll_position);
|
assert_eq!(editor.scroll_position, original_scroll_position);
|
||||||
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
||||||
|
@ -9562,72 +9516,6 @@ mod tests {
|
||||||
save.await.unwrap();
|
save.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_autosave(deterministic: Arc<Deterministic>, 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]
|
#[gpui::test]
|
||||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||||
let mut language = Language::new(
|
let mut language = Language::new(
|
||||||
|
|
|
@ -352,13 +352,8 @@ impl Item for Editor {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let settings = cx.global::<Settings>();
|
|
||||||
let buffer = self.buffer().clone();
|
let buffer = self.buffer().clone();
|
||||||
let mut buffers = buffer.read(cx).all_buffers();
|
let 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 mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||||
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
@ -445,6 +440,10 @@ impl Item for Editor {
|
||||||
Event::Saved | Event::DirtyChanged | Event::TitleChanged
|
Event::Saved | Event::DirtyChanged | Event::TitleChanged
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_edit_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, Event::BufferEdited)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectItem for Editor {
|
impl ProjectItem for Editor {
|
||||||
|
|
|
@ -151,6 +151,7 @@ pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
||||||
pub struct TestAppContext {
|
pub struct TestAppContext {
|
||||||
cx: Rc<RefCell<MutableAppContext>>,
|
cx: Rc<RefCell<MutableAppContext>>,
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
|
condition_duration: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
@ -337,6 +338,7 @@ impl TestAppContext {
|
||||||
let cx = TestAppContext {
|
let cx = TestAppContext {
|
||||||
cx: Rc::new(RefCell::new(cx)),
|
cx: Rc::new(RefCell::new(cx)),
|
||||||
foreground_platform,
|
foreground_platform,
|
||||||
|
condition_duration: None,
|
||||||
};
|
};
|
||||||
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
||||||
cx
|
cx
|
||||||
|
@ -612,6 +614,19 @@ impl TestAppContext {
|
||||||
test_window
|
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 {
|
impl AsyncAppContext {
|
||||||
|
@ -811,7 +826,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
|
||||||
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
|
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
|
||||||
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
||||||
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||||
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
|
||||||
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
|
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
|
||||||
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
||||||
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
|
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
|
||||||
|
@ -1305,7 +1320,7 @@ impl MutableAppContext {
|
||||||
|
|
||||||
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(ViewHandle<V>, &mut MutableAppContext) -> bool,
|
F: 'static + FnMut(ViewHandle<V>, bool, &mut MutableAppContext) -> bool,
|
||||||
V: View,
|
V: View,
|
||||||
{
|
{
|
||||||
let subscription_id = post_inc(&mut self.next_subscription_id);
|
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||||
|
@ -1314,9 +1329,9 @@ impl MutableAppContext {
|
||||||
self.pending_effects.push_back(Effect::FocusObservation {
|
self.pending_effects.push_back(Effect::FocusObservation {
|
||||||
view_id,
|
view_id,
|
||||||
subscription_id,
|
subscription_id,
|
||||||
callback: Box::new(move |cx| {
|
callback: Box::new(move |focused, cx| {
|
||||||
if let Some(observed) = observed.upgrade(cx) {
|
if let Some(observed) = observed.upgrade(cx) {
|
||||||
callback(observed, cx)
|
callback(observed, focused, cx)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -2525,6 +2540,31 @@ impl MutableAppContext {
|
||||||
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
|
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
|
||||||
blurred_view.on_blur(this, window_id, blurred_id);
|
blurred_view.on_blur(this, window_id, blurred_id);
|
||||||
this.cx.views.insert((window_id, blurred_id), blurred_view);
|
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 {
|
if let Some(callbacks) = callbacks {
|
||||||
for (id, callback) in callbacks {
|
for (id, callback) in callbacks {
|
||||||
if let Some(mut callback) = callback {
|
if let Some(mut callback) = callback {
|
||||||
let alive = callback(this);
|
let alive = callback(true, this);
|
||||||
if alive {
|
if alive {
|
||||||
match this
|
match this
|
||||||
.focus_observations
|
.focus_observations
|
||||||
|
@ -3598,20 +3638,21 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||||
|
|
||||||
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(&mut T, ViewHandle<V>, &mut ViewContext<T>),
|
F: 'static + FnMut(&mut T, ViewHandle<V>, bool, &mut ViewContext<T>),
|
||||||
V: View,
|
V: View,
|
||||||
{
|
{
|
||||||
let observer = self.weak_handle();
|
let observer = self.weak_handle();
|
||||||
self.app.observe_focus(handle, move |observed, cx| {
|
self.app
|
||||||
if let Some(observer) = observer.upgrade(cx) {
|
.observe_focus(handle, move |observed, focused, cx| {
|
||||||
observer.update(cx, |observer, cx| {
|
if let Some(observer) = observer.upgrade(cx) {
|
||||||
callback(observer, observed, cx);
|
observer.update(cx, |observer, cx| {
|
||||||
});
|
callback(observer, observed, focused, cx);
|
||||||
true
|
});
|
||||||
} else {
|
true
|
||||||
false
|
} else {
|
||||||
}
|
false
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
|
pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
|
||||||
|
@ -4398,6 +4439,7 @@ impl<T: View> ViewHandle<T> {
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||||
|
let timeout_duration = cx.condition_duration();
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
let mut cx = cx.cx.borrow_mut();
|
||||||
let subscriptions = self.update(&mut *cx, |_, cx| {
|
let subscriptions = self.update(&mut *cx, |_, cx| {
|
||||||
|
@ -4419,14 +4461,9 @@ impl<T: View> ViewHandle<T> {
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
let handle = self.downgrade();
|
let handle = self.downgrade();
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(2)
|
|
||||||
} else {
|
|
||||||
Duration::from_millis(500)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
crate::util::timeout(duration, async move {
|
crate::util::timeout(timeout_duration, async move {
|
||||||
loop {
|
loop {
|
||||||
{
|
{
|
||||||
let cx = cx.borrow();
|
let cx = cx.borrow();
|
||||||
|
@ -6448,11 +6485,13 @@ mod tests {
|
||||||
view_1.update(cx, |_, cx| {
|
view_1.update(cx, |_, cx| {
|
||||||
cx.observe_focus(&view_2, {
|
cx.observe_focus(&view_2, {
|
||||||
let observed_events = observed_events.clone();
|
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_events.lock().push(format!(
|
||||||
"{} observed {}'s focus",
|
"{} observed {}'s {}",
|
||||||
this.name,
|
this.name,
|
||||||
view.read(cx).name
|
view.read(cx).name,
|
||||||
|
label
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -6461,16 +6500,20 @@ mod tests {
|
||||||
view_2.update(cx, |_, cx| {
|
view_2.update(cx, |_, cx| {
|
||||||
cx.observe_focus(&view_1, {
|
cx.observe_focus(&view_1, {
|
||||||
let observed_events = observed_events.clone();
|
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_events.lock().push(format!(
|
||||||
"{} observed {}'s focus",
|
"{} observed {}'s {}",
|
||||||
this.name,
|
this.name,
|
||||||
view.read(cx).name
|
view.read(cx).name,
|
||||||
|
label
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.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| {
|
view_1.update(cx, |_, cx| {
|
||||||
// Ensure only the latest focus is honored.
|
// Ensure only the latest focus is honored.
|
||||||
|
@ -6478,31 +6521,47 @@ mod tests {
|
||||||
cx.focus(&view_1);
|
cx.focus(&view_1);
|
||||||
cx.focus(&view_2);
|
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!(
|
assert_eq!(
|
||||||
*view_events.lock(),
|
mem::take(&mut *view_events.lock()),
|
||||||
[
|
["view 1 blurred", "view 2 focused"],
|
||||||
"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(),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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 blur",
|
||||||
"view 2 observed view 1's focus".to_string(),
|
"view 1 observed view 2's focus"
|
||||||
"view 1 observed view 2's focus".to_string(),
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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)]
|
#[crate::test(self)]
|
||||||
|
|
|
@ -53,7 +53,6 @@ pub struct Buffer {
|
||||||
saved_version: clock::Global,
|
saved_version: clock::Global,
|
||||||
saved_version_fingerprint: String,
|
saved_version_fingerprint: String,
|
||||||
saved_mtime: SystemTime,
|
saved_mtime: SystemTime,
|
||||||
line_ending: LineEnding,
|
|
||||||
transaction_depth: usize,
|
transaction_depth: usize,
|
||||||
was_dirty_before_starting_transaction: Option<bool>,
|
was_dirty_before_starting_transaction: Option<bool>,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
|
@ -98,12 +97,6 @@ pub enum IndentKind {
|
||||||
Tab,
|
Tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum LineEnding {
|
|
||||||
Unix,
|
|
||||||
Windows,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct SelectionSet {
|
struct SelectionSet {
|
||||||
line_mode: bool,
|
line_mode: bool,
|
||||||
|
@ -280,7 +273,7 @@ pub struct Chunk<'a> {
|
||||||
pub is_unnecessary: bool,
|
pub is_unnecessary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Diff {
|
pub struct Diff {
|
||||||
base_version: clock::Global,
|
base_version: clock::Global,
|
||||||
new_text: Arc<str>,
|
new_text: Arc<str>,
|
||||||
changes: Vec<(ChangeTag, usize)>,
|
changes: Vec<(ChangeTag, usize)>,
|
||||||
|
@ -314,32 +307,26 @@ impl CharKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
pub fn new<T: Into<Arc<str>>>(
|
pub fn new<T: Into<String>>(
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
base_text: T,
|
base_text: T,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let history = History::new(base_text.into());
|
|
||||||
let line_ending = LineEnding::detect(&history.base_text);
|
|
||||||
Self::build(
|
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,
|
None,
|
||||||
line_ending,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_file<T: Into<Arc<str>>>(
|
pub fn from_file<T: Into<String>>(
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
base_text: T,
|
base_text: T,
|
||||||
file: Arc<dyn File>,
|
file: Arc<dyn File>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let history = History::new(base_text.into());
|
|
||||||
let line_ending = LineEnding::detect(&history.base_text);
|
|
||||||
Self::build(
|
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),
|
Some(file),
|
||||||
line_ending,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,14 +336,12 @@ impl Buffer {
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let buffer = TextBuffer::new(
|
let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
|
||||||
replica_id,
|
let mut this = Self::build(buffer, file);
|
||||||
message.id,
|
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||||
History::new(Arc::from(message.base_text)),
|
proto::LineEnding::from_i32(message.line_ending)
|
||||||
);
|
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||||
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 ops = message
|
let ops = message
|
||||||
.operations
|
.operations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -421,7 +406,7 @@ impl Buffer {
|
||||||
diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
|
diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
|
||||||
diagnostics_timestamp: self.diagnostics_timestamp.value,
|
diagnostics_timestamp: self.diagnostics_timestamp.value,
|
||||||
completion_triggers: self.completion_triggers.clone(),
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>, line_ending: LineEnding) -> Self {
|
fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>) -> Self {
|
||||||
let saved_mtime;
|
let saved_mtime;
|
||||||
if let Some(file) = file.as_ref() {
|
if let Some(file) = file.as_ref() {
|
||||||
saved_mtime = file.mtime();
|
saved_mtime = file.mtime();
|
||||||
|
@ -446,7 +431,6 @@ impl Buffer {
|
||||||
was_dirty_before_starting_transaction: None,
|
was_dirty_before_starting_transaction: None,
|
||||||
text: buffer,
|
text: buffer,
|
||||||
file,
|
file,
|
||||||
line_ending,
|
|
||||||
syntax_tree: Mutex::new(None),
|
syntax_tree: Mutex::new(None),
|
||||||
parsing_in_background: false,
|
parsing_in_background: false,
|
||||||
parse_count: 0,
|
parse_count: 0,
|
||||||
|
@ -507,7 +491,7 @@ impl Buffer {
|
||||||
self.remote_id(),
|
self.remote_id(),
|
||||||
text,
|
text,
|
||||||
version,
|
version,
|
||||||
self.line_ending,
|
self.line_ending(),
|
||||||
cx.as_mut(),
|
cx.as_mut(),
|
||||||
);
|
);
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
@ -563,7 +547,7 @@ impl Buffer {
|
||||||
this.did_reload(
|
this.did_reload(
|
||||||
this.version(),
|
this.version(),
|
||||||
this.as_rope().fingerprint(),
|
this.as_rope().fingerprint(),
|
||||||
this.line_ending,
|
this.line_ending(),
|
||||||
new_mtime,
|
new_mtime,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -588,14 +572,14 @@ impl Buffer {
|
||||||
) {
|
) {
|
||||||
self.saved_version = version;
|
self.saved_version = version;
|
||||||
self.saved_version_fingerprint = fingerprint;
|
self.saved_version_fingerprint = fingerprint;
|
||||||
self.line_ending = line_ending;
|
self.text.set_line_ending(line_ending);
|
||||||
self.saved_mtime = mtime;
|
self.saved_mtime = mtime;
|
||||||
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
|
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
|
||||||
file.buffer_reloaded(
|
file.buffer_reloaded(
|
||||||
self.remote_id(),
|
self.remote_id(),
|
||||||
&self.saved_version,
|
&self.saved_version,
|
||||||
self.saved_version_fingerprint.clone(),
|
self.saved_version_fingerprint.clone(),
|
||||||
self.line_ending,
|
self.line_ending(),
|
||||||
self.saved_mtime,
|
self.saved_mtime,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -974,13 +958,13 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn diff(&self, new_text: String, cx: &AppContext) -> Task<Diff> {
|
pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
|
||||||
let old_text = self.as_rope().clone();
|
let old_text = self.as_rope().clone();
|
||||||
let base_version = self.version();
|
let base_version = self.version();
|
||||||
cx.background().spawn(async move {
|
cx.background().spawn(async move {
|
||||||
let old_text = old_text.to_string();
|
let old_text = old_text.to_string();
|
||||||
let line_ending = LineEnding::detect(&new_text);
|
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())
|
let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str())
|
||||||
.iter_all_changes()
|
.iter_all_changes()
|
||||||
.map(|c| (c.tag(), c.value().len()))
|
.map(|c| (c.tag(), c.value().len()))
|
||||||
|
@ -995,15 +979,11 @@ impl Buffer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_diff(
|
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
|
||||||
&mut self,
|
|
||||||
diff: Diff,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Option<&Transaction> {
|
|
||||||
if self.version == diff.base_version {
|
if self.version == diff.base_version {
|
||||||
self.finalize_last_transaction();
|
self.finalize_last_transaction();
|
||||||
self.start_transaction();
|
self.start_transaction();
|
||||||
self.line_ending = diff.line_ending;
|
self.text.set_line_ending(diff.line_ending);
|
||||||
let mut offset = diff.start_offset;
|
let mut offset = diff.start_offset;
|
||||||
for (tag, len) in diff.changes {
|
for (tag, len) in diff.changes {
|
||||||
let range = offset..(offset + len);
|
let range = offset..(offset + len);
|
||||||
|
@ -1249,7 +1229,8 @@ impl Buffer {
|
||||||
|
|
||||||
let inserted_ranges = edits
|
let inserted_ranges = edits
|
||||||
.into_iter()
|
.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 first_newline_ix = new_text.find('\n')?;
|
||||||
let new_text_len = new_text.len();
|
let new_text_len = new_text.len();
|
||||||
let start = (delta + range.start as isize) as usize + first_newline_ix + 1;
|
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] {
|
pub fn completion_triggers(&self) -> &[String] {
|
||||||
&self.completion_triggers
|
&self.completion_triggers
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_ending(&self) -> LineEnding {
|
|
||||||
self.line_ending
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[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 {
|
impl Completion {
|
||||||
pub fn sort_key(&self) -> (usize, &str) {
|
pub fn sort_key(&self) -> (usize, &str) {
|
||||||
let kind_key = match self.lsp_completion.kind {
|
let kind_key = match self.lsp_completion.kind {
|
||||||
|
|
|
@ -11,6 +11,20 @@ use text::*;
|
||||||
|
|
||||||
pub use proto::{Buffer, BufferState, LineEnding, SelectionSet};
|
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 {
|
pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
||||||
proto::Operation {
|
proto::Operation {
|
||||||
variant: Some(match operation {
|
variant: Some(match operation {
|
||||||
|
|
|
@ -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]
|
#[gpui::test]
|
||||||
fn test_select_language() {
|
fn test_select_language() {
|
||||||
let registry = LanguageRegistry::test();
|
let registry = LanguageRegistry::test();
|
||||||
|
@ -421,7 +444,7 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
|
||||||
async fn search<'a>(
|
async fn search<'a>(
|
||||||
outline: &'a Outline<Anchor>,
|
outline: &'a Outline<Anchor>,
|
||||||
query: &str,
|
query: &str,
|
||||||
cx: &gpui::TestAppContext,
|
cx: &'a gpui::TestAppContext,
|
||||||
) -> Vec<(&'a str, Vec<usize>)> {
|
) -> Vec<(&'a str, Vec<usize>)> {
|
||||||
let matches = cx
|
let matches = cx
|
||||||
.read(|cx| outline.search(query, cx.background().clone()))
|
.read(|cx| outline.search(query, cx.background().clone()))
|
||||||
|
|
|
@ -334,28 +334,6 @@ impl FakeFs {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_dir(&self, path: impl AsRef<Path>) {
|
|
||||||
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<Path>, content: String) {
|
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
@ -392,7 +370,7 @@ impl FakeFs {
|
||||||
|
|
||||||
match tree {
|
match tree {
|
||||||
Object(map) => {
|
Object(map) => {
|
||||||
self.insert_dir(path).await;
|
self.create_dir(path).await.unwrap();
|
||||||
for (name, contents) in map {
|
for (name, contents) in map {
|
||||||
let mut path = PathBuf::from(path);
|
let mut path = PathBuf::from(path);
|
||||||
path.push(name);
|
path.push(name);
|
||||||
|
@ -400,7 +378,7 @@ impl FakeFs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Null => {
|
Null => {
|
||||||
self.insert_dir(&path).await;
|
self.create_dir(&path).await.unwrap();
|
||||||
}
|
}
|
||||||
String(contents) => {
|
String(contents) => {
|
||||||
self.insert_file(&path, contents).await;
|
self.insert_file(&path, contents).await;
|
||||||
|
|
|
@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
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 fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
|
@ -20,7 +20,10 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
point_to_lsp,
|
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,
|
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CharKind, CodeAction, CodeLabel,
|
||||||
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _,
|
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _,
|
||||||
Language, LanguageRegistry, LanguageServerName, LineEnding, LocalFile, LspAdapter,
|
Language, LanguageRegistry, LanguageServerName, LineEnding, LocalFile, LspAdapter,
|
||||||
|
@ -48,10 +51,12 @@ use std::{
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
mem,
|
mem,
|
||||||
|
num::NonZeroU32,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
|
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
|
||||||
path::{Component, Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
str,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -3022,78 +3027,50 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
||||||
let text_document = lsp::TextDocumentIdentifier::new(
|
let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
||||||
lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
|
let settings = cx.global::<Settings>();
|
||||||
);
|
let language_name = buffer.language().map(|language| language.name());
|
||||||
let capabilities = &language_server.capabilities();
|
(
|
||||||
let tab_size = cx.update(|cx| {
|
settings.format_on_save(language_name.as_deref()),
|
||||||
let language_name = buffer.read(cx).language().map(|language| language.name());
|
settings.tab_size(language_name.as_deref()),
|
||||||
cx.global::<Settings>().tab_size(language_name.as_deref())
|
)
|
||||||
});
|
});
|
||||||
let lsp_edits = if capabilities
|
|
||||||
.document_formatting_provider
|
let transaction = match format_on_save {
|
||||||
.as_ref()
|
settings::FormatOnSave::Off => continue,
|
||||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
|
||||||
{
|
&this,
|
||||||
language_server
|
&buffer,
|
||||||
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
|
&buffer_abs_path,
|
||||||
text_document,
|
&language_server,
|
||||||
options: lsp::FormattingOptions {
|
tab_size,
|
||||||
tab_size: tab_size.into(),
|
&mut cx,
|
||||||
insert_spaces: true,
|
)
|
||||||
insert_final_newline: Some(true),
|
.await
|
||||||
..Default::default()
|
.context("failed to format via language server")?,
|
||||||
},
|
settings::FormatOnSave::External { command, arguments } => {
|
||||||
work_done_progress_params: Default::default(),
|
Self::format_via_external_command(
|
||||||
})
|
&buffer,
|
||||||
.await?
|
&buffer_abs_path,
|
||||||
} else if capabilities
|
&command,
|
||||||
.document_range_formatting_provider
|
&arguments,
|
||||||
.as_ref()
|
&mut cx,
|
||||||
.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::request::RangeFormatting>(
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await?
|
.await
|
||||||
} else {
|
.context(format!(
|
||||||
continue;
|
"failed to format via external command {:?}",
|
||||||
|
command
|
||||||
|
))?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(lsp_edits) = lsp_edits {
|
if let Some(transaction) = transaction {
|
||||||
let edits = this
|
if !push_to_history {
|
||||||
.update(&mut cx, |this, cx| {
|
buffer.update(&mut cx, |buffer, _| {
|
||||||
this.edits_from_lsp(&buffer, lsp_edits, None, cx)
|
buffer.forget_transaction(transaction.id)
|
||||||
})
|
});
|
||||||
.await?;
|
}
|
||||||
buffer.update(&mut cx, |buffer, cx| {
|
project_transaction.0.insert(buffer, transaction);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3101,6 +3078,141 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn format_via_lsp(
|
||||||
|
this: &ModelHandle<Self>,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
abs_path: &Path,
|
||||||
|
language_server: &Arc<LanguageServer>,
|
||||||
|
tab_size: NonZeroU32,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<Option<Transaction>> {
|
||||||
|
let text_document =
|
||||||
|
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap());
|
||||||
|
let capabilities = &language_server.capabilities();
|
||||||
|
let lsp_edits = if capabilities
|
||||||
|
.document_formatting_provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||||
|
{
|
||||||
|
language_server
|
||||||
|
.request::<lsp::request::Formatting>(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::request::RangeFormatting>(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(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(lsp_edits) = lsp_edits {
|
||||||
|
let edits = this
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.edits_from_lsp(&buffer, lsp_edits, None, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
buffer.update(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();
|
||||||
|
Ok(Some(transaction))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn format_via_external_command(
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
buffer_abs_path: &Path,
|
||||||
|
command: &str,
|
||||||
|
arguments: &[String],
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<Option<Transaction>> {
|
||||||
|
let working_dir_path = buffer.read_with(cx, |buffer, cx| {
|
||||||
|
let file = File::from_dyn(buffer.file())?;
|
||||||
|
let worktree = file.worktree.read(cx).as_local()?;
|
||||||
|
let mut worktree_path = worktree.abs_path().to_path_buf();
|
||||||
|
if worktree.root_entry()?.is_file() {
|
||||||
|
worktree_path.pop();
|
||||||
|
}
|
||||||
|
Some(worktree_path)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(working_dir_path) = working_dir_path {
|
||||||
|
let mut child =
|
||||||
|
smol::process::Command::new(command)
|
||||||
|
.args(arguments.iter().map(|arg| {
|
||||||
|
arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
|
||||||
|
}))
|
||||||
|
.current_dir(&working_dir_path)
|
||||||
|
.stdin(smol::process::Stdio::piped())
|
||||||
|
.stdout(smol::process::Stdio::piped())
|
||||||
|
.stderr(smol::process::Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
let stdin = child
|
||||||
|
.stdin
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| anyhow!("failed to acquire stdin"))?;
|
||||||
|
let text = buffer.read_with(cx, |buffer, _| buffer.as_rope().clone());
|
||||||
|
for chunk in text.chunks() {
|
||||||
|
stdin.write_all(chunk.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
stdin.flush().await?;
|
||||||
|
|
||||||
|
let output = child.output().await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"command failed with exit code {:?}:\nstdout: {}\nstderr: {}",
|
||||||
|
output.status.code(),
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
|
let diff = buffer
|
||||||
|
.read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
|
||||||
|
.await;
|
||||||
|
Ok(buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx).cloned()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn definition<T: ToPointUtf16>(
|
pub fn definition<T: ToPointUtf16>(
|
||||||
&self,
|
&self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
@ -3379,7 +3491,8 @@ impl Project {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (old_range, new_text) = match lsp_completion.text_edit.as_ref() {
|
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref()
|
||||||
|
{
|
||||||
// If the language server provides a range to overwrite, then
|
// If the language server provides a range to overwrite, then
|
||||||
// check that the range is valid.
|
// check that the range is valid.
|
||||||
Some(lsp::CompletionTextEdit::Edit(edit)) => {
|
Some(lsp::CompletionTextEdit::Edit(edit)) => {
|
||||||
|
@ -3429,6 +3542,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LineEnding::normalize(&mut new_text);
|
||||||
Some(Completion {
|
Some(Completion {
|
||||||
old_range,
|
old_range,
|
||||||
new_text,
|
new_text,
|
||||||
|
@ -5542,7 +5656,7 @@ impl Project {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let payload = envelope.payload;
|
let payload = envelope.payload;
|
||||||
let version = deserialize_version(payload.version);
|
let version = deserialize_version(payload.version);
|
||||||
let line_ending = LineEnding::from_proto(
|
let line_ending = deserialize_line_ending(
|
||||||
proto::LineEnding::from_i32(payload.line_ending)
|
proto::LineEnding::from_i32(payload.line_ending)
|
||||||
.ok_or_else(|| anyhow!("missing line ending"))?,
|
.ok_or_else(|| anyhow!("missing line ending"))?,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1850,6 +1850,59 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "TypeScript".into(),
|
||||||
|
path_suffixes: vec!["ts".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_typescript::language_typescript()),
|
||||||
|
);
|
||||||
|
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"a.ts": "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||||
|
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let fake_server = fake_language_servers.next().await.unwrap();
|
||||||
|
|
||||||
|
let text = "let a = b.fqn";
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||||
|
let completions = project.update(cx, |project, cx| {
|
||||||
|
project.completions(&buffer, text.len(), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
fake_server
|
||||||
|
.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
|
||||||
|
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||||
|
lsp::CompletionItem {
|
||||||
|
label: "fullyQualifiedName?".into(),
|
||||||
|
insert_text: Some("fully\rQualified\r\nName".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
])))
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.await;
|
||||||
|
let completions = completions.await.unwrap();
|
||||||
|
assert_eq!(completions.len(), 1);
|
||||||
|
assert_eq!(completions[0].new_text, "fully\nQualified\nName");
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||||
let mut language = Language::new(
|
let mut language = Language::new(
|
||||||
|
|
|
@ -23,7 +23,7 @@ use gpui::{
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
proto::{deserialize_version, serialize_version},
|
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||||
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -1750,7 +1750,7 @@ impl language::LocalFile for File {
|
||||||
version: serialize_version(&version),
|
version: serialize_version(&version),
|
||||||
mtime: Some(mtime.into()),
|
mtime: Some(mtime.into()),
|
||||||
fingerprint,
|
fingerprint,
|
||||||
line_ending: line_ending.to_proto() as i32,
|
line_ending: serialize_line_ending(line_ending) as i32,
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
|
@ -329,6 +329,14 @@ impl Item for ProjectSearchView {
|
||||||
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
|
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
|
||||||
matches!(event, ViewEvent::UpdateTab)
|
matches!(event, ViewEvent::UpdateTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_edit_event(event: &Self::Event) -> bool {
|
||||||
|
if let ViewEvent::EditorEvent(editor_event) = event {
|
||||||
|
Editor::is_edit_event(editor_event)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectSearchView {
|
impl ProjectSearchView {
|
||||||
|
@ -365,8 +373,10 @@ impl ProjectSearchView {
|
||||||
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe_focus(&query_editor, |this, _, _| {
|
cx.observe_focus(&query_editor, |this, _, focused, _| {
|
||||||
this.results_editor_was_focused = false;
|
if focused {
|
||||||
|
this.results_editor_was_focused = false;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -377,8 +387,10 @@ impl ProjectSearchView {
|
||||||
});
|
});
|
||||||
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe_focus(&results_editor, |this, _, _| {
|
cx.observe_focus(&results_editor, |this, _, focused, _| {
|
||||||
this.results_editor_was_focused = true;
|
if focused {
|
||||||
|
this.results_editor_was_focused = true;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.subscribe(&results_editor, |this, _, event, cx| {
|
cx.subscribe(&results_editor, |this, _, event, cx| {
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub struct LanguageSettings {
|
||||||
pub hard_tabs: Option<bool>,
|
pub hard_tabs: Option<bool>,
|
||||||
pub soft_wrap: Option<SoftWrap>,
|
pub soft_wrap: Option<SoftWrap>,
|
||||||
pub preferred_line_length: Option<u32>,
|
pub preferred_line_length: Option<u32>,
|
||||||
pub format_on_save: Option<bool>,
|
pub format_on_save: Option<FormatOnSave>,
|
||||||
pub enable_language_server: Option<bool>,
|
pub enable_language_server: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,17 @@ pub enum SoftWrap {
|
||||||
PreferredLineLength,
|
PreferredLineLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FormatOnSave {
|
||||||
|
Off,
|
||||||
|
LanguageServer,
|
||||||
|
External {
|
||||||
|
command: String,
|
||||||
|
arguments: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Autosave {
|
pub enum Autosave {
|
||||||
|
@ -72,7 +83,7 @@ pub struct SettingsFileContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub vim_mode: Option<bool>,
|
pub vim_mode: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub format_on_save: Option<bool>,
|
pub format_on_save: Option<FormatOnSave>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub autosave: Option<Autosave>,
|
pub autosave: Option<Autosave>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -136,9 +147,9 @@ impl Settings {
|
||||||
.unwrap_or(80)
|
.unwrap_or(80)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_on_save(&self, language: Option<&str>) -> bool {
|
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
|
||||||
self.language_setting(language, |settings| settings.format_on_save)
|
self.language_setting(language, |settings| settings.format_on_save.clone())
|
||||||
.unwrap_or(true)
|
.unwrap_or(FormatOnSave::LanguageServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
|
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
|
||||||
|
@ -215,7 +226,7 @@ impl Settings {
|
||||||
merge(&mut self.autosave, data.autosave);
|
merge(&mut self.autosave, data.autosave);
|
||||||
merge_option(
|
merge_option(
|
||||||
&mut self.language_settings.format_on_save,
|
&mut self.language_settings.format_on_save,
|
||||||
data.format_on_save,
|
data.format_on_save.clone(),
|
||||||
);
|
);
|
||||||
merge_option(
|
merge_option(
|
||||||
&mut self.language_settings.enable_language_server,
|
&mut self.language_settings.enable_language_server,
|
||||||
|
@ -339,7 +350,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
|
fn merge_option<T>(target: &mut Option<T>, value: Option<T>) {
|
||||||
if value.is_some() {
|
if value.is_some() {
|
||||||
*target = value;
|
*target = value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,11 @@ mio-extras = "2.0.6"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
ordered-float = "2.1.1"
|
ordered-float = "2.1.1"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
|
dirs = "4.0.0"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
client = { path = "../client", features = ["test-support"]}
|
||||||
|
project = { path = "../project", features = ["test-support"]}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ use alacritty_terminal::{
|
||||||
tty::{self, setup_env},
|
tty::{self, setup_env},
|
||||||
Term,
|
Term,
|
||||||
};
|
};
|
||||||
|
|
||||||
use color_translation::{get_color_at_index, to_alac_rgb};
|
use color_translation::{get_color_at_index, to_alac_rgb};
|
||||||
|
use dirs::home_dir;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc::{unbounded, UnboundedSender},
|
channel::mpsc::{unbounded, UnboundedSender},
|
||||||
StreamExt,
|
StreamExt,
|
||||||
|
@ -18,7 +18,7 @@ use gpui::{
|
||||||
actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity,
|
actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity,
|
||||||
MutableAppContext, View, ViewContext,
|
MutableAppContext, View, ViewContext,
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectPath};
|
use project::{LocalWorktree, Project, ProjectPath};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||||
|
@ -252,11 +252,12 @@ impl Terminal {
|
||||||
///Create a new Terminal in the current working directory or the user's home directory
|
///Create a new Terminal in the current working directory or the user's home directory
|
||||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
|
|
||||||
let abs_path = project
|
let abs_path = project
|
||||||
.active_entry()
|
.active_entry()
|
||||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||||
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||||
.map(|wt| wt.abs_path().to_path_buf());
|
.and_then(get_working_directory);
|
||||||
|
|
||||||
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
|
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
|
||||||
}
|
}
|
||||||
|
@ -452,18 +453,29 @@ impl Item for Terminal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_working_directory(wt: &LocalWorktree) -> Option<PathBuf> {
|
||||||
|
Some(wt.abs_path().to_path_buf())
|
||||||
|
.filter(|path| path.is_dir())
|
||||||
|
.or_else(|| home_dir())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
use std::{path::Path, sync::atomic::AtomicUsize, time::Duration};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use alacritty_terminal::{grid::GridIterator, term::cell::Cell};
|
use alacritty_terminal::{grid::GridIterator, term::cell::Cell};
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use project::{FakeFs, Fs, RealFs, RemoveOptions, Worktree};
|
||||||
|
|
||||||
///Basic integration test, can we get the terminal to show up, execute a command,
|
///Basic integration test, can we get the terminal to show up, execute a command,
|
||||||
//and produce noticable output?
|
//and produce noticable output?
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_terminal(cx: &mut TestAppContext) {
|
async fn test_terminal(cx: &mut TestAppContext) {
|
||||||
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
|
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
|
||||||
|
cx.set_condition_duration(Duration::from_secs(2));
|
||||||
|
|
||||||
terminal.update(cx, |terminal, cx| {
|
terminal.update(cx, |terminal, cx| {
|
||||||
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
|
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
|
||||||
|
@ -487,4 +499,85 @@ mod tests {
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn single_file_worktree(cx: &mut TestAppContext) {
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
|
let client = client::Client::new(http_client.clone());
|
||||||
|
let fake_fs = FakeFs::new(cx.background().clone());
|
||||||
|
|
||||||
|
let path = Path::new("/file/");
|
||||||
|
fake_fs.insert_file(path, "a".to_string()).await;
|
||||||
|
|
||||||
|
let worktree_handle = Worktree::local(
|
||||||
|
client,
|
||||||
|
path,
|
||||||
|
true,
|
||||||
|
fake_fs,
|
||||||
|
Arc::new(AtomicUsize::new(0)),
|
||||||
|
&mut async_cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
async_cx.update(|cx| {
|
||||||
|
let wt = worktree_handle.read(cx).as_local().unwrap();
|
||||||
|
let wd = get_working_directory(wt);
|
||||||
|
assert!(wd.is_some());
|
||||||
|
let path = wd.unwrap();
|
||||||
|
//This should be the system's working directory, so querying the real file system is probably ok.
|
||||||
|
assert!(path.is_dir());
|
||||||
|
assert_eq!(path, home_dir().unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_worktree_directory(cx: &mut TestAppContext) {
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
|
let client = client::Client::new(http_client.clone());
|
||||||
|
|
||||||
|
let fs = RealFs;
|
||||||
|
let mut test_wd = home_dir().unwrap();
|
||||||
|
test_wd.push("dir");
|
||||||
|
|
||||||
|
fs.create_dir(test_wd.as_path())
|
||||||
|
.await
|
||||||
|
.expect("File could not be created");
|
||||||
|
|
||||||
|
let worktree_handle = Worktree::local(
|
||||||
|
client,
|
||||||
|
test_wd.clone(),
|
||||||
|
true,
|
||||||
|
Arc::new(RealFs),
|
||||||
|
Arc::new(AtomicUsize::new(0)),
|
||||||
|
&mut async_cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
async_cx.update(|cx| {
|
||||||
|
let wt = worktree_handle.read(cx).as_local().unwrap();
|
||||||
|
let wd = get_working_directory(wt);
|
||||||
|
assert!(wd.is_some());
|
||||||
|
let path = wd.unwrap();
|
||||||
|
assert!(path.is_dir());
|
||||||
|
assert_eq!(path, test_wd);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Clean up after ourselves.
|
||||||
|
fs.remove_dir(
|
||||||
|
test_wd.as_path(),
|
||||||
|
RemoveOptions {
|
||||||
|
recursive: false,
|
||||||
|
ignore_if_not_exists: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.expect("Could not remove test directory");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
parking_lot = "0.11"
|
parking_lot = "0.11"
|
||||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||||
rand = { version = "0.8.3", optional = true }
|
rand = { version = "0.8.3", optional = true }
|
||||||
|
regex = "1.5"
|
||||||
smallvec = { version = "1.6", features = ["union"] }
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -22,7 +22,7 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
|
||||||
|
|
||||||
match self.0.gen_range(0..100) {
|
match self.0.gen_range(0..100) {
|
||||||
// whitespace
|
// whitespace
|
||||||
0..=19 => [' ', '\n', '\t'].choose(&mut self.0).copied(),
|
0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.0).copied(),
|
||||||
// two-byte greek letters
|
// two-byte greek letters
|
||||||
20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
|
20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
|
||||||
// // three-byte characters
|
// // three-byte characters
|
||||||
|
|
|
@ -58,19 +58,12 @@ impl Rope {
|
||||||
pub fn push(&mut self, text: &str) {
|
pub fn push(&mut self, text: &str) {
|
||||||
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
||||||
let mut new_chunk = ArrayString::new();
|
let mut new_chunk = ArrayString::new();
|
||||||
let mut chars = text.chars().peekable();
|
for ch in text.chars() {
|
||||||
while let Some(mut ch) = chars.next() {
|
|
||||||
if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE {
|
if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE {
|
||||||
new_chunks.push(Chunk(new_chunk));
|
new_chunks.push(Chunk(new_chunk));
|
||||||
new_chunk = ArrayString::new();
|
new_chunk = ArrayString::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ch == '\r' {
|
|
||||||
ch = '\n';
|
|
||||||
if chars.peek().copied() == Some('\n') {
|
|
||||||
chars.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
new_chunk.push(ch);
|
new_chunk.push(ch);
|
||||||
}
|
}
|
||||||
if !new_chunk.is_empty() {
|
if !new_chunk.is_empty() {
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn init_logger() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit() {
|
fn test_edit() {
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("abc".into()));
|
let mut buffer = Buffer::new(0, 0, "abc".into());
|
||||||
assert_eq!(buffer.text(), "abc");
|
assert_eq!(buffer.text(), "abc");
|
||||||
buffer.edit([(3..3, "def")]);
|
buffer.edit([(3..3, "def")]);
|
||||||
assert_eq!(buffer.text(), "abcdef");
|
assert_eq!(buffer.text(), "abcdef");
|
||||||
|
@ -42,7 +42,9 @@ fn test_random_edits(mut rng: StdRng) {
|
||||||
let mut reference_string = RandomCharIter::new(&mut rng)
|
let mut reference_string = RandomCharIter::new(&mut rng)
|
||||||
.take(reference_string_len)
|
.take(reference_string_len)
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
let mut buffer = Buffer::new(0, 0, History::new(reference_string.clone().into()));
|
let mut buffer = Buffer::new(0, 0, reference_string.clone().into());
|
||||||
|
LineEnding::normalize(&mut reference_string);
|
||||||
|
|
||||||
buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
|
buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
|
||||||
let mut buffer_versions = Vec::new();
|
let mut buffer_versions = Vec::new();
|
||||||
log::info!(
|
log::info!(
|
||||||
|
@ -56,6 +58,7 @@ fn test_random_edits(mut rng: StdRng) {
|
||||||
for (old_range, new_text) in edits.iter().rev() {
|
for (old_range, new_text) in edits.iter().rev() {
|
||||||
reference_string.replace_range(old_range.clone(), &new_text);
|
reference_string.replace_range(old_range.clone(), &new_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(buffer.text(), reference_string);
|
assert_eq!(buffer.text(), reference_string);
|
||||||
log::info!(
|
log::info!(
|
||||||
"buffer text {:?}, version: {:?}",
|
"buffer text {:?}, version: {:?}",
|
||||||
|
@ -148,9 +151,34 @@ fn test_random_edits(mut rng: StdRng) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_endings() {
|
||||||
|
assert_eq!(LineEnding::detect(&"🍐✅\n".repeat(1000)), LineEnding::Unix);
|
||||||
|
assert_eq!(LineEnding::detect(&"abcd\n".repeat(1000)), LineEnding::Unix);
|
||||||
|
assert_eq!(
|
||||||
|
LineEnding::detect(&"🍐✅\r\n".repeat(1000)),
|
||||||
|
LineEnding::Windows
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
LineEnding::detect(&"abcd\r\n".repeat(1000)),
|
||||||
|
LineEnding::Windows
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new(0, 0, "one\r\ntwo\rthree".into());
|
||||||
|
assert_eq!(buffer.text(), "one\ntwo\nthree");
|
||||||
|
assert_eq!(buffer.line_ending(), LineEnding::Windows);
|
||||||
|
buffer.check_invariants();
|
||||||
|
|
||||||
|
buffer.edit([(buffer.len()..buffer.len(), "\r\nfour")]);
|
||||||
|
buffer.edit([(0..0, "zero\r\n")]);
|
||||||
|
assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour");
|
||||||
|
assert_eq!(buffer.line_ending(), LineEnding::Windows);
|
||||||
|
buffer.check_invariants();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_line_len() {
|
fn test_line_len() {
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("".into()));
|
let mut buffer = Buffer::new(0, 0, "".into());
|
||||||
buffer.edit([(0..0, "abcd\nefg\nhij")]);
|
buffer.edit([(0..0, "abcd\nefg\nhij")]);
|
||||||
buffer.edit([(12..12, "kl\nmno")]);
|
buffer.edit([(12..12, "kl\nmno")]);
|
||||||
buffer.edit([(18..18, "\npqrs\n")]);
|
buffer.edit([(18..18, "\npqrs\n")]);
|
||||||
|
@ -167,7 +195,7 @@ fn test_line_len() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_common_prefix_at_positionn() {
|
fn test_common_prefix_at_positionn() {
|
||||||
let text = "a = str; b = δα";
|
let text = "a = str; b = δα";
|
||||||
let buffer = Buffer::new(0, 0, History::new(text.into()));
|
let buffer = Buffer::new(0, 0, text.into());
|
||||||
|
|
||||||
let offset1 = offset_after(text, "str");
|
let offset1 = offset_after(text, "str");
|
||||||
let offset2 = offset_after(text, "δα");
|
let offset2 = offset_after(text, "δα");
|
||||||
|
@ -215,7 +243,7 @@ fn test_common_prefix_at_positionn() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_summary_for_range() {
|
fn test_text_summary_for_range() {
|
||||||
let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into()));
|
let buffer = Buffer::new(0, 0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz".into());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.text_summary_for_range::<TextSummary, _>(1..3),
|
buffer.text_summary_for_range::<TextSummary, _>(1..3),
|
||||||
TextSummary {
|
TextSummary {
|
||||||
|
@ -280,7 +308,7 @@ fn test_text_summary_for_range() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chars_at() {
|
fn test_chars_at() {
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("".into()));
|
let mut buffer = Buffer::new(0, 0, "".into());
|
||||||
buffer.edit([(0..0, "abcd\nefgh\nij")]);
|
buffer.edit([(0..0, "abcd\nefgh\nij")]);
|
||||||
buffer.edit([(12..12, "kl\nmno")]);
|
buffer.edit([(12..12, "kl\nmno")]);
|
||||||
buffer.edit([(18..18, "\npqrs")]);
|
buffer.edit([(18..18, "\npqrs")]);
|
||||||
|
@ -302,7 +330,7 @@ fn test_chars_at() {
|
||||||
assert_eq!(chars.collect::<String>(), "PQrs");
|
assert_eq!(chars.collect::<String>(), "PQrs");
|
||||||
|
|
||||||
// Regression test:
|
// Regression test:
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("".into()));
|
let mut buffer = Buffer::new(0, 0, "".into());
|
||||||
buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]);
|
buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]);
|
||||||
buffer.edit([(60..60, "\n")]);
|
buffer.edit([(60..60, "\n")]);
|
||||||
|
|
||||||
|
@ -312,7 +340,7 @@ fn test_chars_at() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_anchors() {
|
fn test_anchors() {
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("".into()));
|
let mut buffer = Buffer::new(0, 0, "".into());
|
||||||
buffer.edit([(0..0, "abc")]);
|
buffer.edit([(0..0, "abc")]);
|
||||||
let left_anchor = buffer.anchor_before(2);
|
let left_anchor = buffer.anchor_before(2);
|
||||||
let right_anchor = buffer.anchor_after(2);
|
let right_anchor = buffer.anchor_after(2);
|
||||||
|
@ -430,7 +458,7 @@ fn test_anchors() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_anchors_at_start_and_end() {
|
fn test_anchors_at_start_and_end() {
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("".into()));
|
let mut buffer = Buffer::new(0, 0, "".into());
|
||||||
let before_start_anchor = buffer.anchor_before(0);
|
let before_start_anchor = buffer.anchor_before(0);
|
||||||
let after_end_anchor = buffer.anchor_after(0);
|
let after_end_anchor = buffer.anchor_after(0);
|
||||||
|
|
||||||
|
@ -453,7 +481,7 @@ fn test_anchors_at_start_and_end() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_undo_redo() {
|
fn test_undo_redo() {
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("1234".into()));
|
let mut buffer = Buffer::new(0, 0, "1234".into());
|
||||||
// Set group interval to zero so as to not group edits in the undo stack.
|
// Set group interval to zero so as to not group edits in the undo stack.
|
||||||
buffer.history.group_interval = Duration::from_secs(0);
|
buffer.history.group_interval = Duration::from_secs(0);
|
||||||
|
|
||||||
|
@ -490,7 +518,7 @@ fn test_undo_redo() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_history() {
|
fn test_history() {
|
||||||
let mut now = Instant::now();
|
let mut now = Instant::now();
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
|
let mut buffer = Buffer::new(0, 0, "123456".into());
|
||||||
|
|
||||||
buffer.start_transaction_at(now);
|
buffer.start_transaction_at(now);
|
||||||
buffer.edit([(2..4, "cd")]);
|
buffer.edit([(2..4, "cd")]);
|
||||||
|
@ -544,7 +572,7 @@ fn test_history() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_finalize_last_transaction() {
|
fn test_finalize_last_transaction() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
|
let mut buffer = Buffer::new(0, 0, "123456".into());
|
||||||
|
|
||||||
buffer.start_transaction_at(now);
|
buffer.start_transaction_at(now);
|
||||||
buffer.edit([(2..4, "cd")]);
|
buffer.edit([(2..4, "cd")]);
|
||||||
|
@ -579,7 +607,7 @@ fn test_finalize_last_transaction() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edited_ranges_for_transaction() {
|
fn test_edited_ranges_for_transaction() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut buffer = Buffer::new(0, 0, History::new("1234567".into()));
|
let mut buffer = Buffer::new(0, 0, "1234567".into());
|
||||||
|
|
||||||
buffer.start_transaction_at(now);
|
buffer.start_transaction_at(now);
|
||||||
buffer.edit([(2..4, "cd")]);
|
buffer.edit([(2..4, "cd")]);
|
||||||
|
@ -618,9 +646,9 @@ fn test_edited_ranges_for_transaction() {
|
||||||
fn test_concurrent_edits() {
|
fn test_concurrent_edits() {
|
||||||
let text = "abcdef";
|
let text = "abcdef";
|
||||||
|
|
||||||
let mut buffer1 = Buffer::new(1, 0, History::new(text.into()));
|
let mut buffer1 = Buffer::new(1, 0, text.into());
|
||||||
let mut buffer2 = Buffer::new(2, 0, History::new(text.into()));
|
let mut buffer2 = Buffer::new(2, 0, text.into());
|
||||||
let mut buffer3 = Buffer::new(3, 0, History::new(text.into()));
|
let mut buffer3 = Buffer::new(3, 0, text.into());
|
||||||
|
|
||||||
let buf1_op = buffer1.edit([(1..2, "12")]);
|
let buf1_op = buffer1.edit([(1..2, "12")]);
|
||||||
assert_eq!(buffer1.text(), "a12cdef");
|
assert_eq!(buffer1.text(), "a12cdef");
|
||||||
|
@ -659,7 +687,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
|
||||||
let mut network = Network::new(rng.clone());
|
let mut network = Network::new(rng.clone());
|
||||||
|
|
||||||
for i in 0..peers {
|
for i in 0..peers {
|
||||||
let mut buffer = Buffer::new(i as ReplicaId, 0, History::new(base_text.clone().into()));
|
let mut buffer = Buffer::new(i as ReplicaId, 0, base_text.clone().into());
|
||||||
buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
|
buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
|
||||||
buffers.push(buffer);
|
buffers.push(buffer);
|
||||||
replica_ids.push(i as u16);
|
replica_ids.push(i as u16);
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub use anchor::*;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use locator::Locator;
|
use locator::Locator;
|
||||||
use operation_queue::OperationQueue;
|
use operation_queue::OperationQueue;
|
||||||
pub use patch::Patch;
|
pub use patch::Patch;
|
||||||
|
@ -26,10 +27,12 @@ pub use point_utf16::*;
|
||||||
use postage::{barrier, oneshot, prelude::*};
|
use postage::{barrier, oneshot, prelude::*};
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use random_char_iter::*;
|
pub use random_char_iter::*;
|
||||||
|
use regex::Regex;
|
||||||
use rope::TextDimension;
|
use rope::TextDimension;
|
||||||
pub use rope::{Chunks, Rope, TextSummary};
|
pub use rope::{Chunks, Rope, TextSummary};
|
||||||
pub use selection::*;
|
pub use selection::*;
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
future::Future,
|
future::Future,
|
||||||
iter::Iterator,
|
iter::Iterator,
|
||||||
|
@ -42,6 +45,10 @@ pub use subscription::*;
|
||||||
pub use sum_tree::Bias;
|
pub use sum_tree::Bias;
|
||||||
use sum_tree::{FilterCursor, SumTree};
|
use sum_tree::{FilterCursor, SumTree};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub type TransactionId = clock::Local;
|
pub type TransactionId = clock::Local;
|
||||||
|
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
|
@ -63,6 +70,7 @@ pub struct BufferSnapshot {
|
||||||
remote_id: u64,
|
remote_id: u64,
|
||||||
visible_text: Rope,
|
visible_text: Rope,
|
||||||
deleted_text: Rope,
|
deleted_text: Rope,
|
||||||
|
line_ending: LineEnding,
|
||||||
undo_map: UndoMap,
|
undo_map: UndoMap,
|
||||||
fragments: SumTree<Fragment>,
|
fragments: SumTree<Fragment>,
|
||||||
insertions: SumTree<InsertionFragment>,
|
insertions: SumTree<InsertionFragment>,
|
||||||
|
@ -86,6 +94,12 @@ pub struct Transaction {
|
||||||
pub ranges: Vec<Range<FullOffset>>,
|
pub ranges: Vec<Range<FullOffset>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum LineEnding {
|
||||||
|
Unix,
|
||||||
|
Windows,
|
||||||
|
}
|
||||||
|
|
||||||
impl HistoryEntry {
|
impl HistoryEntry {
|
||||||
pub fn transaction_id(&self) -> TransactionId {
|
pub fn transaction_id(&self) -> TransactionId {
|
||||||
self.transaction.id
|
self.transaction.id
|
||||||
|
@ -148,9 +162,9 @@ impl HistoryEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct History {
|
struct History {
|
||||||
// TODO: Turn this into a String or Rope, maybe.
|
// TODO: Turn this into a String or Rope, maybe.
|
||||||
pub base_text: Arc<str>,
|
base_text: Arc<str>,
|
||||||
operations: HashMap<clock::Local, Operation>,
|
operations: HashMap<clock::Local, Operation>,
|
||||||
undo_stack: Vec<HistoryEntry>,
|
undo_stack: Vec<HistoryEntry>,
|
||||||
redo_stack: Vec<HistoryEntry>,
|
redo_stack: Vec<HistoryEntry>,
|
||||||
|
@ -539,13 +553,18 @@ pub struct UndoOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
pub fn new(replica_id: u16, remote_id: u64, history: History) -> Buffer {
|
pub fn new(replica_id: u16, remote_id: u64, mut base_text: String) -> Buffer {
|
||||||
|
let line_ending = LineEnding::detect(&base_text);
|
||||||
|
LineEnding::normalize(&mut base_text);
|
||||||
|
|
||||||
|
let history = History::new(base_text.into());
|
||||||
let mut fragments = SumTree::new();
|
let mut fragments = SumTree::new();
|
||||||
let mut insertions = SumTree::new();
|
let mut insertions = SumTree::new();
|
||||||
|
|
||||||
let mut local_clock = clock::Local::new(replica_id);
|
let mut local_clock = clock::Local::new(replica_id);
|
||||||
let mut lamport_clock = clock::Lamport::new(replica_id);
|
let mut lamport_clock = clock::Lamport::new(replica_id);
|
||||||
let mut version = clock::Global::new();
|
let mut version = clock::Global::new();
|
||||||
|
|
||||||
let visible_text = Rope::from(history.base_text.as_ref());
|
let visible_text = Rope::from(history.base_text.as_ref());
|
||||||
if visible_text.len() > 0 {
|
if visible_text.len() > 0 {
|
||||||
let insertion_timestamp = InsertionTimestamp {
|
let insertion_timestamp = InsertionTimestamp {
|
||||||
|
@ -576,6 +595,7 @@ impl Buffer {
|
||||||
remote_id,
|
remote_id,
|
||||||
visible_text,
|
visible_text,
|
||||||
deleted_text: Rope::new(),
|
deleted_text: Rope::new(),
|
||||||
|
line_ending,
|
||||||
fragments,
|
fragments,
|
||||||
insertions,
|
insertions,
|
||||||
version,
|
version,
|
||||||
|
@ -658,7 +678,7 @@ impl Buffer {
|
||||||
let mut new_insertions = Vec::new();
|
let mut new_insertions = Vec::new();
|
||||||
let mut insertion_offset = 0;
|
let mut insertion_offset = 0;
|
||||||
|
|
||||||
let mut ranges = edits
|
let mut edits = edits
|
||||||
.map(|(range, new_text)| (range.to_offset(&*self), new_text))
|
.map(|(range, new_text)| (range.to_offset(&*self), new_text))
|
||||||
.peekable();
|
.peekable();
|
||||||
|
|
||||||
|
@ -666,12 +686,12 @@ impl Buffer {
|
||||||
RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
|
RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
|
||||||
let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>();
|
let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>();
|
||||||
let mut new_fragments =
|
let mut new_fragments =
|
||||||
old_fragments.slice(&ranges.peek().unwrap().0.start, Bias::Right, &None);
|
old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right, &None);
|
||||||
new_ropes.push_tree(new_fragments.summary().text);
|
new_ropes.push_tree(new_fragments.summary().text);
|
||||||
|
|
||||||
let mut fragment_start = old_fragments.start().visible;
|
let mut fragment_start = old_fragments.start().visible;
|
||||||
for (range, new_text) in ranges {
|
for (range, new_text) in edits {
|
||||||
let new_text = new_text.into();
|
let new_text = LineEnding::normalize_arc(new_text.into());
|
||||||
let fragment_end = old_fragments.end(&None).visible;
|
let fragment_end = old_fragments.end(&None).visible;
|
||||||
|
|
||||||
// If the current fragment ends before this range, then jump ahead to the first fragment
|
// If the current fragment ends before this range, then jump ahead to the first fragment
|
||||||
|
@ -714,6 +734,7 @@ impl Buffer {
|
||||||
// Insert the new text before any existing fragments within the range.
|
// Insert the new text before any existing fragments within the range.
|
||||||
if !new_text.is_empty() {
|
if !new_text.is_empty() {
|
||||||
let new_start = new_fragments.summary().text.visible;
|
let new_start = new_fragments.summary().text.visible;
|
||||||
|
|
||||||
edits_patch.push(Edit {
|
edits_patch.push(Edit {
|
||||||
old: fragment_start..fragment_start,
|
old: fragment_start..fragment_start,
|
||||||
new: new_start..new_start + new_text.len(),
|
new: new_start..new_start + new_text.len(),
|
||||||
|
@ -805,6 +826,10 @@ impl Buffer {
|
||||||
edit_op
|
edit_op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_line_ending(&mut self, line_ending: LineEnding) {
|
||||||
|
self.snapshot.line_ending = line_ending;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_ops<I: IntoIterator<Item = Operation>>(&mut self, ops: I) -> Result<()> {
|
pub fn apply_ops<I: IntoIterator<Item = Operation>>(&mut self, ops: I) -> Result<()> {
|
||||||
let mut deferred_ops = Vec::new();
|
let mut deferred_ops = Vec::new();
|
||||||
for op in ops {
|
for op in ops {
|
||||||
|
@ -1412,6 +1437,8 @@ impl Buffer {
|
||||||
fragment_summary.text.deleted,
|
fragment_summary.text.deleted,
|
||||||
self.snapshot.deleted_text.len()
|
self.snapshot.deleted_text.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert!(!self.text().contains("\r\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_group_interval(&mut self, group_interval: Duration) {
|
pub fn set_group_interval(&mut self, group_interval: Duration) {
|
||||||
|
@ -1452,6 +1479,15 @@ impl Buffer {
|
||||||
|
|
||||||
log::info!("mutating buffer {} with {:?}", self.replica_id, edits);
|
log::info!("mutating buffer {} with {:?}", self.replica_id, edits);
|
||||||
let op = self.edit(edits.iter().cloned());
|
let op = self.edit(edits.iter().cloned());
|
||||||
|
if let Operation::Edit(edit) = &op {
|
||||||
|
assert_eq!(edits.len(), edit.new_text.len());
|
||||||
|
for (edit, new_text) in edits.iter_mut().zip(&edit.new_text) {
|
||||||
|
edit.1 = new_text.clone();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
(edits, op)
|
(edits, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1549,6 +1585,10 @@ impl BufferSnapshot {
|
||||||
self.visible_text.to_string()
|
self.visible_text.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn line_ending(&self) -> LineEnding {
|
||||||
|
self.line_ending
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deleted_text(&self) -> String {
|
pub fn deleted_text(&self) -> String {
|
||||||
self.deleted_text.to_string()
|
self.deleted_text.to_string()
|
||||||
}
|
}
|
||||||
|
@ -2310,6 +2350,56 @@ impl operation_queue::Operation for Operation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for LineEnding {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(unix)]
|
||||||
|
return Self::Unix;
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
return Self::CRLF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineEnding {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LineEnding::Unix => "\n",
|
||||||
|
LineEnding::Windows => "\r\n",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(text: &str) -> Self {
|
||||||
|
let mut max_ix = cmp::min(text.len(), 1000);
|
||||||
|
while !text.is_char_boundary(max_ix) {
|
||||||
|
max_ix -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ix) = text[..max_ix].find(&['\n']) {
|
||||||
|
if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
|
||||||
|
Self::Windows
|
||||||
|
} else {
|
||||||
|
Self::Unix
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize(text: &mut String) {
|
||||||
|
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
|
||||||
|
*text = replaced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
||||||
|
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
|
||||||
|
replaced.into()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ToOffset {
|
pub trait ToOffset {
|
||||||
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize;
|
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,7 @@ pub struct Toolbar {
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub item_spacing: f32,
|
pub item_spacing: f32,
|
||||||
|
pub nav_button: Interactive<IconButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
@ -509,28 +510,23 @@ pub struct Interactive<T> {
|
||||||
pub default: T,
|
pub default: T,
|
||||||
pub hover: Option<T>,
|
pub hover: Option<T>,
|
||||||
pub active: Option<T>,
|
pub active: Option<T>,
|
||||||
pub active_hover: Option<T>,
|
pub disabled: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Interactive<T> {
|
impl<T> Interactive<T> {
|
||||||
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
||||||
if active {
|
if active {
|
||||||
if state.hovered {
|
self.active.as_ref().unwrap_or(&self.default)
|
||||||
self.active_hover
|
} else if state.hovered {
|
||||||
.as_ref()
|
self.hover.as_ref().unwrap_or(&self.default)
|
||||||
.or(self.active.as_ref())
|
|
||||||
.unwrap_or(&self.default)
|
|
||||||
} else {
|
|
||||||
self.active.as_ref().unwrap_or(&self.default)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if state.hovered {
|
&self.default
|
||||||
self.hover.as_ref().unwrap_or(&self.default)
|
|
||||||
} else {
|
|
||||||
&self.default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disabled_style(&self) -> &T {
|
||||||
|
self.disabled.as_ref().unwrap_or(&self.default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
|
@ -544,7 +540,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
default: Value,
|
default: Value,
|
||||||
hover: Option<Value>,
|
hover: Option<Value>,
|
||||||
active: Option<Value>,
|
active: Option<Value>,
|
||||||
active_hover: Option<Value>,
|
disabled: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = Helper::deserialize(deserializer)?;
|
let json = Helper::deserialize(deserializer)?;
|
||||||
|
@ -570,14 +566,14 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
|
|
||||||
let hover = deserialize_state(json.hover)?;
|
let hover = deserialize_state(json.hover)?;
|
||||||
let active = deserialize_state(json.active)?;
|
let active = deserialize_state(json.active)?;
|
||||||
let active_hover = deserialize_state(json.active_hover)?;
|
let disabled = deserialize_state(json.disabled)?;
|
||||||
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
||||||
|
|
||||||
Ok(Interactive {
|
Ok(Interactive {
|
||||||
default,
|
default,
|
||||||
hover,
|
hover,
|
||||||
active,
|
active,
|
||||||
active_hover,
|
disabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::{Autosave, Settings};
|
||||||
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
@ -136,13 +136,13 @@ pub struct ItemNavHistory {
|
||||||
item: Rc<dyn WeakItemHandle>,
|
item: Rc<dyn WeakItemHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
struct NavHistory {
|
||||||
pub struct NavHistory {
|
|
||||||
mode: NavigationMode,
|
mode: NavigationMode,
|
||||||
backward_stack: VecDeque<NavigationEntry>,
|
backward_stack: VecDeque<NavigationEntry>,
|
||||||
forward_stack: VecDeque<NavigationEntry>,
|
forward_stack: VecDeque<NavigationEntry>,
|
||||||
closed_stack: VecDeque<NavigationEntry>,
|
closed_stack: VecDeque<NavigationEntry>,
|
||||||
paths_by_item: HashMap<usize, ProjectPath>,
|
paths_by_item: HashMap<usize, ProjectPath>,
|
||||||
|
pane: WeakViewHandle<Pane>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
|
@ -168,17 +168,28 @@ pub struct NavigationEntry {
|
||||||
|
|
||||||
impl Pane {
|
impl Pane {
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let handle = cx.weak_handle();
|
||||||
Self {
|
Self {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
active_item_index: 0,
|
active_item_index: 0,
|
||||||
autoscroll: false,
|
autoscroll: false,
|
||||||
nav_history: Default::default(),
|
nav_history: Rc::new(RefCell::new(NavHistory {
|
||||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
mode: NavigationMode::Normal,
|
||||||
|
backward_stack: Default::default(),
|
||||||
|
forward_stack: Default::default(),
|
||||||
|
closed_stack: Default::default(),
|
||||||
|
paths_by_item: Default::default(),
|
||||||
|
pane: handle.clone(),
|
||||||
|
})),
|
||||||
|
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
|
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
|
||||||
&self.nav_history
|
ItemNavHistory {
|
||||||
|
history: self.nav_history.clone(),
|
||||||
|
item: Rc::new(item.downgrade()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -223,6 +234,26 @@ impl Pane {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_history(&mut self) {
|
||||||
|
self.nav_history.borrow_mut().disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_history(&mut self) {
|
||||||
|
self.nav_history.borrow_mut().enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_navigate_backward(&self) -> bool {
|
||||||
|
!self.nav_history.borrow().backward_stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_navigate_forward(&self) -> bool {
|
||||||
|
!self.nav_history.borrow().forward_stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.toolbar.update(cx, |_, cx| cx.notify());
|
||||||
|
}
|
||||||
|
|
||||||
fn navigate_history(
|
fn navigate_history(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
|
@ -234,7 +265,7 @@ impl Pane {
|
||||||
let to_load = pane.update(cx, |pane, cx| {
|
let to_load = pane.update(cx, |pane, cx| {
|
||||||
loop {
|
loop {
|
||||||
// Retrieve the weak item handle from the history.
|
// Retrieve the weak item handle from the history.
|
||||||
let entry = pane.nav_history.borrow_mut().pop(mode)?;
|
let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
|
||||||
|
|
||||||
// If the item is still present in this pane, then activate it.
|
// If the item is still present in this pane, then activate it.
|
||||||
if let Some(index) = entry
|
if let Some(index) = entry
|
||||||
|
@ -367,7 +398,6 @@ impl Pane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
|
||||||
item.added_to_pane(workspace, pane.clone(), cx);
|
item.added_to_pane(workspace, pane.clone(), cx);
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
// If there is already an active item, then insert the new item
|
// If there is already an active item, then insert the new item
|
||||||
|
@ -625,11 +655,16 @@ impl Pane {
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_mode(NavigationMode::Normal);
|
.set_mode(NavigationMode::Normal);
|
||||||
|
|
||||||
let mut nav_history = pane.nav_history().borrow_mut();
|
|
||||||
if let Some(path) = item.project_path(cx) {
|
if let Some(path) = item.project_path(cx) {
|
||||||
nav_history.paths_by_item.insert(item.id(), path);
|
pane.nav_history
|
||||||
|
.borrow_mut()
|
||||||
|
.paths_by_item
|
||||||
|
.insert(item.id(), path);
|
||||||
} else {
|
} else {
|
||||||
nav_history.paths_by_item.remove(&item.id());
|
pane.nav_history
|
||||||
|
.borrow_mut()
|
||||||
|
.paths_by_item
|
||||||
|
.remove(&item.id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -677,7 +712,13 @@ impl Pane {
|
||||||
_ => return Ok(false),
|
_ => return Ok(false),
|
||||||
}
|
}
|
||||||
} else if is_dirty && (can_save || is_singleton) {
|
} else if is_dirty && (can_save || is_singleton) {
|
||||||
let should_save = if should_prompt_for_save {
|
let will_autosave = cx.read(|cx| {
|
||||||
|
matches!(
|
||||||
|
cx.global::<Settings>().autosave,
|
||||||
|
Autosave::OnFocusChange | Autosave::OnWindowChange
|
||||||
|
) && Self::can_autosave_item(item.as_ref(), cx)
|
||||||
|
});
|
||||||
|
let should_save = if should_prompt_for_save && !will_autosave {
|
||||||
let mut answer = pane.update(cx, |pane, cx| {
|
let mut answer = pane.update(cx, |pane, cx| {
|
||||||
pane.activate_item(item_ix, true, true, cx);
|
pane.activate_item(item_ix, true, true, cx);
|
||||||
cx.prompt(
|
cx.prompt(
|
||||||
|
@ -718,6 +759,23 @@ impl Pane {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
|
||||||
|
let is_deleted = item.project_entry_ids(cx).is_empty();
|
||||||
|
item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn autosave_item(
|
||||||
|
item: &dyn ItemHandle,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
if Self::can_autosave_item(item, cx) {
|
||||||
|
item.save(project, cx)
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(active_item) = self.active_item() {
|
if let Some(active_item) = self.active_item() {
|
||||||
cx.focus(active_item);
|
cx.focus(active_item);
|
||||||
|
@ -930,57 +988,56 @@ impl View for Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemNavHistory {
|
impl ItemNavHistory {
|
||||||
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
|
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
|
||||||
Self {
|
self.history.borrow_mut().push(data, self.item.clone(), cx);
|
||||||
history,
|
|
||||||
item: Rc::new(item.downgrade()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn history(&self) -> Rc<RefCell<NavHistory>> {
|
pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||||
self.history.clone()
|
self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
|
pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||||
self.history.borrow_mut().push(data, self.item.clone());
|
self.history
|
||||||
|
.borrow_mut()
|
||||||
|
.pop(NavigationMode::GoingForward, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NavHistory {
|
impl NavHistory {
|
||||||
pub fn disable(&mut self) {
|
|
||||||
self.mode = NavigationMode::Disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.mode = NavigationMode::Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
|
|
||||||
self.backward_stack.pop_back()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
|
|
||||||
self.forward_stack.pop_back()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop_closed(&mut self) -> Option<NavigationEntry> {
|
|
||||||
self.closed_stack.pop_back()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
|
|
||||||
match mode {
|
|
||||||
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => None,
|
|
||||||
NavigationMode::GoingBack => self.pop_backward(),
|
|
||||||
NavigationMode::GoingForward => self.pop_forward(),
|
|
||||||
NavigationMode::ReopeningClosedItem => self.pop_closed(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_mode(&mut self, mode: NavigationMode) {
|
fn set_mode(&mut self, mode: NavigationMode) {
|
||||||
self.mode = mode;
|
self.mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
|
fn disable(&mut self) {
|
||||||
|
self.mode = NavigationMode::Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable(&mut self) {
|
||||||
|
self.mode = NavigationMode::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||||
|
let entry = match mode {
|
||||||
|
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
NavigationMode::GoingBack => &mut self.backward_stack,
|
||||||
|
NavigationMode::GoingForward => &mut self.forward_stack,
|
||||||
|
NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
|
||||||
|
}
|
||||||
|
.pop_back();
|
||||||
|
if entry.is_some() {
|
||||||
|
self.did_update(cx);
|
||||||
|
}
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push<D: 'static + Any>(
|
||||||
|
&mut self,
|
||||||
|
data: Option<D>,
|
||||||
|
item: Rc<dyn WeakItemHandle>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
NavigationMode::Disabled => {}
|
NavigationMode::Disabled => {}
|
||||||
NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
|
NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
|
||||||
|
@ -1021,5 +1078,12 @@ impl NavHistory {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.did_update(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn did_update(&self, cx: &mut MutableAppContext) {
|
||||||
|
if let Some(pane) = self.pane.upgrade(cx) {
|
||||||
|
cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::ItemHandle;
|
use crate::{ItemHandle, Pane};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
|
elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity,
|
||||||
View, ViewContext, ViewHandle,
|
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
|
||||||
|
|
||||||
pub struct Toolbar {
|
pub struct Toolbar {
|
||||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||||
|
pane: WeakViewHandle<Pane>,
|
||||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ impl View for Toolbar {
|
||||||
let mut primary_left_items = Vec::new();
|
let mut primary_left_items = Vec::new();
|
||||||
let mut primary_right_items = Vec::new();
|
let mut primary_right_items = Vec::new();
|
||||||
let mut secondary_item = None;
|
let mut secondary_item = None;
|
||||||
|
let spacing = theme.item_spacing;
|
||||||
|
|
||||||
for (item, position) in &self.items {
|
for (item, position) in &self.items {
|
||||||
match *position {
|
match *position {
|
||||||
|
@ -68,7 +70,7 @@ impl View for Toolbar {
|
||||||
let left_item = ChildView::new(item.as_ref())
|
let left_item = ChildView::new(item.as_ref())
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
.with_margin_right(theme.item_spacing);
|
.with_margin_right(spacing);
|
||||||
if let Some((flex, expanded)) = flex {
|
if let Some((flex, expanded)) = flex {
|
||||||
primary_left_items.push(left_item.flex(flex, expanded).boxed());
|
primary_left_items.push(left_item.flex(flex, expanded).boxed());
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,7 +81,7 @@ impl View for Toolbar {
|
||||||
let right_item = ChildView::new(item.as_ref())
|
let right_item = ChildView::new(item.as_ref())
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
.with_margin_left(theme.item_spacing)
|
.with_margin_left(spacing)
|
||||||
.flex_float();
|
.flex_float();
|
||||||
if let Some((flex, expanded)) = flex {
|
if let Some((flex, expanded)) = flex {
|
||||||
primary_right_items.push(right_item.flex(flex, expanded).boxed());
|
primary_right_items.push(right_item.flex(flex, expanded).boxed());
|
||||||
|
@ -98,26 +100,98 @@ impl View for Toolbar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pane = self.pane.clone();
|
||||||
|
let mut enable_go_backward = false;
|
||||||
|
let mut enable_go_forward = false;
|
||||||
|
if let Some(pane) = pane.upgrade(cx) {
|
||||||
|
let pane = pane.read(cx);
|
||||||
|
enable_go_backward = pane.can_navigate_backward();
|
||||||
|
enable_go_forward = pane.can_navigate_forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
let container_style = theme.container;
|
||||||
|
let height = theme.height;
|
||||||
|
let button_style = theme.nav_button;
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
.with_child(nav_button(
|
||||||
|
"icons/arrow-left.svg",
|
||||||
|
button_style,
|
||||||
|
enable_go_backward,
|
||||||
|
spacing,
|
||||||
|
super::GoBack {
|
||||||
|
pane: Some(pane.clone()),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.with_child(nav_button(
|
||||||
|
"icons/arrow-right.svg",
|
||||||
|
button_style,
|
||||||
|
enable_go_forward,
|
||||||
|
spacing,
|
||||||
|
super::GoForward {
|
||||||
|
pane: Some(pane.clone()),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
))
|
||||||
.with_children(primary_left_items)
|
.with_children(primary_left_items)
|
||||||
.with_children(primary_right_items)
|
.with_children(primary_right_items)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.height)
|
.with_height(height)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_children(secondary_item)
|
.with_children(secondary_item)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.container)
|
.with_style(container_style)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nav_button<A: Action + Clone>(
|
||||||
|
svg_path: &'static str,
|
||||||
|
style: theme::Interactive<theme::IconButton>,
|
||||||
|
enabled: bool,
|
||||||
|
spacing: f32,
|
||||||
|
action: A,
|
||||||
|
cx: &mut RenderContext<Toolbar>,
|
||||||
|
) -> ElementBox {
|
||||||
|
MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
|
||||||
|
let style = if enabled {
|
||||||
|
style.style_for(state, false)
|
||||||
|
} else {
|
||||||
|
style.disabled_style()
|
||||||
|
};
|
||||||
|
Svg::new(svg_path)
|
||||||
|
.with_color(style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(if enabled {
|
||||||
|
CursorStyle::PointingHand
|
||||||
|
} else {
|
||||||
|
CursorStyle::default()
|
||||||
|
})
|
||||||
|
.on_mouse_down(move |_, cx| cx.dispatch_action(action.clone()))
|
||||||
|
.contained()
|
||||||
|
.with_margin_right(spacing)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
impl Toolbar {
|
impl Toolbar {
|
||||||
pub fn new() -> Self {
|
pub fn new(pane: WeakViewHandle<Pane>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
active_pane_item: None,
|
active_pane_item: None,
|
||||||
|
pane,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use client::{
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
|
use futures::{channel::oneshot, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
color::Color,
|
color::Color,
|
||||||
|
@ -30,7 +31,7 @@ pub use pane_group::*;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::{Autosave, Settings};
|
||||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use status_bar::StatusBar;
|
use status_bar::StatusBar;
|
||||||
|
@ -41,12 +42,14 @@ use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
mem,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering::SeqCst},
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||||
|
@ -296,6 +299,9 @@ pub trait Item: View {
|
||||||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
fn is_edit_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn act_as_type(
|
fn act_as_type(
|
||||||
&self,
|
&self,
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
|
@ -408,7 +414,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
|
|
||||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
|
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
|
||||||
fn added_to_pane(
|
fn added_to_pane(
|
||||||
&self,
|
&self,
|
||||||
|
@ -478,12 +483,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
|
|
||||||
self.update(cx, |item, cx| {
|
|
||||||
item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
|
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
|
||||||
self.update(cx, |item, cx| {
|
self.update(cx, |item, cx| {
|
||||||
cx.add_option_view(|cx| item.clone_on_split(cx))
|
cx.add_option_view(|cx| item.clone_on_split(cx))
|
||||||
|
@ -497,6 +496,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
|
let history = pane.read(cx).nav_history_for_item(self);
|
||||||
|
self.update(cx, |this, cx| this.set_nav_history(history, cx));
|
||||||
|
|
||||||
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
||||||
if let Some(message) = followed_item.to_state_proto(cx) {
|
if let Some(message) = followed_item.to_state_proto(cx) {
|
||||||
workspace.update_followers(
|
workspace.update_followers(
|
||||||
|
@ -510,6 +512,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut pending_autosave = None;
|
||||||
|
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
|
||||||
let pending_update = Rc::new(RefCell::new(None));
|
let pending_update = Rc::new(RefCell::new(None));
|
||||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
||||||
let pane = pane.downgrade();
|
let pane = pane.downgrade();
|
||||||
|
@ -570,6 +574,40 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if T::is_edit_event(event) {
|
||||||
|
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().autosave {
|
||||||
|
let prev_autosave = pending_autosave.take().unwrap_or(Task::ready(Some(())));
|
||||||
|
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
|
||||||
|
let prev_cancel_tx = mem::replace(&mut cancel_pending_autosave, cancel_tx);
|
||||||
|
let project = workspace.project.downgrade();
|
||||||
|
let _ = prev_cancel_tx.send(());
|
||||||
|
pending_autosave = Some(cx.spawn_weak(|_, mut cx| async move {
|
||||||
|
let mut timer = cx
|
||||||
|
.background()
|
||||||
|
.timer(Duration::from_millis(milliseconds))
|
||||||
|
.fuse();
|
||||||
|
prev_autosave.await;
|
||||||
|
futures::select_biased! {
|
||||||
|
_ = cancel_rx => return None,
|
||||||
|
_ = timer => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = project.upgrade(&cx)?;
|
||||||
|
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
None
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.observe_focus(self, move |workspace, item, focused, cx| {
|
||||||
|
if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
|
||||||
|
Pane::autosave_item(&item, workspace.project.clone(), cx).detach_and_log_err(cx);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
@ -774,6 +812,8 @@ impl Workspace {
|
||||||
cx.notify()
|
cx.notify()
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.observe_window_activation(Self::on_window_activation_changed)
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.subscribe(&project, move |this, project, event, cx| {
|
cx.subscribe(&project, move |this, project, event, cx| {
|
||||||
match event {
|
match event {
|
||||||
|
@ -2314,6 +2354,24 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
if !active
|
||||||
|
&& matches!(
|
||||||
|
cx.global::<Settings>().autosave,
|
||||||
|
Autosave::OnWindowChange | Autosave::OnFocusChange
|
||||||
|
)
|
||||||
|
{
|
||||||
|
for pane in &self.panes {
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
for item in pane.items() {
|
||||||
|
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Workspace {
|
impl Entity for Workspace {
|
||||||
|
@ -2631,7 +2689,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{ModelHandle, TestAppContext, ViewContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||||
use project::{FakeFs, Project, ProjectEntryId};
|
use project::{FakeFs, Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
@ -2969,21 +3027,219 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[gpui::test]
|
||||||
|
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
|
||||||
|
Settings::test_async(cx);
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||||
|
|
||||||
|
let item = cx.add_view(window_id, |_| {
|
||||||
|
let mut item = TestItem::new();
|
||||||
|
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||||
|
item
|
||||||
|
});
|
||||||
|
let item_id = item.id();
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.add_item(Box::new(item.clone()), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Autosave on window change.
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.autosave = Autosave::OnWindowChange;
|
||||||
|
});
|
||||||
|
item.is_dirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deactivating the window saves the file.
|
||||||
|
cx.simulate_window_activation(None);
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
|
||||||
|
|
||||||
|
// Autosave on focus change.
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
cx.focus_self();
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.autosave = Autosave::OnFocusChange;
|
||||||
|
});
|
||||||
|
item.is_dirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blurring the item saves the file.
|
||||||
|
item.update(cx, |_, cx| cx.blur());
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
|
||||||
|
|
||||||
|
// Deactivating the window still saves the file.
|
||||||
|
cx.simulate_window_activation(Some(window_id));
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
cx.focus_self();
|
||||||
|
item.is_dirty = true;
|
||||||
|
});
|
||||||
|
cx.simulate_window_activation(None);
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
||||||
|
|
||||||
|
// Autosave after delay.
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
|
||||||
|
});
|
||||||
|
item.is_dirty = true;
|
||||||
|
cx.emit(TestItemEvent::Edit);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delay hasn't fully expired, so the file is still dirty and unsaved.
|
||||||
|
deterministic.advance_clock(Duration::from_millis(250));
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
||||||
|
|
||||||
|
// After delay expires, the file is saved.
|
||||||
|
deterministic.advance_clock(Duration::from_millis(250));
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
|
||||||
|
|
||||||
|
// Autosave on focus change, ensuring closing the tab counts as such.
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.autosave = Autosave::OnFocusChange;
|
||||||
|
});
|
||||||
|
item.is_dirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let pane = workspace.active_pane().clone();
|
||||||
|
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(!cx.has_pending_prompt(window_id));
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||||
|
|
||||||
|
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.add_item(Box::new(item.clone()), cx);
|
||||||
|
});
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
item.project_entry_ids = Default::default();
|
||||||
|
item.is_dirty = true;
|
||||||
|
cx.blur();
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
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 = workspace.update(cx, |workspace, cx| {
|
||||||
|
let pane = workspace.active_pane().clone();
|
||||||
|
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
assert!(cx.has_pending_prompt(window_id));
|
||||||
|
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_pane_navigation(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
Settings::test_async(cx);
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||||
|
|
||||||
|
let item = cx.add_view(window_id, |_| {
|
||||||
|
let mut item = TestItem::new();
|
||||||
|
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||||
|
item
|
||||||
|
});
|
||||||
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
|
||||||
|
let toolbar_notify_count = Rc::new(RefCell::new(0));
|
||||||
|
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.add_item(Box::new(item.clone()), cx);
|
||||||
|
let toolbar_notification_count = toolbar_notify_count.clone();
|
||||||
|
cx.observe(&toolbar, move |_, _, _| {
|
||||||
|
*toolbar_notification_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
|
pane.read_with(cx, |pane, _| {
|
||||||
|
assert!(!pane.can_navigate_backward());
|
||||||
|
assert!(!pane.can_navigate_forward());
|
||||||
|
});
|
||||||
|
|
||||||
|
item.update(cx, |item, cx| {
|
||||||
|
item.set_state("one".to_string(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toolbar must be notified to re-render the navigation buttons
|
||||||
|
assert_eq!(*toolbar_notify_count.borrow(), 1);
|
||||||
|
|
||||||
|
pane.read_with(cx, |pane, _| {
|
||||||
|
assert!(pane.can_navigate_backward());
|
||||||
|
assert!(!pane.can_navigate_forward());
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
Pane::go_back(workspace, Some(pane.clone()), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(*toolbar_notify_count.borrow(), 3);
|
||||||
|
pane.read_with(cx, |pane, _| {
|
||||||
|
assert!(!pane.can_navigate_backward());
|
||||||
|
assert!(pane.can_navigate_forward());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
struct TestItem {
|
struct TestItem {
|
||||||
|
state: String,
|
||||||
save_count: usize,
|
save_count: usize,
|
||||||
save_as_count: usize,
|
save_as_count: usize,
|
||||||
reload_count: usize,
|
reload_count: usize,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
|
is_singleton: bool,
|
||||||
has_conflict: bool,
|
has_conflict: bool,
|
||||||
project_entry_ids: Vec<ProjectEntryId>,
|
project_entry_ids: Vec<ProjectEntryId>,
|
||||||
project_path: Option<ProjectPath>,
|
project_path: Option<ProjectPath>,
|
||||||
is_singleton: bool,
|
nav_history: Option<ItemNavHistory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestItemEvent {
|
||||||
|
Edit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for TestItem {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
state: self.state.clone(),
|
||||||
|
save_count: self.save_count,
|
||||||
|
save_as_count: self.save_as_count,
|
||||||
|
reload_count: self.reload_count,
|
||||||
|
is_dirty: self.is_dirty,
|
||||||
|
is_singleton: self.is_singleton,
|
||||||
|
has_conflict: self.has_conflict,
|
||||||
|
project_entry_ids: self.project_entry_ids.clone(),
|
||||||
|
project_path: self.project_path.clone(),
|
||||||
|
nav_history: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestItem {
|
impl TestItem {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
state: String::new(),
|
||||||
save_count: 0,
|
save_count: 0,
|
||||||
save_as_count: 0,
|
save_as_count: 0,
|
||||||
reload_count: 0,
|
reload_count: 0,
|
||||||
|
@ -2992,12 +3248,24 @@ mod tests {
|
||||||
project_entry_ids: Vec::new(),
|
project_entry_ids: Vec::new(),
|
||||||
project_path: None,
|
project_path: None,
|
||||||
is_singleton: true,
|
is_singleton: true,
|
||||||
|
nav_history: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
|
||||||
|
self.push_to_nav_history(cx);
|
||||||
|
self.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(history) = &mut self.nav_history {
|
||||||
|
history.push(Some(Box::new(self.state.clone())), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for TestItem {
|
impl Entity for TestItem {
|
||||||
type Event = ();
|
type Event = TestItemEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for TestItem {
|
impl View for TestItem {
|
||||||
|
@ -3027,7 +3295,23 @@ mod tests {
|
||||||
self.is_singleton
|
self.is_singleton
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||||
|
self.nav_history = Some(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||||
|
let state = *state.downcast::<String>().unwrap_or_default();
|
||||||
|
if state != self.state {
|
||||||
|
self.state = state;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.push_to_nav_history(cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||||
where
|
where
|
||||||
|
@ -3054,6 +3338,7 @@ mod tests {
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.save_count += 1;
|
self.save_count += 1;
|
||||||
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3064,6 +3349,7 @@ mod tests {
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.save_as_count += 1;
|
self.save_as_count += 1;
|
||||||
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3073,11 +3359,16 @@ mod tests {
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.reload_count += 1;
|
self.reload_count += 1;
|
||||||
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_edit_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, TestItemEvent::Edit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.43.0"
|
version = "0.45.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
|
|
|
@ -554,7 +554,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||||
app_state.fs.as_fake().insert_dir("/root").await;
|
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
||||||
save_task.await.unwrap();
|
save_task.await.unwrap();
|
||||||
editor.read_with(cx, |editor, cx| {
|
editor.read_with(cx, |editor, cx| {
|
||||||
|
@ -680,14 +680,25 @@ mod tests {
|
||||||
async fn test_open_paths(cx: &mut TestAppContext) {
|
async fn test_open_paths(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
|
|
||||||
let fs = app_state.fs.as_fake();
|
app_state
|
||||||
fs.insert_dir("/dir1").await;
|
.fs
|
||||||
fs.insert_dir("/dir2").await;
|
.as_fake()
|
||||||
fs.insert_dir("/dir3").await;
|
.insert_tree(
|
||||||
fs.insert_file("/dir1/a.txt", "".into()).await;
|
"/",
|
||||||
fs.insert_file("/dir2/b.txt", "".into()).await;
|
json!({
|
||||||
fs.insert_file("/dir3/c.txt", "".into()).await;
|
"dir1": {
|
||||||
fs.insert_file("/d.txt", "".into()).await;
|
"a.txt": ""
|
||||||
|
},
|
||||||
|
"dir2": {
|
||||||
|
"b.txt": ""
|
||||||
|
},
|
||||||
|
"dir3": {
|
||||||
|
"c.txt": ""
|
||||||
|
},
|
||||||
|
"d.txt": ""
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||||
|
@ -891,7 +902,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
|
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
app_state.fs.as_fake().insert_dir("/root").await;
|
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
|
@ -980,7 +991,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
|
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
app_state.fs.as_fake().insert_dir("/root").await;
|
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
|
|
|
@ -139,6 +139,19 @@ export default function workspace(theme: Theme) {
|
||||||
background: backgroundColor(theme, 500),
|
background: backgroundColor(theme, 500),
|
||||||
border: border(theme, "secondary", { bottom: true }),
|
border: border(theme, "secondary", { bottom: true }),
|
||||||
itemSpacing: 8,
|
itemSpacing: 8,
|
||||||
|
navButton: {
|
||||||
|
color: iconColor(theme, "secondary"),
|
||||||
|
iconWidth: 8,
|
||||||
|
buttonWidth: 18,
|
||||||
|
cornerRadius: 6,
|
||||||
|
hover: {
|
||||||
|
color: iconColor(theme, "active"),
|
||||||
|
background: backgroundColor(theme, 300),
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
color: withOpacity(iconColor(theme, "muted"), 0.6),
|
||||||
|
},
|
||||||
|
},
|
||||||
padding: { left: 16, right: 8, top: 4, bottom: 4 },
|
padding: { left: 16, right: 8, top: 4, bottom: 4 },
|
||||||
},
|
},
|
||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue