Merge pull request #179 from zed-industries/rpc-protocol-version
Forbid connections from outdated clients
This commit is contained in:
commit
286846cafd
9 changed files with 159 additions and 90 deletions
|
@ -894,8 +894,11 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
|
||||||
let connection_upgrade = header_contains_ignore_case(&request, CONNECTION, "upgrade");
|
let connection_upgrade = header_contains_ignore_case(&request, CONNECTION, "upgrade");
|
||||||
let upgrade_to_websocket = header_contains_ignore_case(&request, UPGRADE, "websocket");
|
let upgrade_to_websocket = header_contains_ignore_case(&request, UPGRADE, "websocket");
|
||||||
let upgrade_requested = connection_upgrade && upgrade_to_websocket;
|
let upgrade_requested = connection_upgrade && upgrade_to_websocket;
|
||||||
|
let client_protocol_version: Option<u32> = request
|
||||||
|
.header("X-Zed-Protocol-Version")
|
||||||
|
.and_then(|v| v.as_str().parse().ok());
|
||||||
|
|
||||||
if !upgrade_requested {
|
if !upgrade_requested || client_protocol_version != Some(zrpc::PROTOCOL_VERSION) {
|
||||||
return Ok(Response::new(StatusCode::UpgradeRequired));
|
return Ok(Response::new(StatusCode::UpgradeRequired));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ title = "$text.0"
|
||||||
avatar_width = 20
|
avatar_width = 20
|
||||||
icon_color = "$text.2.color"
|
icon_color = "$text.2.color"
|
||||||
avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
|
avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
|
||||||
|
outdated_warning = { extends = "$text.2", size = 13 }
|
||||||
|
|
||||||
[workspace.titlebar.offline_icon]
|
[workspace.titlebar.offline_icon]
|
||||||
padding = { right = 4 }
|
padding = { right = 4 }
|
||||||
|
|
|
@ -438,29 +438,37 @@ mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
editor::{self, Insert},
|
editor::{self, Insert},
|
||||||
fs::FakeFs,
|
fs::FakeFs,
|
||||||
test::{temp_tree, test_app_state},
|
test::test_app_state,
|
||||||
workspace::Workspace,
|
workspace::Workspace,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::fs;
|
use std::path::PathBuf;
|
||||||
use tempdir::TempDir;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
|
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
|
||||||
let tmp_dir = TempDir::new("example").unwrap();
|
let app_state = cx.update(test_app_state);
|
||||||
fs::create_dir(tmp_dir.path().join("a")).unwrap();
|
app_state
|
||||||
fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap();
|
.fs
|
||||||
fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap();
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"a": {
|
||||||
|
"banana": "",
|
||||||
|
"bandana": "",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
super::init(cx);
|
super::init(cx);
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let app_state = cx.update(test_app_state);
|
|
||||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_worktree(tmp_dir.path(), cx)
|
workspace.add_worktree(Path::new("/root"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -572,17 +580,17 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_single_file_worktrees(mut cx: gpui::TestAppContext) {
|
async fn test_single_file_worktrees(mut cx: gpui::TestAppContext) {
|
||||||
let temp_dir = TempDir::new("test-single-file-worktrees").unwrap();
|
|
||||||
let dir_path = temp_dir.path().join("the-parent-dir");
|
|
||||||
let file_path = dir_path.join("the-file");
|
|
||||||
fs::create_dir(&dir_path).unwrap();
|
|
||||||
fs::write(&file_path, "").unwrap();
|
|
||||||
|
|
||||||
let app_state = cx.update(test_app_state);
|
let app_state = cx.update(test_app_state);
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
|
||||||
|
.await;
|
||||||
|
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_worktree(&file_path, cx)
|
workspace.add_worktree(Path::new("/root/the-parent-dir/the-file"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -620,18 +628,25 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test(retries = 5)]
|
#[gpui::test(retries = 5)]
|
||||||
async fn test_multiple_matches_with_same_relative_path(mut cx: gpui::TestAppContext) {
|
async fn test_multiple_matches_with_same_relative_path(mut cx: gpui::TestAppContext) {
|
||||||
let tmp_dir = temp_tree(json!({
|
let app_state = cx.update(test_app_state);
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
"dir1": { "a.txt": "" },
|
"dir1": { "a.txt": "" },
|
||||||
"dir2": { "a.txt": "" }
|
"dir2": { "a.txt": "" }
|
||||||
}));
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let app_state = cx.update(test_app_state);
|
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
|
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.open_paths(
|
workspace.open_paths(
|
||||||
&[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")],
|
&[PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,6 +29,8 @@ pub trait Fs: Send + Sync {
|
||||||
latency: Duration,
|
latency: Duration,
|
||||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
||||||
fn is_fake(&self) -> bool;
|
fn is_fake(&self) -> bool;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_fake(&self) -> &FakeFs;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -125,6 +127,11 @@ impl Fs for RealFs {
|
||||||
fn is_fake(&self) -> bool {
|
fn is_fake(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_fake(&self) -> &FakeFs {
|
||||||
|
panic!("called `RealFs::as_fake`")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -413,4 +420,9 @@ impl Fs for FakeFs {
|
||||||
fn is_fake(&self) -> bool {
|
fn is_fake(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_fake(&self) -> &FakeFs {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,8 @@ pub struct Client {
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum EstablishConnectionError {
|
pub enum EstablishConnectionError {
|
||||||
|
#[error("upgrade required")]
|
||||||
|
UpgradeRequired,
|
||||||
#[error("unauthorized")]
|
#[error("unauthorized")]
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
@ -68,8 +70,10 @@ pub enum EstablishConnectionError {
|
||||||
impl From<WebsocketError> for EstablishConnectionError {
|
impl From<WebsocketError> for EstablishConnectionError {
|
||||||
fn from(error: WebsocketError) -> Self {
|
fn from(error: WebsocketError) -> Self {
|
||||||
if let WebsocketError::Http(response) = &error {
|
if let WebsocketError::Http(response) = &error {
|
||||||
if response.status() == StatusCode::UNAUTHORIZED {
|
match response.status() {
|
||||||
return EstablishConnectionError::Unauthorized;
|
StatusCode::UNAUTHORIZED => return EstablishConnectionError::Unauthorized,
|
||||||
|
StatusCode::UPGRADE_REQUIRED => return EstablishConnectionError::UpgradeRequired,
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EstablishConnectionError::Other(error.into())
|
EstablishConnectionError::Other(error.into())
|
||||||
|
@ -85,6 +89,7 @@ impl EstablishConnectionError {
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
SignedOut,
|
SignedOut,
|
||||||
|
UpgradeRequired,
|
||||||
Authenticating,
|
Authenticating,
|
||||||
Connecting,
|
Connecting,
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
|
@ -227,7 +232,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Status::SignedOut => {
|
Status::SignedOut | Status::UpgradeRequired => {
|
||||||
state._maintain_connection.take();
|
state._maintain_connection.take();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -346,6 +351,7 @@ impl Client {
|
||||||
| Status::Reconnecting { .. }
|
| Status::Reconnecting { .. }
|
||||||
| Status::Authenticating
|
| Status::Authenticating
|
||||||
| Status::Reauthenticating => return Ok(()),
|
| Status::Reauthenticating => return Ok(()),
|
||||||
|
Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
if was_disconnected {
|
if was_disconnected {
|
||||||
|
@ -388,8 +394,7 @@ impl Client {
|
||||||
self.set_connection(conn, cx).await;
|
self.set_connection(conn, cx).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(EstablishConnectionError::Unauthorized) => {
|
||||||
if matches!(err, EstablishConnectionError::Unauthorized) {
|
|
||||||
self.state.write().credentials.take();
|
self.state.write().credentials.take();
|
||||||
if used_keychain {
|
if used_keychain {
|
||||||
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
|
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
|
||||||
|
@ -397,12 +402,16 @@ impl Client {
|
||||||
self.authenticate_and_connect(cx).await
|
self.authenticate_and_connect(cx).await
|
||||||
} else {
|
} else {
|
||||||
self.set_status(Status::ConnectionError, cx);
|
self.set_status(Status::ConnectionError, cx);
|
||||||
Err(err)?
|
Err(EstablishConnectionError::Unauthorized)?
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
Err(EstablishConnectionError::UpgradeRequired) => {
|
||||||
|
self.set_status(Status::UpgradeRequired, cx);
|
||||||
|
Err(EstablishConnectionError::UpgradeRequired)?
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
self.set_status(Status::ConnectionError, cx);
|
self.set_status(Status::ConnectionError, cx);
|
||||||
Err(err)?
|
Err(error)?
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -489,10 +498,12 @@ impl Client {
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
cx: &AsyncAppContext,
|
cx: &AsyncAppContext,
|
||||||
) -> Task<Result<Connection, EstablishConnectionError>> {
|
) -> Task<Result<Connection, EstablishConnectionError>> {
|
||||||
let request = Request::builder().header(
|
let request = Request::builder()
|
||||||
|
.header(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
format!("{} {}", credentials.user_id, credentials.access_token),
|
format!("{} {}", credentials.user_id, credentials.access_token),
|
||||||
);
|
)
|
||||||
|
.header("X-Zed-Protocol-Version", zrpc::PROTOCOL_VERSION);
|
||||||
cx.background().spawn(async move {
|
cx.background().spawn(async move {
|
||||||
if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") {
|
if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") {
|
||||||
let stream = smol::net::TcpStream::connect(host).await?;
|
let stream = smol::net::TcpStream::connect(host).await?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
assets::Assets,
|
assets::Assets,
|
||||||
channel::ChannelList,
|
channel::ChannelList,
|
||||||
fs::RealFs,
|
fs::FakeFs,
|
||||||
http::{HttpClient, Request, Response, ServerResponse},
|
http::{HttpClient, Request, Response, ServerResponse},
|
||||||
language::LanguageRegistry,
|
language::LanguageRegistry,
|
||||||
rpc::{self, Client, Credentials, EstablishConnectionError},
|
rpc::{self, Client, Credentials, EstablishConnectionError},
|
||||||
|
@ -177,7 +177,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
||||||
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
|
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
|
||||||
rpc,
|
rpc,
|
||||||
user_store,
|
user_store,
|
||||||
fs: Arc::new(RealFs),
|
fs: Arc::new(FakeFs::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ pub struct Titlebar {
|
||||||
pub offline_icon: OfflineIcon,
|
pub offline_icon: OfflineIcon,
|
||||||
pub icon_color: Color,
|
pub icon_color: Color,
|
||||||
pub avatar: ImageStyle,
|
pub avatar: ImageStyle,
|
||||||
|
pub outdated_warning: ContainedText,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
|
@ -169,7 +170,7 @@ pub struct Selector {
|
||||||
pub active_item: ContainedLabel,
|
pub active_item: ContainedLabel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct ContainedText {
|
pub struct ContainedText {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
|
|
|
@ -1041,6 +1041,16 @@ impl Workspace {
|
||||||
.with_style(theme.workspace.titlebar.offline_icon.container)
|
.with_style(theme.workspace.titlebar.offline_icon.container)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
),
|
),
|
||||||
|
rpc::Status::UpgradeRequired => Some(
|
||||||
|
Label::new(
|
||||||
|
"Please update Zed to collaborate".to_string(),
|
||||||
|
theme.workspace.titlebar.outdated_warning.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.workspace.titlebar.outdated_warning.container)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1195,11 +1205,9 @@ mod tests {
|
||||||
editor::{Editor, Insert},
|
editor::{Editor, Insert},
|
||||||
fs::FakeFs,
|
fs::FakeFs,
|
||||||
test::{temp_tree, test_app_state},
|
test::{temp_tree, test_app_state},
|
||||||
worktree::WorktreeHandle,
|
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{collections::HashSet, fs};
|
use std::collections::HashSet;
|
||||||
use tempdir::TempDir;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
|
async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
|
||||||
|
@ -1268,20 +1276,26 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_entry(mut cx: gpui::TestAppContext) {
|
async fn test_open_entry(mut cx: gpui::TestAppContext) {
|
||||||
let dir = temp_tree(json!({
|
let app_state = cx.update(test_app_state);
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
"a": {
|
"a": {
|
||||||
"file1": "contents 1",
|
"file1": "contents 1",
|
||||||
"file2": "contents 2",
|
"file2": "contents 2",
|
||||||
"file3": "contents 3",
|
"file3": "contents 3",
|
||||||
},
|
},
|
||||||
}));
|
}),
|
||||||
|
)
|
||||||
let app_state = cx.update(test_app_state);
|
.await;
|
||||||
|
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_worktree(dir.path(), cx)
|
workspace.add_worktree(Path::new("/root"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1445,28 +1459,30 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
|
async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
|
||||||
let dir = temp_tree(json!({
|
|
||||||
"a.txt": "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
let app_state = cx.update(test_app_state);
|
let app_state = cx.update(test_app_state);
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"a.txt": "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_worktree(dir.path(), cx)
|
workspace.add_worktree(Path::new("/root"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let tree = cx.read(|cx| {
|
|
||||||
let mut trees = workspace.read(cx).worktrees().iter();
|
|
||||||
trees.next().unwrap().clone()
|
|
||||||
});
|
|
||||||
tree.flush_fs_events(&cx).await;
|
|
||||||
|
|
||||||
// Open a file within an existing worktree.
|
// Open a file within an existing worktree.
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
workspace.update(cx, |view, cx| {
|
workspace.update(cx, |view, cx| {
|
||||||
view.open_paths(&[dir.path().join("a.txt")], cx)
|
view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -1477,7 +1493,12 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx)));
|
cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx)));
|
||||||
fs::write(dir.path().join("a.txt"), "changed").unwrap();
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_file("/root/a.txt", "changed".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
editor
|
editor
|
||||||
.condition(&cx, |editor, cx| editor.has_conflict(cx))
|
.condition(&cx, |editor, cx| editor.has_conflict(cx))
|
||||||
.await;
|
.await;
|
||||||
|
@ -1493,12 +1514,12 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
|
async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
|
||||||
let dir = TempDir::new("test-new-file").unwrap();
|
|
||||||
let app_state = cx.update(test_app_state);
|
let app_state = cx.update(test_app_state);
|
||||||
|
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_worktree(dir.path(), cx)
|
workspace.add_worktree(Path::new("/root"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1511,7 +1532,6 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.clone()
|
||||||
});
|
});
|
||||||
tree.flush_fs_events(&cx).await;
|
|
||||||
|
|
||||||
// Create a new untitled buffer
|
// Create a new untitled buffer
|
||||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
let editor = workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
@ -1537,7 +1557,7 @@ mod tests {
|
||||||
workspace.save_active_item(&Save, cx)
|
workspace.save_active_item(&Save, cx)
|
||||||
});
|
});
|
||||||
cx.simulate_new_path_selection(|parent_dir| {
|
cx.simulate_new_path_selection(|parent_dir| {
|
||||||
assert_eq!(parent_dir, dir.path());
|
assert_eq!(parent_dir, Path::new("/root"));
|
||||||
Some(parent_dir.join("the-new-name.rs"))
|
Some(parent_dir.join("the-new-name.rs"))
|
||||||
});
|
});
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
|
@ -1598,8 +1618,8 @@ mod tests {
|
||||||
async fn test_setting_language_when_saving_as_single_file_worktree(
|
async fn test_setting_language_when_saving_as_single_file_worktree(
|
||||||
mut cx: gpui::TestAppContext,
|
mut cx: gpui::TestAppContext,
|
||||||
) {
|
) {
|
||||||
let dir = TempDir::new("test-new-file").unwrap();
|
|
||||||
let app_state = cx.update(test_app_state);
|
let app_state = cx.update(test_app_state);
|
||||||
|
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
|
|
||||||
// Create a new untitled buffer
|
// Create a new untitled buffer
|
||||||
|
@ -1623,7 +1643,7 @@ mod tests {
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
workspace.save_active_item(&Save, cx)
|
workspace.save_active_item(&Save, cx)
|
||||||
});
|
});
|
||||||
cx.simulate_new_path_selection(|_| Some(dir.path().join("the-new-name.rs")));
|
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
|
||||||
|
@ -1640,7 +1660,7 @@ mod tests {
|
||||||
cx.update(init);
|
cx.update(init);
|
||||||
|
|
||||||
let app_state = cx.update(test_app_state);
|
let app_state = cx.update(test_app_state);
|
||||||
cx.dispatch_global_action(OpenNew(app_state));
|
cx.dispatch_global_action(OpenNew(app_state.clone()));
|
||||||
let window_id = *cx.window_ids().first().unwrap();
|
let window_id = *cx.window_ids().first().unwrap();
|
||||||
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
||||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
let editor = workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
@ -1660,10 +1680,8 @@ mod tests {
|
||||||
workspace.save_active_item(&Save, cx)
|
workspace.save_active_item(&Save, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let dir = TempDir::new("test-new-empty-workspace").unwrap();
|
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
|
||||||
cx.simulate_new_path_selection(|_| {
|
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
||||||
Some(dir.path().canonicalize().unwrap().join("the-new-name"))
|
|
||||||
});
|
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
|
.condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
|
||||||
|
@ -1676,20 +1694,26 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_pane_actions(mut cx: gpui::TestAppContext) {
|
async fn test_pane_actions(mut cx: gpui::TestAppContext) {
|
||||||
cx.update(|cx| pane::init(cx));
|
cx.update(|cx| pane::init(cx));
|
||||||
|
let app_state = cx.update(test_app_state);
|
||||||
let dir = temp_tree(json!({
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
"a": {
|
"a": {
|
||||||
"file1": "contents 1",
|
"file1": "contents 1",
|
||||||
"file2": "contents 2",
|
"file2": "contents 2",
|
||||||
"file3": "contents 3",
|
"file3": "contents 3",
|
||||||
},
|
},
|
||||||
}));
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let app_state = cx.update(test_app_state);
|
|
||||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_worktree(dir.path(), cx)
|
workspace.add_worktree(Path::new("/root"), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -4,3 +4,5 @@ mod peer;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
pub use conn::Connection;
|
pub use conn::Connection;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
|
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 0;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue