diff --git a/Cargo.lock b/Cargo.lock index 7494cb67822db0480effc8e38fca35e239464014..946d99e547cfd23b6e57a233c0cbe370ccbef278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2761,6 +2761,7 @@ dependencies = [ "rpc", "serde", "serde_json", + "settings", "similar", "smallvec", "smol", @@ -6993,7 +6994,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.48.1" +version = "0.49.1" dependencies = [ "activity_indicator", "anyhow", diff --git a/assets/icons/arrow_down_12.svg b/assets/icons/arrow_down_12.svg index 63cbe5099717078431edcfe6b5a434bb90fe636a..5d9a8ee5e374884b64ddd82c2f808adb6681e444 100644 --- a/assets/icons/arrow_down_12.svg +++ b/assets/icons/arrow_down_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down_16.svg b/assets/icons/arrow_down_16.svg index 2e8a42a9eaf6258bc8e089bc3db8ac3527e4426b..65114ebd604f2b76796676b2eab0abb81e8b82f1 100644 --- a/assets/icons/arrow_down_16.svg +++ b/assets/icons/arrow_down_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down_8.svg b/assets/icons/arrow_down_8.svg index 18ac436d2a212c1412839223c433165f70783c7c..53d3c2944e49c690b22eff59bc901133228d0e3f 100644 --- a/assets/icons/arrow_down_8.svg +++ b/assets/icons/arrow_down_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_left_12.svg b/assets/icons/arrow_left_12.svg index d7245fc106a0edad8f756dea857a2773132f8f24..fc7cabc9c67fd35f340d744a9bd6f2fa3d6c1922 100644 --- a/assets/icons/arrow_left_12.svg +++ b/assets/icons/arrow_left_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_left_16.svg b/assets/icons/arrow_left_16.svg index a313cf002e1987c7daa328fac63ffd42c1db38e6..07a2db5348d0157c15996bde26cde380b8d0db11 100644 --- a/assets/icons/arrow_left_16.svg +++ b/assets/icons/arrow_left_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_left_8.svg b/assets/icons/arrow_left_8.svg index 7851d9f2191d299e8d5100741d273d263ecf4dc3..a6a7079dd54c2f3e2ddc79ba95a97706300fed66 100644 --- a/assets/icons/arrow_left_8.svg +++ b/assets/icons/arrow_left_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_12.svg b/assets/icons/arrow_right_12.svg index a38f55dcaf1873a30b9c6fbf577ec56921364a67..00dc3918fcb350e0459d20488fad7e9ccef3c90c 100644 --- a/assets/icons/arrow_right_12.svg +++ b/assets/icons/arrow_right_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_16.svg b/assets/icons/arrow_right_16.svg index 790a142162b2056d5f1b66f2b4d71f479bd1542d..b41e8fc810b7d927e3b298e3321028206253e887 100644 --- a/assets/icons/arrow_right_16.svg +++ b/assets/icons/arrow_right_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_8.svg b/assets/icons/arrow_right_8.svg index 3115e0e6690a3eaba2ce61f235182d031a6e9ce5..ef28fc81e54fcd28ae09ba6540c664680bbeb61e 100644 --- a/assets/icons/arrow_right_8.svg +++ b/assets/icons/arrow_right_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_12.svg b/assets/icons/arrow_up_12.svg index 84bcc4ebd8c8fce0d8699da3a8dbfbe343c5fa8e..ae4cf928993fdd79df3739aa4226fd5261975097 100644 --- a/assets/icons/arrow_up_12.svg +++ b/assets/icons/arrow_up_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_16.svg b/assets/icons/arrow_up_16.svg index 63ea16b1d814d6c8539c038ff2e353022e781049..0d8add4ed7c96ed30aae8d39eaf2e66e9a03019d 100644 --- a/assets/icons/arrow_up_16.svg +++ b/assets/icons/arrow_up_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_8.svg b/assets/icons/arrow_up_8.svg index 24a040abbb63391f7920bbc00a286a5b5b9dccb2..9d3f76a894b1d8461200252f609ae6c9427b77e6 100644 --- a/assets/icons/arrow_up_8.svg +++ b/assets/icons/arrow_up_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/backspace _12.svg b/assets/icons/backspace _12.svg new file mode 100644 index 0000000000000000000000000000000000000000..68bad3da268a98b3d1a44f52dd9687ea6865ef2b --- /dev/null +++ b/assets/icons/backspace _12.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/backspace _16.svg b/assets/icons/backspace _16.svg new file mode 100644 index 0000000000000000000000000000000000000000..965470690e2db31d1dd6b4fdd10185d7825b2594 --- /dev/null +++ b/assets/icons/backspace _16.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/backspace _8.svg b/assets/icons/backspace _8.svg new file mode 100644 index 0000000000000000000000000000000000000000..60972007b6c4c0a40ddc449d4c8f6a439a22e9e1 --- /dev/null +++ b/assets/icons/backspace _8.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/bolt_16.svg b/assets/icons/bolt_16.svg index c3859443b21c9d899bf1b7b64d97f19cee2c441b..aca476ef508173e60f84da60f1ba299f2bdb7009 100644 --- a/assets/icons/bolt_16.svg +++ b/assets/icons/bolt_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_12.svg b/assets/icons/check_12.svg index e4255646bb849886532a895c527a1768e2982518..3e15dd7d1fd4504f4e87e3c8f14881c3ea4c6c72 100644 --- a/assets/icons/check_12.svg +++ b/assets/icons/check_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_16.svg b/assets/icons/check_16.svg index 0ebf3fe75eb9d2b06b16dc2a854bcc5a37b56776..7e959b59242742de30144d1eb4859b7fdfc5b43b 100644 --- a/assets/icons/check_16.svg +++ b/assets/icons/check_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_8.svg b/assets/icons/check_8.svg index 7513909ce12bf95c08c9fb0d79ef670f8a71a140..268f8bb498fb623b6554dc3db1d6a4aa89343f26 100644 --- a/assets/icons/check_8.svg +++ b/assets/icons/check_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_right_16.svg b/assets/icons/chevron_right_16.svg index 3a2aafbcd678ef67d21d127242d75652932dd847..270a33db70b2e2e412ef1351d16e2964f164e512 100644 --- a/assets/icons/chevron_right_16.svg +++ b/assets/icons/chevron_right_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_right_8.svg b/assets/icons/chevron_right_8.svg index 7349274681fc89d09715b98a86770284598932aa..64910c54e7c159420cce8e74f3d93bb2f7640781 100644 --- a/assets/icons/chevron_right_8.svg +++ b/assets/icons/chevron_right_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/cloud_slash_12.svg b/assets/icons/cloud_slash_12.svg index 37d0ee904c77fb16c6f497e24acc5014a3f85b62..adb60d2c2791699f32f0b1bc124a2f2092432d16 100644 --- a/assets/icons/cloud_slash_12.svg +++ b/assets/icons/cloud_slash_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/cloud_slash_8.svg b/assets/icons/cloud_slash_8.svg index 524012d31a7cc0d08edb666f4460f9755a6aa159..9b7f0011b2a9fb9eb1fedb36f45658d63bbf725b 100644 --- a/assets/icons/cloud_slash_8.svg +++ b/assets/icons/cloud_slash_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/download_12.svg b/assets/icons/download_12.svg index 8386cecbebad214f57f6315eef55cd57752cf8cb..bcf66dfdf71679c0c206e1af1322d2236381ce0a 100644 --- a/assets/icons/download_12.svg +++ b/assets/icons/download_12.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/assets/icons/download_8.svg b/assets/icons/download_8.svg index 629ba0c62069b2dc2aa912475cfcf3a5a5d6fd59..fb8b021d6b79289ba1ffa4f70eef41f6ebef8e8d 100644 --- a/assets/icons/download_8.svg +++ b/assets/icons/download_8.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/assets/icons/folder_tree_16.svg b/assets/icons/folder_tree_16.svg index 425bfa6c436466973354d069f079fc44d581d621..a264a3257306e656b373dad7acab1412ac023c2f 100644 --- a/assets/icons/folder_tree_16.svg +++ b/assets/icons/folder_tree_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/magnifying_glass_12.svg b/assets/icons/magnifying_glass_12.svg index b45c30172e139a2804c51276e41eda665b029ee5..0ee5e24b248bda4178ae5e48142b93904311a3e4 100644 --- a/assets/icons/magnifying_glass_12.svg +++ b/assets/icons/magnifying_glass_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/magnifying_glass_8.svg b/assets/icons/magnifying_glass_8.svg index 00eb332440071284ca7f35000b6a245c940bb10e..958614203cfedde2296036d58c4c274aed3c07f5 100644 --- a/assets/icons/magnifying_glass_8.svg +++ b/assets/icons/magnifying_glass_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/terminal_16.svg b/assets/icons/terminal_16.svg index 927982deee1a099ec1f8666872863a49454db6f6..95da7ff4e1e433625938b152417ee0ddc550f330 100644 --- a/assets/icons/terminal_16.svg +++ b/assets/icons/terminal_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_circle_8.svg b/assets/icons/user_circle_8.svg index 4bbf9c03015947093c0261b4c60b31402edc54c6..7c884eec39e3b36cb1b8a5a719a3e5ecf6d22469 100644 --- a/assets/icons/user_circle_8.svg +++ b/assets/icons/user_circle_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_group_12.svg b/assets/icons/user_group_12.svg index 5eae1d55b7e1406d0956c67cf6b9dba9949faefc..13ed0b12e4a1f64b30d56f631d4bea0cbd8c7345 100644 --- a/assets/icons/user_group_12.svg +++ b/assets/icons/user_group_12.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_group_16.svg b/assets/icons/user_group_16.svg index 4f72d4b6b4e617c1619055dee7c246cc7f2e02f6..aa99277646653c899ee049547e5574b76b25b840 100644 --- a/assets/icons/user_group_16.svg +++ b/assets/icons/user_group_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_group_8.svg b/assets/icons/user_group_8.svg index 39058e51d42ef9707f890ece172cb6d98f5d8d32..9cf29f6e15f03b2d570303bd2d6f3b5696edb610 100644 --- a/assets/icons/user_group_8.svg +++ b/assets/icons/user_group_8.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/user_plus_16.svg b/assets/icons/user_plus_16.svg index 213eb8f13b0e9ff1793e5946fc3be0243e42232b..3fd6e13554dc9f93b3c76cbd0cf5ccced38df8ce 100644 --- a/assets/icons/user_plus_16.svg +++ b/assets/icons/user_plus_16.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 72252edd71ebfb49a534caa85d289e6ddd1b3d8f..28eec35defc36c52970be443eba0bb0ac16c328e 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -192,6 +192,7 @@ "shift-f8": "editor::GoToPrevDiagnostic", "f2": "editor::Rename", "f12": "editor::GoToDefinition", + "cmd-f12": "editor::GoToTypeDefinition", "alt-shift-f12": "editor::FindAllReferences", "ctrl-m": "editor::MoveToEnclosingBracket", "alt-cmd-[": "editor::Fold", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2da8c263426c45a440cd9ec9aed3ccd44ace5cb9..5262daab5fde4f3bb705780c801ea7a8d07ef94e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -14,30 +14,30 @@ "k": "vim::Up", "l": "vim::Right", "0": "vim::StartOfLine", - "shift-$": "vim::EndOfLine", - "shift-G": "vim::EndOfDocument", + "$": "vim::EndOfLine", + "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", - "shift-W": [ + "shift-w": [ "vim::NextWordStart", { "ignorePunctuation": true } ], "e": "vim::NextWordEnd", - "shift-E": [ + "shift-e": [ "vim::NextWordEnd", { "ignorePunctuation": true } ], "b": "vim::PreviousWordStart", - "shift-B": [ + "shift-b": [ "vim::PreviousWordStart", { "ignorePunctuation": true } ], - "shift-%": "vim::Matching", + "%": "vim::Matching", "escape": "editor::Cancel" } }, @@ -48,12 +48,12 @@ "vim::PushOperator", "Change" ], - "shift-C": "vim::ChangeToEndOfLine", + "shift-c": "vim::ChangeToEndOfLine", "d": [ "vim::PushOperator", "Delete" ], - "shift-D": "vim::DeleteToEndOfLine", + "shift-d": "vim::DeleteToEndOfLine", "y": [ "vim::PushOperator", "Yank" @@ -62,14 +62,14 @@ "vim::SwitchMode", "Insert" ], - "shift-I": "vim::InsertFirstNonWhitespace", + "shift-i": "vim::InsertFirstNonWhitespace", "a": "vim::InsertAfter", - "shift-A": "vim::InsertEndOfLine", + "shift-a": "vim::InsertEndOfLine", "x": "vim::DeleteRight", - "shift-X": "vim::DeleteLeft", - "shift-^": "vim::FirstNonWhitespace", + "shift-x": "vim::DeleteLeft", + "^": "vim::FirstNonWhitespace", "o": "vim::InsertLineBelow", - "shift-O": "vim::InsertLineAbove", + "shift-o": "vim::InsertLineAbove", "v": [ "vim::SwitchMode", { @@ -78,7 +78,7 @@ } } ], - "shift-V": [ + "shift-v": [ "vim::SwitchMode", { "Visual": { @@ -113,7 +113,7 @@ "context": "Editor && vim_operator == c", "bindings": { "w": "vim::ChangeWord", - "shift-W": [ + "shift-w": [ "vim::ChangeWord", { "ignorePunctuation": true diff --git a/assets/settings/default.json b/assets/settings/default.json index 6c34d6be70ebd3193469814e628f6c136335a9d8..5c05e8eaba1b72f29d76bba9863cb90dfe01badc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -84,7 +84,8 @@ "shell": "system", // What working directory to use when launching the terminal. // May take 4 values: - // 1. Use the current file's project directory. + // 1. Use the current file's project directory. Will Fallback to the + // first project directory strategy if unsuccessful // "working_directory": "current_project_directory" // 2. Use the first project in this workspace's directory // "working_directory": "first_project_directory" diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 3730b5d21afc2a37e22843320b2b4deaf41cbef3..02ad100df81e8ba13e5d52ad805017cab6a498b3 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -82,6 +82,7 @@ impl ActivityIndicator { buffer.update(cx, |buffer, cx| { buffer.edit( [(0..0, format!("Language server error: {}\n\n", lsp_name))], + None, cx, ); }); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0e9ec4076ad43754a53e11f833935c9b807f025c..af084dc88d45eff6acbcf57c3de73499e34fbee6 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -569,14 +569,14 @@ impl Client { ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { Status::SignedOut => true, - Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { - false + Status::ConnectionError + | Status::ConnectionLost + | Status::Authenticating { .. } + | Status::Reauthenticating { .. } + | Status::ReconnectionError { .. } => false, + Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { + return Ok(()) } - Status::Connected { .. } - | Status::Connecting { .. } - | Status::Reconnecting { .. } - | Status::Authenticating - | Status::Reauthenticating => return Ok(()), Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?, }; @@ -593,13 +593,22 @@ impl Client { read_from_keychain = credentials.is_some(); } if credentials.is_none() { - credentials = Some(match self.authenticate(&cx).await { - Ok(credentials) => credentials, - Err(err) => { - self.set_status(Status::ConnectionError, cx); - return Err(err); + let mut status_rx = self.status(); + let _ = status_rx.next().await; + futures::select_biased! { + authenticate = self.authenticate(&cx).fuse() => { + match authenticate { + Ok(creds) => credentials = Some(creds), + Err(err) => { + self.set_status(Status::ConnectionError, cx); + return Err(err); + } + } } - }); + _ = status_rx.next().fuse() => { + return Err(anyhow!("authentication canceled")); + } + } } let credentials = credentials.unwrap(); @@ -899,40 +908,42 @@ impl Client { // custom URL scheme instead of this local HTTP server. let (user_id, access_token) = executor .spawn(async move { - if let Some(req) = server.recv_timeout(Duration::from_secs(10 * 60))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); + for _ in 0..100 { + if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { + let path = req.url(); + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value.to_string()); + } else if key == "user_id" { + user_id = Some(value.to_string()); + } } - } - let post_auth_url = - format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); - req.respond( - tiny_http::Response::empty(302).with_header( - tiny_http::Header::from_bytes( - &b"Location"[..], - post_auth_url.as_bytes(), - ) - .unwrap(), - ), - ) - .context("failed to respond to login http request")?; - Ok(( - user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, - access_token - .ok_or_else(|| anyhow!("missing access_token parameter"))?, - )) - } else { - Err(anyhow!("didn't receive login redirect")) + let post_auth_url = + format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); + req.respond( + tiny_http::Response::empty(302).with_header( + tiny_http::Header::from_bytes( + &b"Location"[..], + post_auth_url.as_bytes(), + ) + .unwrap(), + ), + ) + .context("failed to respond to login http request")?; + return Ok(( + user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, + access_token + .ok_or_else(|| anyhow!("missing access_token parameter"))?, + )); + } } + + Err(anyhow!("didn't receive login redirect")) }) .await?; @@ -1061,7 +1072,9 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { mod tests { use super::*; use crate::test::{FakeHttpClient, FakeServer}; - use gpui::TestAppContext; + use gpui::{executor::Deterministic, TestAppContext}; + use parking_lot::Mutex; + use std::future; #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { @@ -1098,6 +1111,48 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } + #[gpui::test(iterations = 10)] + async fn test_authenticating_more_than_once( + cx: &mut TestAppContext, + deterministic: Arc, + ) { + cx.foreground().forbid_parking(); + + let auth_count = Arc::new(Mutex::new(0)); + let dropped_auth_count = Arc::new(Mutex::new(0)); + let client = Client::new(FakeHttpClient::with_404_response()); + client.override_authenticate({ + let auth_count = auth_count.clone(); + let dropped_auth_count = dropped_auth_count.clone(); + move |cx| { + let auth_count = auth_count.clone(); + let dropped_auth_count = dropped_auth_count.clone(); + cx.foreground().spawn(async move { + *auth_count.lock() += 1; + let _drop = util::defer(move || *dropped_auth_count.lock() += 1); + future::pending::<()>().await; + unreachable!() + }) + } + }); + + let _authenticate = cx.spawn(|cx| { + let client = client.clone(); + async move { client.authenticate_and_connect(false, &cx).await } + }); + deterministic.run_until_parked(); + assert_eq!(*auth_count.lock(), 1); + assert_eq!(*dropped_auth_count.lock(), 0); + + let _authenticate = cx.spawn(|cx| { + let client = client.clone(); + async move { client.authenticate_and_connect(false, &cx).await } + }); + deterministic.run_until_parked(); + assert_eq!(*auth_count.lock(), 2); + assert_eq!(*dropped_auth_count.lock(), 1); + } + #[test] fn test_encode_and_decode_worktree_url() { let url = encode_worktree_url(5, "deadbeef"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f45a54472800ddb690b4e27a72de0fc71ad3a1d0..1c1893dc796c65eaedae1e792809a01db0daf427 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -6,7 +6,6 @@ use async_trait::async_trait; use axum::http::StatusCode; use collections::HashMap; use futures::StreamExt; -use nanoid::nanoid; use serde::{Deserialize, Serialize}; pub use sqlx::postgres::PgPoolOptions as DbOptions; use sqlx::{types::Uuid, FromRow, QueryBuilder, Row}; @@ -218,7 +217,7 @@ impl Db for PostgresDb { .push_bind(github_login) .push_bind(email_address) .push_bind(false) - .push_bind(nanoid!(16)) + .push_bind(random_invite_code()) .push_bind(invite_count as i32); }, ); @@ -346,7 +345,7 @@ impl Db for PostgresDb { WHERE id = $2 AND invite_code IS NULL ", ) - .bind(nanoid!(16)) + .bind(random_invite_code()) .bind(id) .execute(&mut tx) .await?; @@ -451,15 +450,17 @@ impl Db for PostgresDb { let invitee_id = sqlx::query_scalar( " INSERT INTO users - (github_login, email_address, admin, inviter_id) + (github_login, email_address, admin, inviter_id, invite_code, invite_count) VALUES - ($1, $2, 'f', $3) + ($1, $2, 'f', $3, $4, $5) RETURNING id ", ) .bind(login) .bind(email_address) .bind(inviter_id) + .bind(random_invite_code()) + .bind(5) .fetch_one(&mut tx) .await .map(UserId)?; @@ -1458,6 +1459,10 @@ fn fuzzy_like_string(string: &str) -> String { result } +fn random_invite_code() -> String { + nanoid::nanoid!(16) +} + #[cfg(test)] pub mod tests { use super::*; @@ -2381,6 +2386,20 @@ pub mod tests { .unwrap_err(); let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); assert_eq!(invite_count, 1); + + // Ensure invited users get invite codes too. + assert_eq!( + db.get_invite_code_for_user(user2).await.unwrap().unwrap().1, + 5 + ); + assert_eq!( + db.get_invite_code_for_user(user3).await.unwrap().unwrap().1, + 5 + ); + assert_eq!( + db.get_invite_code_for_user(user4).await.unwrap().unwrap().1, + 5 + ); } pub struct TestDb { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a3dcd5fbce3942355da8cfacacb875b6bca0fd41..1f3ccef0be0d6ecd3946e2971850d3be0b7b3412 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -842,8 +842,8 @@ async fn test_propagate_saves_and_fs_changes( .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], cx)); - buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx)); + buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx)); // Open and edit that buffer as the host. let buffer_a = project_a @@ -855,7 +855,7 @@ async fn test_propagate_saves_and_fs_changes( .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") .await; buffer_a.update(cx_a, |buf, cx| { - buf.edit([(buf.len()..buf.len(), "i-am-a")], cx) + buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx) }); // Wait for edits to propagate @@ -871,7 +871,7 @@ async fn test_propagate_saves_and_fs_changes( // Edit the buffer as the host and concurrently save as guest B. let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx)); - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( client_a.fs.load("/a/file1".as_ref()).await.unwrap(), @@ -1237,7 +1237,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T .await .unwrap(); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx)); buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -1251,7 +1251,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T assert!(!buf.has_conflict()); }); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx)); buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -1342,9 +1342,9 @@ async fn test_editing_while_guest_opens_buffer( // Edit the buffer as client A while client B is still opening it. cx_b.background().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx)); cx_b.background().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx)); let text = buffer_a.read_with(cx_a, |buf, _| buf.text()); let buffer_b = buffer_b.await.unwrap(); @@ -1882,8 +1882,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .await .unwrap(); buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(4..7, "six")], cx); - buffer.edit([(10..11, "6")], cx); + buffer.edit([(4..7, "six")], None, cx); + buffer.edit([(10..11, "6")], None, cx); assert_eq!(buffer.text(), "let six = 6;"); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); @@ -2589,7 +2589,7 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file. let mut fake_symbol = symbols[0].clone(); - fake_symbol.path = Path::new("/code/secrets").into(); + fake_symbol.path.path = Path::new("/code/secrets").into(); let error = project_b .update(cx_b, |project, cx| { project.open_buffer_for_symbol(&fake_symbol, cx) @@ -2964,7 +2964,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T ); rename.editor.update(cx, |rename_editor, cx| { rename_editor.buffer().update(cx, |rename_buffer, cx| { - rename_buffer.edit([(0..3, "THREE")], cx); + rename_buffer.edit([(0..3, "THREE")], None, cx); }); }); }); diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 532d545b9121ddcbdd809b23567fb768fb265cfa..aef90378791a9f49f6e08dddd647324ae0ee361c 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1270,13 +1270,6 @@ mod tests { .detach(); }); - let request = server.receive::().await.unwrap(); - server - .respond( - request.receipt(), - proto::RegisterProjectResponse { project_id: 200 }, - ) - .await; let get_users_request = server.receive::().await.unwrap(); server .respond( @@ -1307,6 +1300,14 @@ mod tests { ) .await; + let request = server.receive::().await.unwrap(); + server + .respond( + request.receipt(), + proto::RegisterProjectResponse { project_id: 200 }, + ) + .await; + server.send(proto::UpdateContacts { incoming_requests: vec![proto::IncomingContactRequest { requester_id: 1, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6f50da6a7d28a51b67c0711b6dfe35ea1ab0adf7..9b12df60d9d1d3c6a67c6f302ab97847ed6c5ae6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -897,7 +897,7 @@ pub mod tests { let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); buffer.update(cx, |buffer, cx| { - buffer.edit([(ix..ix, "and ")], cx); + buffer.edit([(ix..ix, "and ")], None, cx); }); let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); @@ -936,6 +936,7 @@ pub mod tests { (Point::new(1, 1)..Point::new(1, 1), "\t"), (Point::new(2, 1)..Point::new(2, 1), "\t"), ], + None, cx, ) }); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index ed0df25d69b20d13e519a1baef6568269660a6dc..52379011709c2b3fc81183065fcbf22e643e5114 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1164,7 +1164,7 @@ mod tests { // Insert a line break, separating two block decorations into separate lines. let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], cx); + buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx); buffer.snapshot(cx) }); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 95c3abb25238adbf76dccf496ad2e719f815b881..a6e5536d15ac6b1ddc4dac29029f11d402351a7c 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1240,6 +1240,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 1), "123"), (Point::new(2, 3)..Point::new(2, 3), "123"), ], + None, cx, ); buffer.snapshot(cx) @@ -1262,7 +1263,7 @@ mod tests { ); let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], cx); + buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx); buffer.snapshot(cx) }); let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); @@ -1318,7 +1319,7 @@ mod tests { // Edit within one of the folds. let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(0..1, "12345")], cx); + buffer.edit([(0..1, "12345")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = @@ -1360,7 +1361,7 @@ mod tests { assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], cx); + buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d92bd04251b34fae0a739afa56aad04101e3079e..558a6bfd9852490cd79946ba49375bc88db7d953 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -38,9 +38,9 @@ use hover_popover::{hide_hover, HoverState}; pub use items::MAX_TAB_TITLE_LEN; pub use language::{char_kind, CharKind}; use language::{ - BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, - IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::LinkGoToDefinitionState; pub use multi_buffer::{ @@ -187,6 +187,7 @@ actions!( SelectLargerSyntaxNode, SelectSmallerSyntaxNode, GoToDefinition, + GoToTypeDefinition, MoveToEnclosingBracket, UndoSelection, RedoSelection, @@ -297,6 +298,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::go_to_diagnostic); cx.add_action(Editor::go_to_prev_diagnostic); cx.add_action(Editor::go_to_definition); + cx.add_action(Editor::go_to_type_definition); cx.add_action(Editor::page_up); cx.add_action(Editor::page_down); cx.add_action(Editor::fold); @@ -755,9 +757,11 @@ impl CompletionsMenu { .collect() }; matches.sort_unstable_by_key(|mat| { + let completion = &self.completions[mat.candidate_id]; ( + completion.lsp_completion.sort_text.as_ref(), Reverse(OrderedFloat(mat.score)), - self.completions[mat.candidate_id].sort_key(), + completion.sort_key(), ) }); @@ -877,6 +881,7 @@ struct ActiveDiagnosticGroup { pub struct ClipboardSelection { pub len: usize, pub is_entire_line: bool, + pub first_line_indent: u32, } #[derive(Debug)] @@ -892,6 +897,11 @@ pub struct NavigationData { pub struct EditorCreated(pub ViewHandle); +enum GotoDefinitionKind { + Symbol, + Type, +} + impl Editor { pub fn single_line( field_editor_style: Option, @@ -1462,7 +1472,8 @@ impl Editor { S: ToOffset, T: Into>, { - self.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) @@ -1471,8 +1482,9 @@ impl Editor { S: ToOffset, T: Into>, { - self.buffer - .update(cx, |buffer, cx| buffer.edit_with_autoindent(edits, cx)); + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, Some(AutoindentMode::EachLine), cx) + }); } fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { @@ -1885,9 +1897,7 @@ impl Editor { .unzip() }; - this.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(edits, cx); - }); + this.edit_with_autoindent(edits, cx); let buffer = this.buffer.read(cx).snapshot(cx); let new_selections = selection_fixup_info .into_iter() @@ -1920,10 +1930,11 @@ impl Editor { }) .collect::>() }; - buffer.edit_with_autoindent( + buffer.edit( old_selections .iter() .map(|s| (s.start..s.end, text.clone())), + Some(AutoindentMode::EachLine), cx, ); anchors @@ -1984,6 +1995,7 @@ impl Editor { (s.end.clone()..s.end.clone(), pair_end.clone()), ] }), + None, cx, ); }); @@ -2059,6 +2071,7 @@ impl Editor { selection_ranges .iter() .map(|range| (range.clone(), pair_end.clone())), + None, cx, ); snapshot = buffer.snapshot(cx); @@ -2361,8 +2374,11 @@ impl Editor { this.insert_snippet(&ranges, snippet, cx).log_err(); } else { this.buffer.update(cx, |buffer, cx| { - buffer - .edit_with_autoindent(ranges.iter().map(|range| (range.clone(), text)), cx); + buffer.edit( + ranges.iter().map(|range| (range.clone(), text)), + Some(AutoindentMode::EachLine), + cx, + ); }); } }); @@ -2723,11 +2739,12 @@ impl Editor { ) -> Result<()> { let tabstops = self.buffer.update(cx, |buffer, cx| { let snippet_text: Arc = snippet.text.clone().into(); - buffer.edit_with_autoindent( + buffer.edit( insertion_ranges .iter() .cloned() .map(|range| (range, snippet_text.clone())), + Some(AutoindentMode::EachLine), cx, ); @@ -2931,7 +2948,11 @@ impl Editor { let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); IndentSize::spaces(chars_to_next_tab_stop) }; - buffer.edit([(cursor..cursor, tab_size.chars().collect::())], cx); + buffer.edit( + [(cursor..cursor, tab_size.chars().collect::())], + None, + cx, + ); cursor.column += tab_size.len; selection.start = cursor; selection.end = cursor; @@ -3004,6 +3025,7 @@ impl Editor { row_start..row_start, indent_delta.chars().collect::(), )], + None, cx, ); @@ -3078,6 +3100,7 @@ impl Editor { deletion_ranges .into_iter() .map(|range| (range, empty_str.clone())), + None, cx, ); }); @@ -3143,6 +3166,7 @@ impl Editor { edit_ranges .into_iter() .map(|range| (range, empty_str.clone())), + None, cx, ); buffer.snapshot(cx) @@ -3200,7 +3224,7 @@ impl Editor { self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, cx); + buffer.edit(edits, None, cx); }); this.request_autoscroll(Autoscroll::Fit, cx); @@ -3309,7 +3333,7 @@ impl Editor { this.unfold_ranges(unfold_ranges, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } }); this.fold_ranges(refold_ranges, cx); @@ -3414,7 +3438,7 @@ impl Editor { this.unfold_ranges(unfold_ranges, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } }); this.fold_ranges(refold_ranges, cx); @@ -3465,7 +3489,8 @@ impl Editor { }); edits }); - this.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + this.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); let selections = this.selections.all::(cx); this.change_selections(Some(Autoscroll::Fit), cx, |s| { s.select(selections); @@ -3495,6 +3520,7 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, + first_line_indent: buffer.indent_size_for_line(selection.start.row).len, }); } } @@ -3532,6 +3558,7 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, + first_line_indent: buffer.indent_size_for_line(start.row).len, }); } } @@ -3566,18 +3593,22 @@ impl Editor { let snapshot = buffer.read(cx); let mut start_offset = 0; let mut edits = Vec::new(); + let mut start_columns = Vec::new(); let line_mode = this.selections.line_mode; for (ix, selection) in old_selections.iter().enumerate() { let to_insert; let entire_line; + let start_column; if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; entire_line = clipboard_selection.is_entire_line; start_offset = end_offset; + start_column = clipboard_selection.first_line_indent; } else { to_insert = clipboard_text.as_str(); entire_line = all_selections_were_entire_line; + start_column = 0; } // If the corresponding selection was empty when this slice of the @@ -3593,9 +3624,16 @@ impl Editor { }; edits.push((range, to_insert)); + start_columns.push(start_column); } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns: start_columns, + }), + cx, + ); }); let selections = this.selections.all::(cx); @@ -4430,6 +4468,7 @@ impl Editor { .iter() .cloned() .map(|range| (range, empty_str.clone())), + None, cx, ); } else { @@ -4439,7 +4478,7 @@ impl Editor { let position = Point::new(range.start.row, min_column); (position..position, full_comment_prefix.clone()) }); - buffer.edit(edits, cx); + buffer.edit(edits, None, cx); } } } @@ -4661,6 +4700,22 @@ impl Editor { workspace: &mut Workspace, _: &GoToDefinition, cx: &mut ViewContext, + ) { + Self::go_to_definition_of_kind(GotoDefinitionKind::Symbol, workspace, cx); + } + + pub fn go_to_type_definition( + workspace: &mut Workspace, + _: &GoToTypeDefinition, + cx: &mut ViewContext, + ) { + Self::go_to_definition_of_kind(GotoDefinitionKind::Type, workspace, cx); + } + + fn go_to_definition_of_kind( + kind: GotoDefinitionKind, + workspace: &mut Workspace, + cx: &mut ViewContext, ) { let active_item = workspace.active_item(cx); let editor_handle = if let Some(editor) = active_item @@ -4682,7 +4737,11 @@ impl Editor { }; let project = workspace.project().clone(); - let definitions = project.update(cx, |project, cx| project.definition(&buffer, head, cx)); + let definitions = project.update(cx, |project, cx| match kind { + GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx), + GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx), + }); + cx.spawn(|workspace, mut cx| async move { let definitions = definitions.await?; workspace.update(&mut cx, |workspace, cx| { @@ -4873,9 +4932,9 @@ impl Editor { editor.override_text_style = Some(Box::new(move |style| old_highlight_id.style(&style.syntax))); } - editor - .buffer - .update(cx, |buffer, cx| buffer.edit([(0..0, old_name.clone())], cx)); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); editor.select_all(&SelectAll, cx); editor }); @@ -6656,8 +6715,8 @@ mod tests { // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { buffer.start_transaction_at(now, cx); - buffer.edit([(0..1, "a")], cx); - buffer.edit([(1..1, "b")], cx); + buffer.edit([(0..1, "a")], None, cx); + buffer.edit([(1..1, "b")], None, cx); buffer.end_transaction_at(now, cx); }); @@ -7198,6 +7257,7 @@ mod tests { (Point::new(1, 0)..Point::new(1, 0), "\t"), (Point::new(1, 1)..Point::new(1, 1), "\t"), ], + None, cx, ); }); @@ -7834,6 +7894,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 0), ""), (Point::new(4, 2)..Point::new(6, 0), ""), ], + None, cx, ); assert_eq!( @@ -7892,7 +7953,7 @@ mod tests { // Edit the buffer directly, deleting ranges surrounding the editor's selections buffer.update(cx, |buffer, cx| { - buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], cx); + buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); }); @@ -8629,6 +8690,118 @@ mod tests { t|he lazy dog"}); } + #[gpui::test] + async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Cut an indented block, without the leading whitespace. + cx.set_state(indoc! {" + const a = ( + b(), + [c( + d, + e + )} + ); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + | + ); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + c( + d, + e + )| + ); + "}); + + // Paste it at a line with a lower indent level. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.set_state(indoc! {" + | + const a = ( + b(), + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + c( + d, + e + )| + const a = ( + b(), + ); + "}); + + // Cut an indented block, with the leading whitespace. + cx.set_state(indoc! {" + const a = ( + b(), + [ c( + d, + e + ) + }); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + |); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + c( + d, + e + ) + |); + "}); + + // Paste it at a line with a higher indent level. + cx.set_state(indoc! {" + const a = ( + b(), + c( + d, + e| + ) + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.set_state(indoc! {" + const a = ( + b(), + c( + d, + ec( + d, + e + )| + ) + ); + "}); + } + #[gpui::test] fn test_select_all(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b8bee49d8d8feb79120bfa461b2bf7727f8a0b11..fa4c12667142c6ee86fbff1dd80a87660862c8a0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6,7 +6,9 @@ use super::{ use crate::{ display_map::{BlockStyle, DisplaySnapshot, TransformBlock}, hover_popover::HoverAt, - link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink}, + link_go_to_definition::{ + CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink, + }, mouse_context_menu::DeployMouseContextMenu, EditorStyle, }; @@ -122,7 +124,12 @@ impl EditorElement { if cmd && paint.text_bounds.contains_point(position) { let (point, overshoot) = paint.point_for_position(&self.snapshot(cx), layout, position); if overshoot.is_zero() { - cx.dispatch_action(GoToFetchedDefinition { point }); + if shift { + cx.dispatch_action(GoToFetchedTypeDefinition { point }); + } else { + cx.dispatch_action(GoToFetchedDefinition { point }); + } + return true; } } @@ -238,8 +245,12 @@ impl EditorElement { fn mouse_moved( &self, - position: Vector2F, - cmd: bool, + MouseMovedEvent { + cmd, + shift, + position, + .. + }: MouseMovedEvent, layout: &LayoutState, paint: &PaintState, cx: &mut EventContext, @@ -260,6 +271,7 @@ impl EditorElement { cx.dispatch_action(UpdateGoToDefinitionLink { point, cmd_held: cmd, + shift_held: shift, }); if paint @@ -283,8 +295,11 @@ impl EditorElement { true } - fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool { - cx.dispatch_action(CmdChanged { cmd_down: cmd }); + fn modifiers_changed(&self, event: ModifiersChangedEvent, cx: &mut EventContext) -> bool { + cx.dispatch_action(CmdShiftChanged { + cmd_down: event.cmd, + shift_down: event.shift, + }); false } @@ -1534,32 +1549,34 @@ impl Element for EditorElement { paint, cx, ), + Event::MouseDown(MouseButtonEvent { button: MouseButton::Right, position, .. }) => self.mouse_right_down(*position, layout, paint, cx), + Event::MouseUp(MouseButtonEvent { button: MouseButton::Left, position, .. }) => self.mouse_up(*position, cx), + Event::MouseMoved(MouseMovedEvent { pressed_button: Some(MouseButton::Left), position, .. }) => self.mouse_dragged(*position, layout, paint, cx), + Event::ScrollWheel(ScrollWheelEvent { position, delta, precise, }) => self.scroll(*position, *delta, *precise, layout, paint, cx), - Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => { - self.modifiers_changed(*cmd, cx) - } - Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => { - self.mouse_moved(*position, *cmd, layout, paint, cx) - } + + &Event::ModifiersChanged(event) => self.modifiers_changed(event, cx), + + &Event::MouseMoved(event) => self.mouse_moved(event, layout, paint, cx), _ => false, } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index f0dc359b4b58843b51882279beb093ed074537cb..b57179c07dca5cf3362b44353fd6d9997014ddc6 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -8,18 +8,21 @@ use util::TryFutureExt; use workspace::Workspace; use crate::{ - Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, Select, SelectPhase, + Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, GoToTypeDefinition, + Select, SelectPhase, }; #[derive(Clone, PartialEq)] pub struct UpdateGoToDefinitionLink { pub point: Option, pub cmd_held: bool, + pub shift_held: bool, } #[derive(Clone, PartialEq)] -pub struct CmdChanged { +pub struct CmdShiftChanged { pub cmd_down: bool, + pub shift_down: bool, } #[derive(Clone, PartialEq)] @@ -27,28 +30,44 @@ pub struct GoToFetchedDefinition { pub point: DisplayPoint, } +#[derive(Clone, PartialEq)] +pub struct GoToFetchedTypeDefinition { + pub point: DisplayPoint, +} + impl_internal_actions!( editor, - [UpdateGoToDefinitionLink, CmdChanged, GoToFetchedDefinition] + [ + UpdateGoToDefinitionLink, + CmdShiftChanged, + GoToFetchedDefinition, + GoToFetchedTypeDefinition + ] ); pub fn init(cx: &mut MutableAppContext) { cx.add_action(update_go_to_definition_link); - cx.add_action(cmd_changed); + cx.add_action(cmd_shift_changed); cx.add_action(go_to_fetched_definition); + cx.add_action(go_to_fetched_type_definition); } #[derive(Default)] pub struct LinkGoToDefinitionState { pub last_mouse_location: Option, pub symbol_range: Option>, + pub kind: Option, pub definitions: Vec, pub task: Option>>, } pub fn update_go_to_definition_link( editor: &mut Editor, - &UpdateGoToDefinitionLink { point, cmd_held }: &UpdateGoToDefinitionLink, + &UpdateGoToDefinitionLink { + point, + cmd_held, + shift_held, + }: &UpdateGoToDefinitionLink, cx: &mut ViewContext, ) { // Store new mouse point as an anchor @@ -72,7 +91,13 @@ pub fn update_go_to_definition_link( editor.link_go_to_definition_state.last_mouse_location = point.clone(); if cmd_held { if let Some(point) = point { - show_link_definition(editor, point, snapshot, cx); + let kind = if shift_held { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + }; + + show_link_definition(kind, editor, point, snapshot, cx); return; } } @@ -80,9 +105,12 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } -pub fn cmd_changed( +pub fn cmd_shift_changed( editor: &mut Editor, - &CmdChanged { cmd_down }: &CmdChanged, + &CmdShiftChanged { + cmd_down, + shift_down, + }: &CmdShiftChanged, cx: &mut ViewContext, ) { if let Some(point) = editor @@ -92,19 +120,37 @@ pub fn cmd_changed( { if cmd_down { let snapshot = editor.snapshot(cx); - show_link_definition(editor, point.clone(), snapshot, cx); + let kind = if shift_down { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + }; + + show_link_definition(kind, editor, point.clone(), snapshot, cx); } else { hide_link_definition(editor, cx) } } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LinkDefinitionKind { + Symbol, + Type, +} + pub fn show_link_definition( + definition_kind: LinkDefinitionKind, editor: &mut Editor, trigger_point: Anchor, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { + let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind); + if !same_kind { + hide_link_definition(editor, cx); + } + if editor.pending_rename.is_some() { return; } @@ -135,17 +181,20 @@ pub fn show_link_definition( return; }; - // Don't request again if the location is within the symbol region of a previous request + // Don't request again if the location is within the symbol region of a previous request with the same kind if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { - if symbol_range + let point_after_start = symbol_range .start .cmp(&trigger_point, &snapshot.buffer_snapshot) - .is_le() - && symbol_range - .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) - .is_ge() - { + .is_le(); + + let point_before_end = symbol_range + .end + .cmp(&trigger_point, &snapshot.buffer_snapshot) + .is_ge(); + + let point_within_range = point_after_start && point_before_end; + if point_within_range && same_kind { return; } } @@ -154,8 +203,14 @@ pub fn show_link_definition( async move { // query the LSP for definition info let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| { - project.definition(&buffer, buffer_position.clone(), cx) + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position.clone(), cx) + } + + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position.clone(), cx) + } }) }); @@ -181,6 +236,7 @@ pub fn show_link_definition( this.update(&mut cx, |this, cx| { // Clear any existing highlights this.clear_text_highlights::(cx); + this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() .and_then(|(symbol_range, _)| symbol_range.clone()); @@ -258,7 +314,24 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( workspace: &mut Workspace, - GoToFetchedDefinition { point }: &GoToFetchedDefinition, + &GoToFetchedDefinition { point }: &GoToFetchedDefinition, + cx: &mut ViewContext, +) { + go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx); +} + +pub fn go_to_fetched_type_definition( + workspace: &mut Workspace, + &GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition, + cx: &mut ViewContext, +) { + go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx); +} + +fn go_to_fetched_definition_of_kind( + kind: LinkDefinitionKind, + workspace: &mut Workspace, + point: DisplayPoint, cx: &mut ViewContext, ) { let active_item = workspace.active_item(cx); @@ -271,13 +344,14 @@ pub fn go_to_fetched_definition( return; }; - let definitions = editor_handle.update(cx, |editor, cx| { + let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| { let definitions = editor.link_go_to_definition_state.definitions.clone(); hide_link_definition(editor, cx); - definitions + (definitions, editor.link_go_to_definition_state.kind) }); - if !definitions.is_empty() { + let is_correct_kind = cached_definitions_kind == Some(kind); + if !cached_definitions.is_empty() && is_correct_kind { editor_handle.update(cx, |editor, cx| { if !editor.focused { cx.focus_self(); @@ -285,7 +359,7 @@ pub fn go_to_fetched_definition( } }); - Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx); + Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx); } else { editor_handle.update(cx, |editor, cx| { editor.select( @@ -298,7 +372,13 @@ pub fn go_to_fetched_definition( ); }); - Editor::go_to_definition(workspace, &GoToDefinition, cx); + match kind { + LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx), + + LinkDefinitionKind::Type => { + Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx) + } + } } } @@ -306,11 +386,128 @@ pub fn go_to_fetched_definition( mod tests { use futures::StreamExt; use indoc::indoc; + use lsp::request::{GotoDefinition, GotoTypeDefinition}; use crate::test::EditorLspTestContext; use super::*; + #[gpui::test] + async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct A; + let v|ariable = A; + "}); + + // Basic hold cmd+shift, expect highlight in region if response contains type definition + let hover_point = cx.display_point(indoc! {" + struct A; + let v|ariable = A; + "}); + let symbol_range = cx.lsp_range(indoc! {" + struct A; + let [variable] = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct [A]; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + // Press cmd+shift to trigger highlight + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + &UpdateGoToDefinitionLink { + point: Some(hover_point), + cmd_held: true, + shift_held: true, + }, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let [variable] = A; + "}); + + // Unpress shift causes highlight to go away (normal goto-definition is not valid here) + cx.update_editor(|editor, cx| { + cmd_shift_changed( + editor, + &CmdShiftChanged { + cmd_down: true, + shift_down: false, + }, + cx, + ); + }); + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let variable = A; + "}); + + // Cmd+shift click without existing definition requests and jumps + let hover_point = cx.display_point(indoc! {" + struct A; + let v|ariable = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct [A]; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.update_workspace(|workspace, cx| { + go_to_fetched_type_definition( + workspace, + &GoToFetchedTypeDefinition { point: hover_point }, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + + cx.assert_editor_state(indoc! {" + struct [A}; + let variable = A; + "}); + } + #[gpui::test] async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { let mut cx = EditorLspTestContext::new_rust( @@ -327,7 +524,8 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Basic hold cmd, expect highlight in region if response contains definition let hover_point = cx.display_point(indoc! {" @@ -335,38 +533,41 @@ mod tests { do_w|ork(); fn do_work() - test();"}); - + test(); + "}); let symbol_range = cx.lsp_range(indoc! {" fn test() [do_work](); fn do_work() - test();"}); + test(); + "}); let target_range = cx.lsp_range(indoc! {" fn test() do_work(); fn [do_work]() - test();"}); + test(); + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - origin_selection_range: Some(symbol_range), - target_uri: url.clone(), - target_range, - target_selection_range: target_range, - }, - ]))) - }); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -378,11 +579,19 @@ mod tests { [do_work](); fn do_work() - test();"}); + test(); + "}); // Unpress cmd causes highlight to go away cx.update_editor(|editor, cx| { - cmd_changed(editor, &CmdChanged { cmd_down: false }, cx); + cmd_shift_changed( + editor, + &CmdShiftChanged { + cmd_down: false, + shift_down: false, + }, + cx, + ); }); // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" @@ -390,28 +599,29 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Response without source range still highlights word cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - // No origin range - origin_selection_range: None, - target_uri: url.clone(), - target_range, - target_selection_range: target_range, - }, - ]))) - }); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + // No origin range + origin_selection_range: None, + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -424,7 +634,8 @@ mod tests { [do_work](); fn do_work() - test();"}); + test(); + "}); // Moving mouse to location with no response dismisses highlight let hover_point = cx.display_point(indoc! {" @@ -432,19 +643,21 @@ mod tests { do_work(); fn do_work() - test();"}); - let mut requests = - cx.lsp - .handle_request::(move |_, _| async move { - // No definitions returned - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) - }); + test(); + "}); + let mut requests = cx + .lsp + .handle_request::(move |_, _| async move { + // No definitions returned + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) + }); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -458,7 +671,8 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Move mouse without cmd and then pressing cmd triggers highlight let hover_point = cx.display_point(indoc! {" @@ -466,13 +680,15 @@ mod tests { do_work(); fn do_work() - te|st();"}); + te|st(); + "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: false, + shift_held: false, }, cx, ); @@ -485,34 +701,43 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); let symbol_range = cx.lsp_range(indoc! {" fn test() do_work(); fn do_work() - [test]();"}); + [test](); + "}); let target_range = cx.lsp_range(indoc! {" fn [test]() do_work(); fn do_work() - test();"}); - - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - origin_selection_range: Some(symbol_range), - target_uri: url, - target_range, - target_selection_range: target_range, - }, - ]))) - }); + test(); + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); cx.update_editor(|editor, cx| { - cmd_changed(editor, &CmdChanged { cmd_down: true }, cx); + cmd_shift_changed( + editor, + &CmdShiftChanged { + cmd_down: true, + shift_down: false, + }, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -522,7 +747,8 @@ mod tests { do_work(); fn do_work() - [test]();"}); + [test](); + "}); // Moving within symbol range doesn't re-request let hover_point = cx.display_point(indoc! {" @@ -530,13 +756,15 @@ mod tests { do_work(); fn do_work() - tes|t();"}); + tes|t(); + "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, &UpdateGoToDefinitionLink { point: Some(hover_point), cmd_held: true, + shift_held: false, }, cx, ); @@ -547,7 +775,8 @@ mod tests { do_work(); fn do_work() - [test]();"}); + [test](); + "}); // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_workspace(|workspace, cx| { @@ -555,7 +784,7 @@ mod tests { }); // Assert selection moved to to definition cx.lsp - .handle_request::(move |_, _| async move { + .handle_request::(move |_, _| async move { // Empty definition response to make sure we aren't hitting the lsp and using // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) @@ -565,14 +794,16 @@ mod tests { do_work(); fn do_work() - test();"}); + test(); + "}); // Assert no link highlights after jump cx.assert_editor_text_highlights::(indoc! {" fn test() do_work(); fn do_work() - test();"}); + test(); + "}); // Cmd click without existing definition requests and jumps let hover_point = cx.display_point(indoc! {" @@ -580,25 +811,26 @@ mod tests { do_w|ork(); fn do_work() - test();"}); + test(); + "}); let target_range = cx.lsp_range(indoc! {" fn test() do_work(); fn [do_work]() - test();"}); - - let mut requests = - cx.handle_request::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ - lsp::LocationLink { - origin_selection_range: None, - target_uri: url, - target_range, - target_selection_range: target_range, - }, - ]))) - }); + test(); + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); cx.update_workspace(|workspace, cx| { go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx); }); @@ -610,6 +842,7 @@ mod tests { do_work(); fn [do_work}() - test();"}); + test(); + "}); } } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index ce2faf8fa63c40c445086048408f3b5be47b8895..513a9ed99ce06e0ac25d2f70d5f4d829155f1426 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -2,8 +2,8 @@ use context_menu::ContextMenuItem; use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext}; use crate::{ - DisplayPoint, Editor, EditorMode, Event, FindAllReferences, GoToDefinition, Rename, SelectMode, - ToggleCodeActions, + DisplayPoint, Editor, EditorMode, Event, FindAllReferences, GoToDefinition, GoToTypeDefinition, + Rename, SelectMode, ToggleCodeActions, }; #[derive(Clone, PartialEq)] @@ -50,6 +50,7 @@ pub fn deploy_context_menu( vec![ ContextMenuItem::item("Rename Symbol", Rename), ContextMenuItem::item("Go To Definition", GoToDefinition), + ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition), ContextMenuItem::item("Find All References", FindAllReferences), ContextMenuItem::item( "Code Actions", diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 00ef7b11a0943d30deefc0c9a21f2e666da25c45..1fc7cf0560c262106aad9f63a6504d4ea57f40b0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,11 +7,10 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, - IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, - ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, + DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, + Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; -use settings::Settings; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -303,28 +302,10 @@ impl MultiBuffer { self.read(cx).symbols_containing(offset, theme) } - pub fn edit(&mut self, edits: I, cx: &mut ModelContext) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits, false, cx) - } - - pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ModelContext) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits, true, cx) - } - - pub fn edit_internal( + pub fn edit( &mut self, edits: I, - autoindent: bool, + mut autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, @@ -346,26 +327,23 @@ impl MultiBuffer { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| { - if autoindent { - let language_name = buffer.language().map(|language| language.name()); - let settings = cx.global::(); - let indent_size = if settings.hard_tabs(language_name.as_deref()) { - IndentSize::tab() - } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) - }; - buffer.edit_with_autoindent(edits, indent_size, cx); - } else { - buffer.edit(edits, cx); - } + buffer.edit(edits, autoindent_mode, cx); }); } - let mut buffer_edits: HashMap, Arc, bool)>> = + let original_indent_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) => mem::take(original_indent_columns), + _ => Default::default(), + }; + + let mut buffer_edits: HashMap, Arc, bool, u32)>> = Default::default(); let mut cursor = snapshot.excerpts.cursor::(); - for (range, new_text) in edits { + for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); + let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); cursor.seek(&range.start, Bias::Right, &()); if cursor.item().is_none() && range.start == *cursor.start() { cursor.prev(&()); @@ -396,7 +374,12 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((buffer_start..buffer_end, new_text, true)); + .push(( + buffer_start..buffer_end, + new_text, + true, + original_indent_column, + )); } else { let start_excerpt_range = buffer_start ..start_excerpt @@ -413,11 +396,21 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((start_excerpt_range, new_text.clone(), true)); + .push(( + start_excerpt_range, + new_text.clone(), + true, + original_indent_column, + )); buffer_edits .entry(end_excerpt.buffer_id) .or_insert(Vec::new()) - .push((end_excerpt_range, new_text.clone(), false)); + .push(( + end_excerpt_range, + new_text.clone(), + false, + original_indent_column, + )); cursor.seek(&range.start, Bias::Right, &()); cursor.next(&()); @@ -432,6 +425,7 @@ impl MultiBuffer { excerpt.range.context.to_offset(&excerpt.buffer), new_text.clone(), false, + original_indent_column, )); cursor.next(&()); } @@ -439,19 +433,25 @@ impl MultiBuffer { } for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|(range, _, _)| range.start); + edits.sort_unstable_by_key(|(range, _, _, _)| range.start); self.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { let mut edits = edits.into_iter().peekable(); let mut insertions = Vec::new(); + let mut original_indent_columns = Vec::new(); let mut deletions = Vec::new(); let empty_str: Arc = "".into(); - while let Some((mut range, new_text, mut is_insertion)) = edits.next() { - while let Some((next_range, _, next_is_insertion)) = edits.peek() { + while let Some(( + mut range, + new_text, + mut is_insertion, + original_indent_column, + )) = edits.next() + { + while let Some((next_range, _, next_is_insertion, _)) = edits.peek() { if range.end >= next_range.start { range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; edits.next(); } else { @@ -460,6 +460,7 @@ impl MultiBuffer { } if is_insertion { + original_indent_columns.push(original_indent_column); insertions.push(( buffer.anchor_before(range.start)..buffer.anchor_before(range.end), new_text.clone(), @@ -471,22 +472,26 @@ impl MultiBuffer { )); } } - let language_name = buffer.language().map(|l| l.name()); - if autoindent { - let settings = cx.global::(); - let indent_size = if settings.hard_tabs(language_name.as_deref()) { - IndentSize::tab() + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns: Default::default(), + }) + } else { + None + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) + None }; - buffer.edit_with_autoindent(deletions, indent_size, cx); - buffer.edit_with_autoindent(insertions, indent_size, cx); - } else { - buffer.edit(deletions, cx); - buffer.edit(insertions, cx); - } + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); }) } } @@ -1402,7 +1407,7 @@ impl MultiBuffer { log::info!("mutating multi-buffer with {:?}", edits); drop(snapshot); - self.edit(edits, cx); + self.edit(edits, None, cx); } pub fn randomly_edit_excerpts( @@ -3220,6 +3225,7 @@ mod tests { use gpui::MutableAppContext; use language::{Buffer, Rope}; use rand::prelude::*; + use settings::Settings; use std::{env, rc::Rc}; use text::{Point, RandomCharIter}; use util::test::sample_text; @@ -3239,7 +3245,7 @@ mod tests { .collect::>() ); - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], cx)); + buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), buffer.read(cx).text()); @@ -3262,11 +3268,11 @@ mod tests { let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "a"); - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], cx)); + guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "ab"); - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], cx)); + guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "abc"); } @@ -3407,6 +3413,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 0), text), (Point::new(2, 1)..Point::new(2, 3), text), ], + None, cx, ); }); @@ -3544,8 +3551,8 @@ mod tests { let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], cx); - buffer.edit([(5..5, "Y")], cx); + buffer.edit([(0..0, "X")], None, cx); + buffer.edit([(5..5, "Y")], None, cx); }); let new_snapshot = multibuffer.read(cx).snapshot(cx); @@ -3592,12 +3599,12 @@ mod tests { assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], cx); - buffer.edit([(5..5, "X")], cx); + buffer.edit([(0..0, "W")], None, cx); + buffer.edit([(5..5, "X")], None, cx); }); buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], cx); - buffer.edit([(6..6, "Z")], cx); + buffer.edit([(0..0, "Y")], None, cx); + buffer.edit([(6..6, "Z")], None, cx); }); let new_snapshot = multibuffer.read(cx).snapshot(cx); @@ -3626,7 +3633,7 @@ mod tests { // Create an insertion id in buffer 1 that doesn't exist in buffer 2. // Add an excerpt from buffer 1 that spans this new insertion. - buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], cx)); + buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { multibuffer .push_excerpts( @@ -4199,6 +4206,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 0), "A"), (Point::new(1, 0)..Point::new(1, 0), "A"), ], + None, cx, ); multibuffer.edit( @@ -4206,6 +4214,7 @@ mod tests { (Point::new(0, 1)..Point::new(0, 1), "B"), (Point::new(1, 1)..Point::new(1, 1), "B"), ], + None, cx, ); multibuffer.end_transaction_at(now, cx); @@ -4214,19 +4223,19 @@ mod tests { // Edit buffer 1 through the multibuffer now += 2 * group_interval; multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(2..2, "C")], cx); + multibuffer.edit([(2..2, "C")], None, cx); multibuffer.end_transaction_at(now, cx); assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); // Edit buffer 1 independently buffer_1.update(cx, |buffer_1, cx| { buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], cx); + buffer_1.edit([(3..3, "D")], None, cx); buffer_1.end_transaction_at(now, cx); now += 2 * group_interval; buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], cx); + buffer_1.edit([(4..4, "E")], None, cx); buffer_1.end_transaction_at(now, cx); }); assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); @@ -4267,7 +4276,7 @@ mod tests { // Redo stack gets cleared after an edit. now += 2 * group_interval; multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(0..0, "X")], cx); + multibuffer.edit([(0..0, "X")], None, cx); multibuffer.end_transaction_at(now, cx); assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); multibuffer.redo(cx); diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index f6abb22ddc4312f22f8d68013dab4082c7cea0c9..401ab33d7f79a37101e6da93e7baa966dd3ffc09 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -181,7 +181,7 @@ pub async fn match_strings( cancel_flag: &AtomicBool, background: Arc, ) -> Vec { - if candidates.is_empty() { + if candidates.is_empty() || max_results == 0 { return Default::default(); } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 22cb3ba5dea4d357405f2e0d99b1e2efe6242d6a..d32344b0dbc8c2096d4527616316eef9a92e73bc 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -41,6 +41,7 @@ pub struct Keystroke { pub alt: bool, pub shift: bool, pub cmd: bool, + pub function: bool, pub key: String, } @@ -277,6 +278,7 @@ impl Keystroke { let mut alt = false; let mut shift = false; let mut cmd = false; + let mut function = false; let mut key = None; let mut components = source.split("-").peekable(); @@ -286,6 +288,7 @@ impl Keystroke { "alt" => alt = true, "shift" => shift = true, "cmd" => cmd = true, + "fn" => function = true, _ => { if let Some(component) = components.peek() { if component.is_empty() && source.ends_with('-') { @@ -306,6 +309,7 @@ impl Keystroke { alt, shift, cmd, + function, key: key.unwrap(), }) } @@ -464,6 +468,7 @@ mod tests { alt: false, shift: false, cmd: false, + function: false, } ); @@ -475,6 +480,7 @@ mod tests { alt: true, shift: true, cmd: false, + function: false, } ); @@ -486,6 +492,7 @@ mod tests { alt: false, shift: true, cmd: true, + function: false, } ); diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index e833bd8eb5f78ec639222bde1b9b6338e4e2d15a..6ac75926be426e2a7512d35ea9c06cd5c7f64047 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -11,7 +11,7 @@ pub struct KeyUpEvent { pub keystroke: Keystroke, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct ModifiersChangedEvent { pub ctrl: bool, pub alt: bool, @@ -19,7 +19,7 @@ pub struct ModifiersChangedEvent { pub cmd: bool, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct ScrollWheelEvent { pub position: Vector2F, pub delta: Vector2F, diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index e0e178aa8c0833471d5cfb7be37a2a516138bfa6..5aedc63b2e03caecf92d03dfc912263f4b6ca9ca 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -210,19 +210,24 @@ impl Event { unsafe fn parse_keystroke(native_event: id) -> Keystroke { use cocoa::appkit::*; + let mut chars_ignoring_modifiers = + CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) + .to_str() + .unwrap(); + let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); let modifiers = native_event.modifierFlags(); + let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - - let mut chars_ignoring_modifiers = - CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) - .to_str() - .unwrap(); + let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) + && first_char.map_or(true, |ch| { + ch < NSUpArrowFunctionKey || ch > NSModeSwitchFunctionKey + }); #[allow(non_upper_case_globals)] - let key = match chars_ignoring_modifiers.chars().next().map(|ch| ch as u16) { + let key = match first_char { Some(SPACE_KEY) => "space", Some(BACKSPACE_KEY) => "backspace", Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", @@ -282,6 +287,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { alt, shift, cmd, + function, key: key.into(), } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 5dc10c7b57185bd9dc070054c1732ffd18319102..88957575398b8affa6df2d1322df9c462773c6d9 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -184,6 +184,7 @@ impl MacForegroundPlatform { (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), ] { if *modifier { mask |= *flag; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index de175812ba31485c24ebe359e12f30a993951c4e..05cc542b98d6fa8f66fe7fb00b3a1acc66c05508 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -154,6 +154,10 @@ unsafe fn build_classes() { sel!(performKeyEquivalent:), handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL, ); + decl.add_method( + sel!(keyDown:), + handle_key_down as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(mouseDown:), handle_view_event as extern "C" fn(&Object, Sel, id), @@ -275,7 +279,8 @@ struct WindowState { should_close_callback: Option bool>>, close_callback: Option>, input_handler: Option>, - pending_key_down_event: Option, + pending_key_down: Option<(KeyDownEvent, Option)>, + performed_key_equivalent: bool, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, @@ -287,6 +292,11 @@ struct WindowState { previous_modifiers_changed_event: Option, } +struct InsertText { + replacement_range: Option>, + text: String, +} + impl Window { pub fn open( id: usize, @@ -359,7 +369,8 @@ impl Window { close_callback: None, activate_callback: None, input_handler: None, - pending_key_down_event: None, + pending_key_down: None, + performed_key_equivalent: false, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -689,13 +700,28 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { } extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { + handle_key_event(this, native_event, true) +} + +extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) { + handle_key_event(this, native_event, false); +} + +extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL { let window_state = unsafe { get_window_state(this) }; let mut window_state_borrow = window_state.as_ref().borrow_mut(); let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { - window_state_borrow.pending_key_down_event = match event { + if key_equivalent { + window_state_borrow.performed_key_equivalent = true; + } else if window_state_borrow.performed_key_equivalent { + return NO; + } + + let function_is_held; + window_state_borrow.pending_key_down = match event { Event::KeyDown(event) => { let keydown = event.keystroke.clone(); // Ignore events from held-down keys after some of the initially-pressed keys @@ -708,19 +734,23 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> window_state_borrow.last_fresh_keydown = Some(keydown); } - Some(event) + function_is_held = event.keystroke.function; + Some((event, None)) } _ => return NO, }; drop(window_state_borrow); - unsafe { - let input_context: id = msg_send![this, inputContext]; - let _: BOOL = msg_send![input_context, handleEvent: native_event]; + if !function_is_held { + unsafe { + let input_context: id = msg_send![this, inputContext]; + let _: BOOL = msg_send![input_context, handleEvent: native_event]; + } } + let mut handled = false; let mut window_state_borrow = window_state.borrow_mut(); - if let Some(event) = window_state_borrow.pending_key_down_event.take() { + if let Some((event, insert_text)) = window_state_borrow.pending_key_down.take() { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); @@ -729,14 +759,26 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> .flatten() .is_some(); if !is_composing { - callback(Event::KeyDown(event)); + handled = callback(Event::KeyDown(event)); + } + + if !handled { + if let Some(insert) = insert_text { + handled = true; + with_input_handler(this, |input_handler| { + input_handler + .replace_text_in_range(insert.replacement_range, &insert.text) + }); + } } window_state.borrow_mut().event_callback = Some(callback); } + } else { + handled = true; } - YES + handled as BOOL } else { NO } @@ -819,6 +861,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { ctrl: false, alt: false, shift: false, + function: false, key: ".".into(), }; let event = Event::KeyDown(KeyDownEvent { @@ -837,6 +880,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { extern "C" fn send_event(this: &Object, _: Sel, native_event: id) { unsafe { let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event]; + get_window_state(this).borrow_mut().performed_key_equivalent = false; } } @@ -1042,7 +1086,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS unsafe { let window_state = get_window_state(this); let mut window_state_borrow = window_state.borrow_mut(); - let pending_key_down_event = window_state_borrow.pending_key_down_event.take(); + let pending_key_down = window_state_borrow.pending_key_down.take(); drop(window_state_borrow); let is_attributed_string: BOOL = @@ -1062,24 +1106,17 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS .flatten() .is_some(); - if is_composing || text.chars().count() > 1 || pending_key_down_event.is_none() { + if is_composing || text.chars().count() > 1 || pending_key_down.is_none() { with_input_handler(this, |input_handler| { input_handler.replace_text_in_range(replacement_range, text) }); } else { - let mut handled = false; - - let event_callback = window_state.borrow_mut().event_callback.take(); - if let Some(mut event_callback) = event_callback { - handled = event_callback(Event::KeyDown(pending_key_down_event.unwrap())); - window_state.borrow_mut().event_callback = Some(event_callback); - } - - if !handled { - with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(replacement_range, text) - }); - } + let mut pending_key_down = pending_key_down.unwrap(); + pending_key_down.1 = Some(InsertText { + replacement_range, + text: text.to_string(), + }); + window_state.borrow_mut().pending_key_down = Some(pending_key_down); } } } @@ -1092,10 +1129,7 @@ extern "C" fn set_marked_text( replacement_range: NSRange, ) { unsafe { - get_window_state(this) - .borrow_mut() - .pending_key_down_event - .take(); + get_window_state(this).borrow_mut().pending_key_down.take(); let is_attributed_string: BOOL = msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index c70ad6b7311576cacf7c509f8dc6d10f15aa2f49..6e9f368e77be909c1a8fb2d149be679b3aa0b66b 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -16,6 +16,7 @@ test-support = [ "text/test-support", "tree-sitter-rust", "tree-sitter-typescript", + "settings/test-support", "util/test-support", ] @@ -27,6 +28,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rpc = { path = "../rpc" } +settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } theme = { path = "../theme" } @@ -56,6 +58,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index bff9438124313f53d961c4e1bf0cee5f3c9cee35..e6b0d48820b0db06de9cf96c4c887dbaff561607 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,12 +14,13 @@ use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; +use settings::Settings; use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; use std::{ any::Any, cmp::{self, Ordering}, - collections::{BTreeMap, HashMap}, + collections::BTreeMap, ffi::OsStr, future::Future, iter::{self, Iterator, Peekable}, @@ -228,12 +229,37 @@ struct SyntaxTree { version: clock::Global, } +#[derive(Clone, Debug)] +pub enum AutoindentMode { + /// Indent each line of inserted text. + EachLine, + /// Apply the same indentation adjustment to all of the lines + /// in a given insertion. + Block { + /// The original indentation level of the first line of each + /// insertion, if it has been copied. + original_indent_columns: Vec, + }, +} + #[derive(Clone)] struct AutoindentRequest { before_edit: BufferSnapshot, - edited: Vec, - inserted: Option>>, + entries: Vec, indent_size: IndentSize, + is_block_mode: bool, +} + +#[derive(Clone)] +struct AutoindentRequestEntry { + /// A range of the buffer whose indentation should be adjusted. + range: Range, + /// Whether or not these lines should be considered brand new, for the + /// purpose of auto-indent. When text is not new, its indentation will + /// only be adjusted if the suggested indentation level has *changed* + /// since the edit was made. + first_line_is_new: bool, + original_indent_column: Option, } #[derive(Debug)] @@ -796,19 +822,25 @@ impl Buffer { Some(async move { let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { - let old_to_new_rows = request - .edited - .iter() - .map(|anchor| anchor.summary::(&request.before_edit).row) - .zip( - request - .edited - .iter() - .map(|anchor| anchor.summary::(&snapshot).row), - ) - .collect::>(); - - let mut old_suggestions = HashMap::::default(); + // Resolve each edited range to its row in the current buffer and in the + // buffer before this batch of edits. + let mut row_ranges = Vec::new(); + let mut old_to_new_rows = BTreeMap::new(); + for entry in &request.entries { + let position = entry.range.start; + let new_row = position.to_point(&snapshot).row; + let new_end_row = entry.range.end.to_point(&snapshot).row + 1; + if !entry.first_line_is_new { + let old_row = position.to_point(&request.before_edit).row; + old_to_new_rows.insert(old_row, new_row); + } + row_ranges.push((new_row..new_end_row, entry.original_indent_column)); + } + + // Build a map containing the suggested indentation for each of the edited lines + // with respect to the state of the buffer before these edits. This map is keyed + // by the rows for these lines in the current state of the buffer. + let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); for old_edited_range in old_edited_ranges { @@ -819,19 +851,15 @@ impl Buffer { .flatten(); for (old_row, suggestion) in old_edited_range.zip(suggestions) { if let Some(suggestion) = suggestion { - let mut suggested_indent = old_to_new_rows + let suggested_indent = old_to_new_rows .get(&suggestion.basis_row) .and_then(|from_row| old_suggestions.get(from_row).copied()) .unwrap_or_else(|| { request .before_edit .indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } + }) + .with_delta(suggestion.delta, request.indent_size); old_suggestions .insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent); } @@ -839,10 +867,21 @@ impl Buffer { yield_now().await; } - // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the - // buffer before the edit, but keyed by the row for these lines after the edits were applied. - let new_edited_row_ranges = - contiguous_ranges(old_to_new_rows.values().copied(), max_rows_between_yields); + // In block mode, only compute indentation suggestions for the first line + // of each insertion. Otherwise, compute suggestions for every inserted line. + let new_edited_row_ranges = contiguous_ranges( + row_ranges.iter().flat_map(|(range, _)| { + if request.is_block_mode { + range.start..range.start + 1 + } else { + range.clone() + } + }), + max_rows_between_yields, + ); + + // Compute new suggestions for each line, but only include them in the result + // if they differ from the old suggestion for that line. for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -850,17 +889,13 @@ impl Buffer { .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { if let Some(suggestion) = suggestion { - let mut suggested_indent = indent_sizes + let suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() .unwrap_or_else(|| { snapshot.indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } + }) + .with_delta(suggestion.delta, request.indent_size); if old_suggestions .get(&new_row) .map_or(true, |old_indentation| { @@ -874,36 +909,40 @@ impl Buffer { yield_now().await; } - if let Some(inserted) = request.inserted.as_ref() { - let inserted_row_ranges = contiguous_ranges( - inserted - .iter() - .map(|range| range.to_point(&snapshot)) - .flat_map(|range| range.start.row..range.end.row + 1), - max_rows_between_yields, - ); - for inserted_row_range in inserted_row_ranges { - let suggestions = snapshot - .suggest_autoindents(inserted_row_range.clone()) + // For each block of inserted text, adjust the indentation of the remaining + // lines of the block by the same amount as the first line was adjusted. + if request.is_block_mode { + for (row_range, original_indent_column) in + row_ranges .into_iter() - .flatten(); - for (row, suggestion) in inserted_row_range.zip(suggestions) { - if let Some(suggestion) = suggestion { - let mut suggested_indent = indent_sizes - .get(&suggestion.basis_row) - .copied() - .unwrap_or_else(|| { - snapshot.indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; + .filter_map(|(range, original_indent_column)| { + if range.len() > 1 { + Some((range, original_indent_column?)) + } else { + None } - indent_sizes.insert(row, suggested_indent); + }) + { + let new_indent = indent_sizes + .get(&row_range.start) + .copied() + .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); + let delta = new_indent.len as i64 - original_indent_column as i64; + if delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent.kind { + if delta > 0 { + size.len = size.len + delta as u32; + } else if delta < 0 { + size.len = size.len.saturating_sub(-delta as u32); + } + } + size + }); } } - yield_now().await; } } } @@ -945,6 +984,7 @@ impl Buffer { .take((size.len - current_size.len) as usize) .collect::(), )], + None, cx, ); } else if size.len < current_size.len { @@ -953,6 +993,7 @@ impl Buffer { Point::new(row, 0)..Point::new(row, current_size.len - size.len), "", )], + None, cx, ); } @@ -990,7 +1031,7 @@ impl Buffer { match tag { ChangeTag::Equal => offset += len, ChangeTag::Delete => { - self.edit([(range, "")], cx); + self.edit([(range, "")], None, cx); } ChangeTag::Insert => { self.edit( @@ -999,6 +1040,7 @@ impl Buffer { &diff.new_text[range.start - diff.start_offset ..range.end - diff.start_offset], )], + None, cx, ); offset += len; @@ -1135,40 +1177,13 @@ impl Buffer { where T: Into>, { - self.edit_internal([(0..self.len(), text)], None, cx) + self.edit([(0..self.len(), text)], None, cx) } pub fn edit( &mut self, edits_iter: I, - cx: &mut ModelContext, - ) -> Option - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits_iter, None, cx) - } - - pub fn edit_with_autoindent( - &mut self, - edits_iter: I, - indent_size: IndentSize, - cx: &mut ModelContext, - ) -> Option - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits_iter, Some(indent_size), cx) - } - - pub fn edit_internal( - &mut self, - edits_iter: I, - autoindent_size: Option, + autoindent_mode: Option, cx: &mut ModelContext, ) -> Option where @@ -1203,58 +1218,79 @@ impl Buffer { self.start_transaction(); self.pending_autoindent.take(); - let autoindent_request = - self.language - .as_ref() - .and_then(|_| autoindent_size) - .map(|autoindent_size| { - let before_edit = self.snapshot(); - let edited = edits - .iter() - .filter_map(|(range, new_text)| { - let start = range.start.to_point(self); - if new_text.starts_with('\n') - && start.column == self.line_len(start.row) - { - None - } else { - Some(self.anchor_before(range.start)) - } - }) - .collect(); - (before_edit, edited, autoindent_size) - }); + let autoindent_request = autoindent_mode + .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); let edit_operation = self.text.edit(edits.iter().cloned()); let edit_id = edit_operation.local_timestamp(); - if let Some((before_edit, edited, size)) = autoindent_request { - let mut delta = 0isize; + if let Some((before_edit, mode)) = autoindent_request { + let language_name = self.language().map(|language| language.name()); + let settings = cx.global::(); + let indent_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) + }; + let (start_columns, is_block_mode) = match mode { + AutoindentMode::Block { + original_indent_columns: start_columns, + } => (start_columns, true), + AutoindentMode::EachLine => (Default::default(), false), + }; - let inserted_ranges = edits + let mut delta = 0isize; + let entries = edits .into_iter() + .enumerate() .zip(&edit_operation.as_edit().unwrap().new_text) - .filter_map(|((range, _), new_text)| { - let first_newline_ix = new_text.find('\n')?; + .map(|((ix, (range, _)), new_text)| { let new_text_len = new_text.len(); - let start = (delta + range.start as isize) as usize + first_newline_ix + 1; - let end = (delta + range.start as isize) as usize + new_text_len; + let old_start = range.start.to_point(&before_edit); + let new_start = (delta + range.start as isize) as usize; delta += new_text_len as isize - (range.end as isize - range.start as isize); - Some(self.anchor_before(start)..self.anchor_after(end)) - }) - .collect::>>(); - let inserted = if inserted_ranges.is_empty() { - None - } else { - Some(inserted_ranges) - }; + let mut range_of_insertion_to_indent = 0..new_text_len; + let mut first_line_is_new = false; + let mut start_column = None; + + // When inserting an entire line at the beginning of an existing line, + // treat the insertion as new. + if new_text.contains('\n') + && old_start.column <= before_edit.indent_size_for_line(old_start.row).len + { + first_line_is_new = true; + } + + // When inserting text starting with a newline, avoid auto-indenting the + // previous line. + if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') { + range_of_insertion_to_indent.start += 1; + first_line_is_new = true; + } + + // Avoid auto-indenting before the insertion. + if is_block_mode { + start_column = start_columns.get(ix).copied(); + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; + } + } + + AutoindentRequestEntry { + first_line_is_new, + original_indent_column: start_column, + range: self.anchor_before(new_start + range_of_insertion_to_indent.start) + ..self.anchor_after(new_start + range_of_insertion_to_indent.end), + } + }) + .collect(); self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, - edited, - inserted, - indent_size: size, + entries, + indent_size, + is_block_mode, })); } @@ -1541,7 +1577,7 @@ impl Buffer { edits.push((range, new_text)); } log::info!("mutating buffer {} with {:?}", self.replica_id(), edits); - self.edit(edits, cx); + self.edit(edits, None, cx); } pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng, cx: &mut ModelContext) { @@ -2139,8 +2175,12 @@ impl BufferSnapshot { } pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { + indent_size_for_text(text.chars_at(Point::new(row, 0))) +} + +pub fn indent_size_for_text(text: impl Iterator) -> IndentSize { let mut result = IndentSize::spaces(0); - for c in text.chars_at(Point::new(row, 0)) { + for c in text { let kind = match c { ' ' => IndentKind::Space, '\t' => IndentKind::Tab, @@ -2503,23 +2543,24 @@ impl IndentSize { IndentKind::Tab => '\t', } } -} -impl std::ops::AddAssign for IndentSize { - fn add_assign(&mut self, other: IndentSize) { - if self.len == 0 { - *self = other; - } else if self.kind == other.kind { - self.len += other.len; - } - } -} - -impl std::ops::SubAssign for IndentSize { - fn sub_assign(&mut self, other: IndentSize) { - if self.kind == other.kind && self.len >= other.len { - self.len -= other.len; + pub fn with_delta(mut self, direction: Ordering, size: IndentSize) -> Self { + match direction { + Ordering::Less => { + if self.kind == size.kind && self.len >= size.len { + self.len -= size.len; + } + } + Ordering::Equal => {} + Ordering::Greater => { + if self.len == 0 { + self = size; + } else if self.kind == size.kind { + self.len += size.len; + } + } } + self } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index ac3759c257b21f7deafcd8ef011805208fc9699a..937ff069305cabe280d4d5de5949ea5423181054 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -3,6 +3,7 @@ use clock::ReplicaId; use collections::BTreeMap; use gpui::{ModelHandle, MutableAppContext}; use rand::prelude::*; +use settings::Settings; use std::{ cell::RefCell, env, @@ -24,6 +25,7 @@ fn init_logger() { #[gpui::test] fn test_line_endings(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); @@ -31,12 +33,12 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); - buffer.edit_with_autoindent( + buffer.edit( [(buffer.len()..buffer.len(), "\r\nfour")], - IndentSize::spaces(2), + Some(AutoindentMode::EachLine), cx, ); - buffer.edit([(0..0, "zero\r\n")], cx); + buffer.edit([(0..0, "zero\r\n")], None, cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); @@ -116,7 +118,7 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { // An edit emits an edited event, followed by a dirty changed event, // since the buffer was previously in a clean state. - buffer.edit([(2..4, "XYZ")], cx); + buffer.edit([(2..4, "XYZ")], None, cx); // An empty transaction does not emit any events. buffer.start_transaction(); @@ -125,8 +127,8 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { // A transaction containing two edits emits one edited event. now += Duration::from_secs(1); buffer.start_transaction_at(now); - buffer.edit([(5..5, "u")], cx); - buffer.edit([(6..6, "w")], cx); + buffer.edit([(5..5, "u")], None, cx); + buffer.edit([(6..6, "w")], None, cx); buffer.end_transaction_at(now, cx); // Undoing a transaction emits one edited event. @@ -226,11 +228,11 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { buf.start_transaction(); let offset = buf.text().find(")").unwrap(); - buf.edit([(offset..offset, "b: C")], cx); + buf.edit([(offset..offset, "b: C")], None, cx); assert!(!buf.is_parsing()); let offset = buf.text().find("}").unwrap(); - buf.edit([(offset..offset, " d; ")], cx); + buf.edit([(offset..offset, " d; ")], None, cx); assert!(!buf.is_parsing()); buf.end_transaction(cx); @@ -255,19 +257,19 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { // * add a turbofish to the method call buffer.update(cx, |buf, cx| { let offset = buf.text().find(";").unwrap(); - buf.edit([(offset..offset, ".e")], cx); + buf.edit([(offset..offset, ".e")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find(";").unwrap(); - buf.edit([(offset..offset, "(f)")], cx); + buf.edit([(offset..offset, "(f)")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find("(f)").unwrap(); - buf.edit([(offset..offset, "::")], cx); + buf.edit([(offset..offset, "::")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); assert!(buf.is_parsing()); }); @@ -545,6 +547,7 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| { let text = " mod x { @@ -620,34 +623,37 @@ fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { + let settings = Settings::test(cx); + cx.set_global(settings); + cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::spaces(4), cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(2, 4)..Point::new(2, 4), ".c")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(2, 8)..Point::new(2, 9), "")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); @@ -658,34 +664,38 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { + let mut settings = Settings::test(cx); + settings.editor_overrides.hard_tabs = Some(true); + cx.set_global(settings); + cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::tab(), cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], - IndentSize::tab(), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(2, 1)..Point::new(2, 1), ".c")], - IndentSize::tab(), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(2, 2)..Point::new(2, 3), "")], - IndentSize::tab(), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); @@ -696,6 +706,9 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) { + let settings = Settings::test(cx); + cx.set_global(settings); + cx.add_model(|cx| { let text = " fn a() { @@ -709,12 +722,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, // their indentation is not adjusted. - buffer.edit_with_autoindent( + buffer.edit( [ (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -730,12 +743,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // When appending new content after these lines, the indentation is based on the // preceding lines' actual indentation. - buffer.edit_with_autoindent( + buffer.edit( [ (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -756,26 +769,54 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta }); cx.add_model(|cx| { - let text = "fn a() {\n {\n b()?\n }\n\n Ok(())\n}"; + let text = " + fn a() { + { + b()? + } + Ok(()) + } + " + .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent( + + // Delete a closing curly brace changes the suggested indent for the line. + buffer.edit( [(Point::new(3, 4)..Point::new(3, 5), "")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( buffer.text(), - "fn a() {\n {\n b()?\n \n\n Ok(())\n}" + " + fn a() { + { + b()? + | + Ok(()) + } + " + .replace("|", "") // included in the string to preserve trailing whites + .unindent() ); - buffer.edit_with_autoindent( + // Manually editing the leading whitespace + buffer.edit( [(Point::new(3, 0)..Point::new(3, 12), "")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( buffer.text(), - "fn a() {\n {\n b()?\n\n\n Ok(())\n}" + " + fn a() { + { + b()? + + Ok(()) + } + " + .unindent() ); buffer }); @@ -783,6 +824,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta #[gpui::test] fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " fn a() {} @@ -791,7 +833,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(5..5, "\nb")], IndentSize::spaces(4), cx); + buffer.edit([(5..5, "\nb")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), " @@ -803,9 +845,9 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // The indentation suggestion changed because `@end` node (a close paren) // is now at the beginning of the line. - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(1, 4)..Point::new(1, 5), "")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -823,17 +865,137 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte #[gpui::test] fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], IndentSize::spaces(4), cx); + buffer.edit( + [(0..1, "\n"), (2..3, "\n")], + Some(AutoindentMode::EachLine), + cx, + ); assert_eq!(buffer.text(), "\n\n\n"); buffer }); } #[gpui::test] -fn test_autoindent_disabled(cx: &mut MutableAppContext) { +fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); + cx.add_model(|cx| { + let text = " + const a: usize = 1; + fn b() { + if c { + let d = 2; + } + } + " + .unindent(); + + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + buffer.edit( + [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], + Some(AutoindentMode::EachLine), + cx, + ); + assert_eq!( + buffer.text(), + " + const a: usize = 1; + fn b() { + if c { + e( + f() + ); + let d = 2; + } + } + " + .unindent() + ); + + buffer + }); +} + +#[gpui::test] +fn test_autoindent_block_mode(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); + cx.add_model(|cx| { + let text = r#" + fn a() { + b(); + } + "# + .unindent(); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + + let inserted_text = r#" + " + c + d + e + " + "# + .unindent(); + + // Insert the block at column zero. The entire block is indented + // so that the first line matches the previous line's indentation. + buffer.edit( + [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + Some(AutoindentMode::Block { + original_indent_columns: vec![0], + }), + cx, + ); + assert_eq!( + buffer.text(), + r#" + fn a() { + b(); + " + c + d + e + " + } + "# + .unindent() + ); + + // Insert the block at a deeper indent level. The entire block is outdented. + buffer.undo(cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], + Some(AutoindentMode::Block { + original_indent_columns: vec![0], + }), + cx, + ); + assert_eq!( + buffer.text(), + r#" + fn a() { + b(); + " + c + d + e + " + } + "# + .unindent() + ); + + buffer + }); +} + +#[gpui::test] +fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " * one @@ -853,9 +1015,9 @@ fn test_autoindent_disabled(cx: &mut MutableAppContext) { )), cx, ); - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "\n")], - IndentSize::spaces(4), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -879,18 +1041,18 @@ fn test_serialization(cx: &mut gpui::MutableAppContext) { let buffer1 = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "abc", cx); - buffer.edit([(3..3, "D")], cx); + buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); buffer.start_transaction_at(now); - buffer.edit([(4..4, "E")], cx); + buffer.edit([(4..4, "E")], None, cx); buffer.end_transaction_at(now, cx); assert_eq!(buffer.text(), "abcDE"); buffer.undo(cx); assert_eq!(buffer.text(), "abcD"); - buffer.edit([(4..4, "F")], cx); + buffer.edit([(4..4, "F")], None, cx); assert_eq!(buffer.text(), "abcDF"); buffer }); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 8c689808aefac3f4c98e1a8826d1d9e957eedc57..5c8916e34eede7de7f6ddd109ba38841980baa14 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,3 +1,4 @@ +pub use lsp_types::request::*; pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; diff --git a/crates/project/src/db.rs b/crates/project/src/db.rs index bc125e4303a6e394c4538633daf4878b805fa52f..0b630bf256147270577703d4d0df3b993d90b234 100644 --- a/crates/project/src/db.rs +++ b/crates/project/src/db.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::path::PathBuf; +use std::path::Path; use std::sync::Arc; pub struct Db(DbStore); @@ -16,8 +16,8 @@ enum DbStore { impl Db { /// Open or create a database at the given file path. - pub fn open(path: PathBuf) -> Result> { - let db = rocksdb::DB::open_default(&path)?; + pub fn open(path: &Path) -> Result> { + let db = rocksdb::DB::open_default(path)?; Ok(Arc::new(Self(DbStore::Real(db)))) } @@ -125,7 +125,7 @@ mod tests { fn test_db() { let dir = TempDir::new("db-test").unwrap(); let fake_db = Db::open_fake(); - let real_db = Db::open(dir.path().join("test.db")).unwrap(); + let real_db = Db::open(&dir.path().join("test.db")).unwrap(); for db in [&real_db, &fake_db] { assert_eq!( @@ -152,7 +152,7 @@ mod tests { drop(real_db); - let real_db = Db::open(dir.path().join("test.db")).unwrap(); + let real_db = Db::open(&dir.path().join("test.db")).unwrap(); assert_eq!( real_db.read(["key-1", "key-2", "key-3"]).unwrap(), &[Some("one".as_bytes().to_vec()), None, None,] diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 0c30ee29248cb19074c272fc2c92b3225df79111..4d679ec9cb1ac96af7d90e083a3ebb01d01d501a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -8,11 +8,11 @@ use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16, + range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, PointUtf16, ToPointUtf16, }; -use lsp::{DocumentHighlightKind, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; -use std::{cmp::Reverse, ops::Range, path::Path}; +use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; #[async_trait(?Send)] pub(crate) trait LspCommand: 'static + Sized { @@ -75,6 +75,10 @@ pub(crate) struct GetDefinition { pub position: PointUtf16, } +pub(crate) struct GetTypeDefinition { + pub position: PointUtf16, +} + pub(crate) struct GetReferences { pub position: PointUtf16, } @@ -238,13 +242,7 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let (lsp_adapter, lsp_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, &mut cx)?; Project::deserialize_workspace_edit( project, edit, @@ -352,87 +350,99 @@ impl LspCommand for GetDefinition { message: Option, project: ModelHandle, buffer: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { - let mut definitions = Vec::new(); - let (lsp_adapter, language_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) + location_links_from_lsp(message, project, buffer, cx).await + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { + proto::GetDefinition { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetDefinition, + _: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(message.version)) }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; + .await; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } - if let Some(message) = message { - let mut unresolved_links = Vec::new(); - match message { - lsp::GotoDefinitionResponse::Scalar(loc) => { - unresolved_links.push((None, loc.uri, loc.range)); - } - lsp::GotoDefinitionResponse::Array(locs) => { - unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); - } - lsp::GotoDefinitionResponse::Link(links) => { - unresolved_links.extend(links.into_iter().map(|l| { - ( - l.origin_selection_range, - l.target_uri, - l.target_selection_range, - ) - })); - } - } + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &AppContext, + ) -> proto::GetDefinitionResponse { + let links = location_links_to_proto(response, project, peer_id, cx); + proto::GetDefinitionResponse { links } + } - for (origin_range, target_uri, target_range) in unresolved_links { - let target_buffer_handle = project - .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) - }) - .await?; + async fn response_from_proto( + self, + message: proto::GetDefinitionResponse, + project: ModelHandle, + _: ModelHandle, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_proto(message.links, project, cx).await + } - cx.read(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = origin_buffer - .clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = origin_buffer - .clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); + fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { + message.buffer_id + } +} - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; +#[async_trait(?Send)] +impl LspCommand for GetTypeDefinition { + type Response = Vec; + type LspRequest = lsp::request::GotoTypeDefinition; + type ProtoRequest = proto::GetTypeDefinition; - definitions.push(LocationLink { - origin: origin_location, - target: target_location, - }) - }); - } + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoTypeDefinitionParams { + lsp::GotoTypeDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), } + } - Ok(definitions) + async fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + buffer: ModelHandle, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_lsp(message, project, buffer, cx).await } - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { - proto::GetDefinition { + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { + proto::GetTypeDefinition { project_id, buffer_id: buffer.remote_id(), position: Some(language::proto::serialize_anchor( @@ -443,7 +453,7 @@ impl LspCommand for GetDefinition { } async fn from_proto( - message: proto::GetDefinition, + message: proto::GetTypeDefinition, _: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, @@ -468,101 +478,213 @@ impl LspCommand for GetDefinition { peer_id: PeerId, _: &clock::Global, cx: &AppContext, - ) -> proto::GetDefinitionResponse { - let links = response - .into_iter() - .map(|definition| { - let origin = definition.origin.map(|origin| { - let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx); - proto::Location { - start: Some(serialize_anchor(&origin.range.start)), - end: Some(serialize_anchor(&origin.range.end)), - buffer: Some(buffer), - } - }); - - let buffer = - project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx); - let target = proto::Location { - start: Some(serialize_anchor(&definition.target.range.start)), - end: Some(serialize_anchor(&definition.target.range.end)), - buffer: Some(buffer), - }; - - proto::LocationLink { - origin, - target: Some(target), - } - }) - .collect(); - proto::GetDefinitionResponse { links } + ) -> proto::GetTypeDefinitionResponse { + let links = location_links_to_proto(response, project, peer_id, cx); + proto::GetTypeDefinitionResponse { links } } async fn response_from_proto( self, - message: proto::GetDefinitionResponse, + message: proto::GetTypeDefinitionResponse, project: ModelHandle, _: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { - let mut links = Vec::new(); - for link in message.links { - let origin = match link.origin { - Some(origin) => { - let buffer = origin - .buffer - .ok_or_else(|| anyhow!("missing origin buffer"))?; - let buffer = project - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let start = origin - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing origin start"))?; - let end = origin - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing origin end"))?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) - .await; - Some(Location { - buffer, - range: start..end, - }) - } - None => None, - }; + location_links_from_proto(message.links, project, cx).await + } - let target = link.target.ok_or_else(|| anyhow!("missing target"))?; - let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - let buffer = project - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let start = target - .start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target start"))?; - let end = target - .end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target end"))?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) - .await; - let target = Location { - buffer, - range: start..end, - }; + fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 { + message.buffer_id + } +} - links.push(LocationLink { origin, target }) +fn language_server_for_buffer( + project: &ModelHandle, + buffer: &ModelHandle, + cx: &mut AsyncAppContext, +) -> Result<(Arc, Arc)> { + project + .read_with(cx, |project, cx| { + project + .language_server_for_buffer(buffer.read(cx), cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) + .ok_or_else(|| anyhow!("no language server found for buffer")) +} + +async fn location_links_from_proto( + proto_links: Vec, + project: ModelHandle, + mut cx: AsyncAppContext, +) -> Result> { + let mut links = Vec::new(); + + for link in proto_links { + let origin = match link.origin { + Some(origin) => { + let buffer = origin + .buffer + .ok_or_else(|| anyhow!("missing origin buffer"))?; + let buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = origin + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin start"))?; + let end = origin + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .await; + Some(Location { + buffer, + range: start..end, + }) + } + None => None, + }; + + let target = link.target.ok_or_else(|| anyhow!("missing target"))?; + let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = target + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = target + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .await; + let target = Location { + buffer, + range: start..end, + }; + + links.push(LocationLink { origin, target }) + } + + Ok(links) +} + +async fn location_links_from_lsp( + message: Option, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, +) -> Result> { + let message = match message { + Some(message) => message, + None => return Ok(Vec::new()), + }; + + let mut unresolved_links = Vec::new(); + match message { + lsp::GotoDefinitionResponse::Scalar(loc) => { + unresolved_links.push((None, loc.uri, loc.range)); + } + + lsp::GotoDefinitionResponse::Array(locs) => { + unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); + } + + lsp::GotoDefinitionResponse::Link(links) => { + unresolved_links.extend(links.into_iter().map(|l| { + ( + l.origin_selection_range, + l.target_uri, + l.target_selection_range, + ) + })); } - Ok(links) } - fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { - message.buffer_id + let (lsp_adapter, language_server) = language_server_for_buffer(&project, &buffer, &mut cx)?; + let mut definitions = Vec::new(); + for (origin_range, target_uri, target_range) in unresolved_links { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_via_lsp( + target_uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let origin_location = origin_range.map(|origin_range| { + let origin_buffer = buffer.read(cx); + let origin_start = + origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); + let origin_end = + origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); + Location { + buffer: buffer.clone(), + range: origin_buffer.anchor_after(origin_start) + ..origin_buffer.anchor_before(origin_end), + } + }); + + let target_buffer = target_buffer_handle.read(cx); + let target_start = + target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); + let target_end = + target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); + let target_location = Location { + buffer: target_buffer_handle, + range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }; + + definitions.push(LocationLink { + origin: origin_location, + target: target_location, + }) + }); } + Ok(definitions) +} + +fn location_links_to_proto( + links: Vec, + project: &mut Project, + peer_id: PeerId, + cx: &AppContext, +) -> Vec { + links + .into_iter() + .map(|definition| { + let origin = definition.origin.map(|origin| { + let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx); + proto::Location { + start: Some(serialize_anchor(&origin.range.start)), + end: Some(serialize_anchor(&origin.range.end)), + buffer: Some(buffer), + } + }); + + let buffer = project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx); + let target = proto::Location { + start: Some(serialize_anchor(&definition.target.range.start)), + end: Some(serialize_anchor(&definition.target.range.end)), + buffer: Some(buffer), + }; + + proto::LocationLink { + origin, + target: Some(target), + } + }) + .collect() } #[async_trait(?Send)] @@ -595,13 +717,8 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let (lsp_adapter, language_server) = project - .read_with(&cx, |project, cx| { - project - .language_server_for_buffer(buffer.read(cx), cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - .ok_or_else(|| anyhow!("no language server found for buffer"))?; + let (lsp_adapter, language_server) = + language_server_for_buffer(&project, &buffer, &mut cx)?; if let Some(locations) = locations { for lsp_location in locations { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 759eb901942ae91f9064d96c4f2eb5fd901c6d68..8adc10ba5564ac57d29cb5107b9fd73d3817d53d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -254,10 +254,9 @@ pub struct DocumentHighlight { #[derive(Clone, Debug)] pub struct Symbol { - pub source_worktree_id: WorktreeId, - pub worktree_id: WorktreeId, pub language_server_name: LanguageServerName, - pub path: PathBuf, + pub source_worktree_id: WorktreeId, + pub path: ProjectPath, pub label: CodeLabel, pub name: String, pub kind: lsp::SymbolKind, @@ -3169,7 +3168,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -3251,6 +3250,16 @@ impl Project { self.request_lsp(buffer.clone(), GetDefinition { position }, cx) } + pub fn type_definition( + &self, + buffer: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx) + } + pub fn references( &self, buffer: &ModelHandle, @@ -3324,16 +3333,19 @@ impl Project { if let Some((worktree, rel_path)) = this.find_local_worktree(&abs_path, cx) { - worktree_id = worktree.read(cx).id(); + worktree_id = (&worktree.read(cx)).id(); path = rel_path; } else { path = relativize_path(&worktree_abs_path, &abs_path); } - let signature = this.symbol_signature(worktree_id, &path); - let language = this.languages.select_language(&path); + let project_path = ProjectPath { + worktree_id, + path: path.into(), + }; + let signature = this.symbol_signature(&project_path); + let language = this.languages.select_language(&project_path.path); let language_server_name = adapter.name.clone(); - Some(async move { let label = if let Some(language) = language { language @@ -3344,15 +3356,14 @@ impl Project { }; Symbol { - source_worktree_id, - worktree_id, language_server_name, + source_worktree_id, + path: project_path, label: label.unwrap_or_else(|| { CodeLabel::plain(lsp_symbol.name.clone(), None) }), kind: lsp_symbol.kind, name: lsp_symbol.name, - path, range: range_from_lsp(lsp_symbol.location.range), signature, } @@ -3410,7 +3421,7 @@ impl Project { }; let worktree_abs_path = if let Some(worktree_abs_path) = self - .worktree_for_id(symbol.worktree_id, cx) + .worktree_for_id(symbol.path.worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) .map(|local_worktree| local_worktree.abs_path()) { @@ -3418,7 +3429,7 @@ impl Project { } else { return Task::ready(Err(anyhow!("worktree not found for symbol"))); }; - let symbol_abs_path = worktree_abs_path.join(&symbol.path); + let symbol_abs_path = worktree_abs_path.join(&symbol.path.path); let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) { uri } else { @@ -3662,7 +3673,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -4022,7 +4033,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -4622,11 +4633,11 @@ impl Project { self.active_entry } - pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { self.worktree_for_id(path.worktree_id, cx)? .read(cx) .entry_for_path(&path.path) - .map(|entry| entry.id) + .cloned() } pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option { @@ -5436,7 +5447,7 @@ impl Project { .read_with(&cx, |this, _| this.deserialize_symbol(symbol)) .await?; let symbol = this.read_with(&cx, |this, _| { - let signature = this.symbol_signature(symbol.worktree_id, &symbol.path); + let signature = this.symbol_signature(&symbol.path); if signature == symbol.signature { Ok(symbol) } else { @@ -5454,10 +5465,10 @@ impl Project { }) } - fn symbol_signature(&self, worktree_id: WorktreeId, path: &Path) -> [u8; 32] { + fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] { let mut hasher = Sha256::new(); - hasher.update(worktree_id.to_proto().to_be_bytes()); - hasher.update(path.to_string_lossy().as_bytes()); + hasher.update(project_path.worktree_id.to_proto().to_be_bytes()); + hasher.update(project_path.path.to_string_lossy().as_bytes()); hasher.update(self.nonce.to_be_bytes()); hasher.finalize().as_slice().try_into().unwrap() } @@ -5655,14 +5666,17 @@ impl Project { .end .ok_or_else(|| anyhow!("invalid end"))?; let kind = unsafe { mem::transmute(serialized_symbol.kind) }; - let path = PathBuf::from(serialized_symbol.path); - let language = languages.select_language(&path); - Ok(Symbol { - source_worktree_id, + let path = ProjectPath { worktree_id, + path: PathBuf::from(serialized_symbol.path).into(), + }; + let language = languages.select_language(&path.path); + Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), ), + source_worktree_id, + path, label: { match language { Some(language) => { @@ -5676,7 +5690,6 @@ impl Project { }, name: serialized_symbol.name, - path, range: PointUtf16::new(start.row, start.column) ..PointUtf16::new(end.row, end.column), kind, @@ -5764,6 +5777,10 @@ impl Project { let mut lsp_edits = lsp_edits.into_iter().peekable(); let mut edits = Vec::new(); while let Some((mut range, mut new_text)) = lsp_edits.next() { + // Clip invalid ranges provided by the language server. + range.start = snapshot.clip_point_utf16(range.start, Bias::Left); + range.end = snapshot.clip_point_utf16(range.end, Bias::Left); + // Combine any LSP edits that are adjacent. // // Also, combine LSP edits that are separated from each other by only @@ -5791,12 +5808,6 @@ impl Project { lsp_edits.next(); } - if snapshot.clip_point_utf16(range.start, Bias::Left) != range.start - || snapshot.clip_point_utf16(range.end, Bias::Left) != range.end - { - return Err(anyhow!("invalid edits received from language server")); - } - // For multiline edits, perform a diff of the old and new text so that // we can identify the changes more precisely, preserving the locations // of any anchors positioned in the unchanged regions. @@ -6144,12 +6155,12 @@ impl From for fs::RemoveOptions { fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { - source_worktree_id: symbol.source_worktree_id.to_proto(), - worktree_id: symbol.worktree_id.to_proto(), language_server_name: symbol.language_server_name.0.to_string(), + source_worktree_id: symbol.source_worktree_id.to_proto(), + worktree_id: symbol.path.worktree_id.to_proto(), + path: symbol.path.path.to_string_lossy().to_string(), name: symbol.name.clone(), kind: unsafe { mem::transmute(symbol.kind) }, - path: symbol.path.to_string_lossy().to_string(), start: Some(proto::Point { row: symbol.range.start.row, column: symbol.range.start.column, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 1080daff1d221bb3de9cd3998a981121634bb4e4..4c5e9ef8e1b95994dbe9550b745ef6fb091d79b4 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -169,7 +169,7 @@ async fn test_managing_language_servers( }); // Edit a buffer. The changes are reported to the language server. - rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], cx)); + rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx)); assert_eq!( fake_rust_server .receive_notification::() @@ -226,8 +226,10 @@ async fn test_managing_language_servers( }); // Changes are reported only to servers matching the buffer's language. - toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], cx)); - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "let x = 1;")], cx)); + toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx)); + rust_buffer2.update(cx, |buffer, cx| { + buffer.edit([(0..0, "let x = 1;")], None, cx) + }); assert_eq!( fake_rust_server .receive_notification::() @@ -348,7 +350,7 @@ async fn test_managing_language_servers( }); // The renamed file's version resets after changing language server. - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], cx)); + rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx)); assert_eq!( fake_json_server .receive_notification::() @@ -972,7 +974,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { .await; // Edit the buffer, moving the content down - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], cx)); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx)); let change_notification_1 = fake_server .receive_notification::() .await; @@ -1137,9 +1139,13 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Keep editing the buffer and ensure disk-based diagnostics get translated according to the // changes since the last save. buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], cx); - buffer.edit([(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], cx); - buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], + None, + cx, + ); + buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx); }); let change_notification_2 = fake_server .receive_notification::() @@ -1330,6 +1336,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(0, 0)..Point::new(0, 0), "// above first function\n", )], + None, cx, ); buffer.edit( @@ -1337,6 +1344,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(2, 0)..Point::new(2, 0), " // inside first function\n", )], + None, cx, ); buffer.edit( @@ -1344,6 +1352,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(6, 4)..Point::new(6, 4), "// inside second function ", )], + None, cx, ); @@ -1405,7 +1414,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { buffer.update(cx, |buffer, cx| { for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -1517,7 +1526,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp ); for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -1565,7 +1574,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { .unwrap(); // Simulate the language server sending us edits in a non-ordered fashion, - // with ranges sometimes being inverted. + // with ranges sometimes being inverted or pointing to invalid locations. let edits = project .update(cx, |project, cx| { project.edits_from_lsp( @@ -1580,7 +1589,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { new_text: "a::{b, c}".into(), }, lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)), new_text: "".into(), }, lsp::TextEdit { @@ -1620,7 +1629,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { ); for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -2025,7 +2034,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { buffer .update(cx, |buffer, cx| { assert_eq!(buffer.text(), "the old contents"); - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); buffer.save(cx) }) .await @@ -2053,7 +2062,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { .unwrap(); buffer .update(cx, |buffer, cx| { - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); buffer.save(cx) }) .await @@ -2073,7 +2082,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { project.create_buffer("", None, cx).unwrap() }); buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "abc")], cx); + buffer.edit([(0..0, "abc")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); @@ -2329,7 +2338,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert!(!buffer.is_dirty()); assert!(events.borrow().is_empty()); - buffer.edit([(1..2, "")], cx); + buffer.edit([(1..2, "")], None, cx); }); // after the first edit, the buffer is dirty, and emits a dirtied event. @@ -2356,8 +2365,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!(*events.borrow(), &[language::Event::Saved]); events.borrow_mut().clear(); - buffer.edit([(1..1, "B")], cx); - buffer.edit([(2..2, "D")], cx); + buffer.edit([(1..1, "B")], None, cx); + buffer.edit([(2..2, "D")], None, cx); }); // after editing again, the buffer is dirty, and emits another dirty event. @@ -2376,7 +2385,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { // After restoring the buffer to its previously-saved state, // the buffer is not considered dirty anymore. - buffer.edit([(1..3, "")], cx); + buffer.edit([(1..3, "")], None, cx); assert!(buffer.text() == "ac"); assert!(!buffer.is_dirty()); }); @@ -2427,7 +2436,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { }); buffer3.update(cx, |buffer, cx| { - buffer.edit([(0..0, "x")], cx); + buffer.edit([(0..0, "x")], None, cx); }); events.borrow_mut().clear(); fs.remove_file("/dir/file3".as_ref(), Default::default()) @@ -2495,7 +2504,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { // Modify the buffer buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, " ")], cx); + buffer.edit([(0..0, " ")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); @@ -2986,7 +2995,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) { .unwrap(); buffer_4.update(cx, |buffer, cx| { let text = "two::TWO"; - buffer.edit([(20..28, text), (31..43, text)], cx); + buffer.edit([(20..28, text), (31..43, text)], None, cx); }); assert_eq!( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 792d970fcd26af1a19369bc381ea971f5aa287f6..0a32a8427bf489111065ca80b76c9086557e2ab8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1028,11 +1028,11 @@ impl ProjectPanel { .with_child( ConstrainedBox::new(if kind == EntryKind::Dir { if details.is_expanded { - Svg::new("icons/chevron_right_8.svg") + Svg::new("icons/chevron_down_8.svg") .with_color(style.icon_color) .boxed() } else { - Svg::new("icons/chevron_down_8.svg") + Svg::new("icons/chevron_right_8.svg") .with_color(style.icon_color) .boxed() } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 10425f63a83068a7b48194f7ac5060564982f28d..8f2305eaff7ec17d78cc58852a4f299dc7720e76 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -26,7 +26,8 @@ pub struct ProjectSymbolsView { project: ModelHandle, selected_match_index: usize, symbols: Vec, - match_candidates: Vec, + visible_match_candidates: Vec, + external_match_candidates: Vec, show_worktree_root_name: bool, pending_update: Task<()>, matches: Vec, @@ -63,7 +64,8 @@ impl ProjectSymbolsView { picker: cx.add_view(|cx| Picker::new(handle, cx)), selected_match_index: 0, symbols: Default::default(), - match_candidates: Default::default(), + visible_match_candidates: Default::default(), + external_match_candidates: Default::default(), matches: Default::default(), show_worktree_root_name: false, pending_update: Task::ready(()), @@ -80,38 +82,39 @@ impl ProjectSymbolsView { } fn filter(&mut self, query: &str, cx: &mut ViewContext) { - let mut matches = if query.is_empty() { - self.match_candidates - .iter() - .enumerate() - .map(|(candidate_id, candidate)| StringMatch { - candidate_id, - score: Default::default(), - positions: Default::default(), - string: candidate.string.clone(), - }) - .collect() - } else { - cx.background_executor().block(fuzzy::match_strings( - &self.match_candidates, - query, - false, - 100, - &Default::default(), - cx.background().clone(), - )) - }; - - matches.sort_unstable_by_key(|mat| { - let label = &self.symbols[mat.candidate_id].label; + const MAX_MATCHES: usize = 100; + let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( + &self.visible_match_candidates, + query, + false, + MAX_MATCHES, + &Default::default(), + cx.background().clone(), + )); + let mut external_matches = cx.background_executor().block(fuzzy::match_strings( + &self.external_match_candidates, + query, + false, + MAX_MATCHES - visible_matches.len(), + &Default::default(), + cx.background().clone(), + )); + let sort_key_for_match = |mat: &StringMatch| { + let symbol = &self.symbols[mat.candidate_id]; ( Reverse(OrderedFloat(mat.score)), - &label.text[label.filter_range.clone()], + &symbol.label.text[symbol.label.filter_range.clone()], ) - }); + }; + + visible_matches.sort_unstable_by_key(sort_key_for_match); + external_matches.sort_unstable_by_key(sort_key_for_match); + let mut matches = visible_matches; + matches.append(&mut external_matches); for mat in &mut matches { - let filter_start = self.symbols[mat.candidate_id].label.filter_range.start; + let symbol = &self.symbols[mat.candidate_id]; + let filter_start = symbol.label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } @@ -198,7 +201,8 @@ impl PickerDelegate for ProjectSymbolsView { if let Some(this) = this.upgrade(&cx) { if let Some(symbols) = symbols { this.update(&mut cx, |this, cx| { - this.match_candidates = symbols + let project = this.project.read(cx); + let (visible_match_candidates, external_match_candidates) = symbols .iter() .enumerate() .map(|(id, symbol)| { @@ -208,7 +212,14 @@ impl PickerDelegate for ProjectSymbolsView { .to_string(), ) }) - .collect(); + .partition(|candidate| { + project + .entry_for_path(&symbols[candidate.id].path, cx) + .map_or(false, |e| !e.is_ignored) + }); + + this.visible_match_candidates = visible_match_candidates; + this.external_match_candidates = external_match_candidates; this.symbols = symbols; this.filter(&query, cx); }); @@ -232,10 +243,10 @@ impl PickerDelegate for ProjectSymbolsView { let symbol = &self.symbols[string_match.candidate_id]; let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); - let mut path = symbol.path.to_string_lossy(); + let mut path = symbol.path.path.to_string_lossy(); if self.show_worktree_root_name { let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) { + if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) { path = Cow::Owned(format!( "{}{}{}", worktree.read(cx).root_name(), @@ -275,7 +286,7 @@ mod tests { use gpui::{serde_json::json, TestAppContext}; use language::{FakeLspAdapter, Language, LanguageConfig}; use project::FakeFs; - use std::sync::Arc; + use std::{path::Path, sync::Arc}; #[gpui::test] async fn test_project_symbols(cx: &mut TestAppContext) { @@ -309,15 +320,21 @@ mod tests { // Set up fake langauge server to return fuzzy matches against // a fixed set of symbol names. - let fake_symbol_names = ["one", "ton", "uno"]; + let fake_symbols = [ + symbol("one", "/external"), + symbol("ton", "/dir/test.rs"), + symbol("uno", "/dir/test.rs"), + ]; let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::( move |params: lsp::WorkspaceSymbolParams, cx| { let executor = cx.background(); + let fake_symbols = fake_symbols.clone(); async move { - let candidates = fake_symbol_names - .into_iter() - .map(|name| StringMatchCandidate::new(0, name.into())) + let candidates = fake_symbols + .iter() + .enumerate() + .map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone())) .collect::>(); let matches = if params.query.is_empty() { Vec::new() @@ -334,7 +351,10 @@ mod tests { }; Ok(Some( - matches.into_iter().map(|mat| symbol(&mat.string)).collect(), + matches + .into_iter() + .map(|mat| fake_symbols[mat.candidate_id].clone()) + .collect(), )) } }, @@ -367,8 +387,8 @@ mod tests { cx.foreground().run_until_parked(); symbols_view.read_with(cx, |symbols_view, _| { assert_eq!(symbols_view.matches.len(), 2); - assert_eq!(symbols_view.matches[0].string, "one"); - assert_eq!(symbols_view.matches[1].string, "ton"); + assert_eq!(symbols_view.matches[0].string, "ton"); + assert_eq!(symbols_view.matches[1].string, "one"); }); // Spawn more updates such that in the end, there are again no matches. @@ -383,7 +403,7 @@ mod tests { }); } - fn symbol(name: &str) -> lsp::SymbolInformation { + fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { #[allow(deprecated)] lsp::SymbolInformation { name: name.to_string(), @@ -392,7 +412,7 @@ mod tests { deprecated: None, container_name: None, location: lsp::Location::new( - lsp::Url::from_file_path("/a/b").unwrap(), + lsp::Url::from_file_path(path.as_ref()).unwrap(), lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), ), } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 35f3049edbad4f694cee2fe70811b53d18e75adb..f52815a8be60d306e5d328f4910cae30828c29a8 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -26,85 +26,87 @@ message Envelope { GetDefinition get_definition = 20; GetDefinitionResponse get_definition_response = 21; - GetReferences get_references = 22; - GetReferencesResponse get_references_response = 23; - GetDocumentHighlights get_document_highlights = 24; - GetDocumentHighlightsResponse get_document_highlights_response = 25; - GetProjectSymbols get_project_symbols = 26; - GetProjectSymbolsResponse get_project_symbols_response = 27; - OpenBufferForSymbol open_buffer_for_symbol = 28; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; - - UpdateProject update_project = 30; - RegisterProjectActivity register_project_activity = 31; - UpdateWorktree update_worktree = 32; - UpdateWorktreeExtensions update_worktree_extensions = 33; - - CreateProjectEntry create_project_entry = 34; - RenameProjectEntry rename_project_entry = 35; - CopyProjectEntry copy_project_entry = 36; - DeleteProjectEntry delete_project_entry = 37; - ProjectEntryResponse project_entry_response = 38; - - UpdateDiagnosticSummary update_diagnostic_summary = 39; - StartLanguageServer start_language_server = 40; - UpdateLanguageServer update_language_server = 41; - - OpenBufferById open_buffer_by_id = 42; - OpenBufferByPath open_buffer_by_path = 43; - OpenBufferResponse open_buffer_response = 44; - UpdateBuffer update_buffer = 45; - UpdateBufferFile update_buffer_file = 46; - SaveBuffer save_buffer = 47; - BufferSaved buffer_saved = 48; - BufferReloaded buffer_reloaded = 49; - ReloadBuffers reload_buffers = 50; - ReloadBuffersResponse reload_buffers_response = 51; - FormatBuffers format_buffers = 52; - FormatBuffersResponse format_buffers_response = 53; - GetCompletions get_completions = 54; - GetCompletionsResponse get_completions_response = 55; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 56; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 57; - GetCodeActions get_code_actions = 58; - GetCodeActionsResponse get_code_actions_response = 59; - GetHover get_hover = 60; - GetHoverResponse get_hover_response = 61; - ApplyCodeAction apply_code_action = 62; - ApplyCodeActionResponse apply_code_action_response = 63; - PrepareRename prepare_rename = 64; - PrepareRenameResponse prepare_rename_response = 65; - PerformRename perform_rename = 66; - PerformRenameResponse perform_rename_response = 67; - SearchProject search_project = 68; - SearchProjectResponse search_project_response = 69; - - GetChannels get_channels = 70; - GetChannelsResponse get_channels_response = 71; - JoinChannel join_channel = 72; - JoinChannelResponse join_channel_response = 73; - LeaveChannel leave_channel = 74; - SendChannelMessage send_channel_message = 75; - SendChannelMessageResponse send_channel_message_response = 76; - ChannelMessageSent channel_message_sent = 77; - GetChannelMessages get_channel_messages = 78; - GetChannelMessagesResponse get_channel_messages_response = 79; - - UpdateContacts update_contacts = 80; - UpdateInviteInfo update_invite_info = 81; - ShowContacts show_contacts = 82; - - GetUsers get_users = 83; - FuzzySearchUsers fuzzy_search_users = 84; - UsersResponse users_response = 85; - RequestContact request_contact = 86; - RespondToContactRequest respond_to_contact_request = 87; - RemoveContact remove_contact = 88; - - Follow follow = 89; - FollowResponse follow_response = 90; - UpdateFollowers update_followers = 91; - Unfollow unfollow = 92; + GetTypeDefinition get_type_definition = 22; + GetTypeDefinitionResponse get_type_definition_response = 23; + GetReferences get_references = 24; + GetReferencesResponse get_references_response = 25; + GetDocumentHighlights get_document_highlights = 26; + GetDocumentHighlightsResponse get_document_highlights_response = 27; + GetProjectSymbols get_project_symbols = 28; + GetProjectSymbolsResponse get_project_symbols_response = 29; + OpenBufferForSymbol open_buffer_for_symbol = 30; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31; + + UpdateProject update_project = 32; + RegisterProjectActivity register_project_activity = 33; + UpdateWorktree update_worktree = 34; + UpdateWorktreeExtensions update_worktree_extensions = 35; + + CreateProjectEntry create_project_entry = 36; + RenameProjectEntry rename_project_entry = 37; + CopyProjectEntry copy_project_entry = 38; + DeleteProjectEntry delete_project_entry = 39; + ProjectEntryResponse project_entry_response = 40; + + UpdateDiagnosticSummary update_diagnostic_summary = 41; + StartLanguageServer start_language_server = 42; + UpdateLanguageServer update_language_server = 43; + + OpenBufferById open_buffer_by_id = 44; + OpenBufferByPath open_buffer_by_path = 45; + OpenBufferResponse open_buffer_response = 46; + UpdateBuffer update_buffer = 47; + UpdateBufferFile update_buffer_file = 48; + SaveBuffer save_buffer = 49; + BufferSaved buffer_saved = 50; + BufferReloaded buffer_reloaded = 51; + ReloadBuffers reload_buffers = 52; + ReloadBuffersResponse reload_buffers_response = 53; + FormatBuffers format_buffers = 54; + FormatBuffersResponse format_buffers_response = 55; + GetCompletions get_completions = 56; + GetCompletionsResponse get_completions_response = 57; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 58; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 59; + GetCodeActions get_code_actions = 60; + GetCodeActionsResponse get_code_actions_response = 61; + GetHover get_hover = 62; + GetHoverResponse get_hover_response = 63; + ApplyCodeAction apply_code_action = 64; + ApplyCodeActionResponse apply_code_action_response = 65; + PrepareRename prepare_rename = 66; + PrepareRenameResponse prepare_rename_response = 67; + PerformRename perform_rename = 68; + PerformRenameResponse perform_rename_response = 69; + SearchProject search_project = 70; + SearchProjectResponse search_project_response = 71; + + GetChannels get_channels = 72; + GetChannelsResponse get_channels_response = 73; + JoinChannel join_channel = 74; + JoinChannelResponse join_channel_response = 75; + LeaveChannel leave_channel = 76; + SendChannelMessage send_channel_message = 77; + SendChannelMessageResponse send_channel_message_response = 78; + ChannelMessageSent channel_message_sent = 79; + GetChannelMessages get_channel_messages = 80; + GetChannelMessagesResponse get_channel_messages_response = 81; + + UpdateContacts update_contacts = 82; + UpdateInviteInfo update_invite_info = 83; + ShowContacts show_contacts = 84; + + GetUsers get_users = 85; + FuzzySearchUsers fuzzy_search_users = 86; + UsersResponse users_response = 87; + RequestContact request_contact = 88; + RespondToContactRequest respond_to_contact_request = 89; + RemoveContact remove_contact = 90; + + Follow follow = 91; + FollowResponse follow_response = 92; + UpdateFollowers update_followers = 93; + Unfollow unfollow = 94; } } @@ -263,6 +265,17 @@ message GetDefinitionResponse { repeated LocationLink links = 1; } +message GetTypeDefinition { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetTypeDefinitionResponse { + repeated LocationLink links = 1; +} + message GetReferences { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index e3844a869276a2fb3ed1a6737c775b909d459c29..8cd5ca36fbf523e8a25cf6ee125ba022957273ac 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -106,6 +106,8 @@ messages!( (GetCompletionsResponse, Background), (GetDefinition, Background), (GetDefinitionResponse, Background), + (GetTypeDefinition, Background), + (GetTypeDefinitionResponse, Background), (GetDocumentHighlights, Background), (GetDocumentHighlightsResponse, Background), (GetReferences, Background), @@ -183,6 +185,7 @@ request_messages!( (GetHover, GetHoverResponse), (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), + (GetTypeDefinition, GetTypeDefinitionResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetReferences, GetReferencesResponse), (GetProjectSymbols, GetProjectSymbolsResponse), @@ -226,6 +229,7 @@ entity_messages!( GetCodeActions, GetCompletions, GetDefinition, + GetTypeDefinition, GetDocumentHighlights, GetHover, GetReferences, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 015ac10707c7cd7960a30f3c13cf350f433e0ce6..c4017015f9067280d76de94f6bcc242cdf6a1cca 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 28; +pub const PROTOCOL_VERSION: u32 = 29; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 529da6f7b6ea3b70d5e6e85593c0a721a44a202d..52631e71b4b71827e6ad03ede931034e1f021b68 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -260,7 +260,7 @@ impl BufferSearchBar { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], cx); + query_buffer.edit([(0..len, query)], None, cx); }); }); } diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index d0d63b6f49db1580a38fd923bcd43789260c2df0..bf8763da064e85975275197442ea8c103df62052 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -177,4 +177,12 @@ impl View for ConnectedView { self.terminal .update(cx, |terminal, _| terminal.write_to_pty(text.into())); } + + fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { + let mut context = Self::default_keymap_context(); + if self.modal { + context.set.insert("ModalTerminal".into()); + } + context + } } diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 58d4b1e4c0a0992f6a79dbdc101acaeb61992c65..215bfe1ad983770c1c14116f9f2380bbfee4fe6f 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -323,6 +323,7 @@ mod test { alt: false, shift: false, cmd: false, + function: false, key: "🖖🏻".to_string(), //2 char string }; assert_eq!(to_esc_str(&ks, &TermMode::NONE), None); diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 2fbf7134951a8e60c68873085119c9e0cea11940..322f26ea3ca2612edb78921008ff377f9b938d0a 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,4 +1,5 @@ use gpui::{ModelHandle, ViewContext}; +use settings::{Settings, WorkingDirectory}; use workspace::Workspace; use crate::{ @@ -28,7 +29,14 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon // No connection was stored, create a new terminal if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { // No terminal modal visible, construct a new one. - let working_directory = get_working_directory(workspace, cx); + let wd_strategy = cx + .global::() + .terminal_overrides + .working_directory + .clone() + .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + + let working_directory = get_working_directory(workspace, cx, wd_strategy); let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx)); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index df0c0e59704f1b88c966d44c551028caf9b49bc6..2e06f8644170061be10ac0a3c3368b8b3134b176 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -742,4 +742,28 @@ mod alacritty_unix { pub fn default_shell(pw: &Passwd<'_>) -> Program { Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) } + + //Active entry with a work tree, worktree is a file, integration test with the strategy interface + #[gpui::test] + async fn active_entry_worktree_is_file_int(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = + get_working_directory(workspace, cx, WorkingDirectory::CurrentProjectDirectory); + let first = first_project_directory(workspace, cx); + assert_eq!(res, first); + }); + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f7c470bb96a5071a9f9096551be9ca0911971124..349a505a08d0cb52b04ce5256530b92fb71e55fa 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -38,11 +38,7 @@ pub struct Theme { pub struct Workspace { pub background: Color, pub titlebar: Titlebar, - pub active_pane_active_tab: Tab, - pub active_pane_inactive_tab: Tab, - pub inactive_pane_active_tab: Tab, - pub inactive_pane_inactive_tab: Tab, - pub pane_button: Interactive, + pub tab_bar: TabBar, pub pane_divider: Border, pub leader_border_opacity: f32, pub leader_border_width: f32, @@ -72,6 +68,22 @@ pub struct Titlebar { pub outdated_warning: ContainedText, } +#[derive(Clone, Deserialize, Default)] +pub struct TabBar { + #[serde(flatten)] + pub container: ContainerStyle, + pub pane_button: Interactive, + pub active_pane: TabStyles, + pub inactive_pane: TabStyles, + pub height: f32, +} + +#[derive(Clone, Deserialize, Default)] +pub struct TabStyles { + pub active_tab: Tab, + pub inactive_tab: Tab, +} + #[derive(Clone, Deserialize, Default)] pub struct AvatarRibbon { #[serde(flatten)] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 65909702ba5f9d37cf68988411e646fa0c018c3b..39663e0db42ebf28974dc075a6f3f3cea06c2059 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -13,7 +13,7 @@ use change::init as change_init; use collections::HashSet; use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::{Point, SelectionGoal}; +use language::{AutoindentMode, Point, SelectionGoal}; use workspace::Workspace; use self::{change::change_over, delete::delete_over, yank::yank_over}; @@ -278,7 +278,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { } } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { @@ -427,7 +427,7 @@ mod test { #[gpui::test] async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-$"]); + let mut cx = cx.binding(["$"]); cx.assert("T|est test", "Test tes|t"); cx.assert("Test tes|t", "Test tes|t"); cx.assert( @@ -471,7 +471,7 @@ mod test { #[gpui::test] async fn test_jump_to_end(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-G"]); + let mut cx = cx.binding(["shift-g"]); cx.assert( indoc! {" @@ -561,7 +561,7 @@ mod test { ); for cursor_offset in cursor_offsets { - cx.simulate_keystroke("shift-W"); + cx.simulate_keystroke("shift-w"); cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]); } } @@ -607,7 +607,7 @@ mod test { Mode::Normal, ); for cursor_offset in cursor_offsets { - cx.simulate_keystroke("shift-E"); + cx.simulate_keystroke("shift-e"); cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]); } } @@ -653,7 +653,7 @@ mod test { Mode::Normal, ); for cursor_offset in cursor_offsets.into_iter().rev() { - cx.simulate_keystroke("shift-B"); + cx.simulate_keystroke("shift-b"); cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]); } } @@ -740,7 +740,7 @@ mod test { #[gpui::test] async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-a"]).mode_after(Mode::Insert); cx.assert("The q|uick", "The quick|"); cx.assert("The q|uick ", "The quick |"); cx.assert("|", "|"); @@ -765,7 +765,7 @@ mod test { #[gpui::test] async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-^"]); + let mut cx = cx.binding(["^"]); cx.assert("The q|uick", "|The quick"); cx.assert(" The q|uick", " |The quick"); cx.assert("|", "|"); @@ -792,7 +792,7 @@ mod test { #[gpui::test] async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-i"]).mode_after(Mode::Insert); cx.assert("The q|uick", "|The quick"); cx.assert(" The q|uick", " |The quick"); cx.assert("|", "|"); @@ -817,7 +817,7 @@ mod test { #[gpui::test] async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-D"]); + let mut cx = cx.binding(["shift-d"]); cx.assert( indoc! {" The q|uick @@ -858,7 +858,7 @@ mod test { #[gpui::test] async fn test_delete_left(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-X"]); + let mut cx = cx.binding(["shift-x"]); cx.assert("Te|st", "T|st"); cx.assert("T|est", "|est"); cx.assert("|Test", "|Test"); @@ -956,7 +956,7 @@ mod test { #[gpui::test] async fn test_insert_line_above(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-o"]).mode_after(Mode::Insert); cx.assert( "|", diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 55445c930e960c98f1cec0aef56900e87900d979..03f213b5845961d6db47a351b99df0ab83053111 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -139,7 +139,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["c", "shift-W"]); + let mut cx = cx.binding(["c", "shift-w"]); cx.assert("Test te|st-test test", "Test te| test"); } @@ -174,7 +174,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["c", "shift-E"]); + let mut cx = cx.binding(["c", "shift-e"]); cx.assert("Test te|st-test test", "Test te| test"); } @@ -204,14 +204,14 @@ mod test { test"}, ); - let mut cx = cx.binding(["c", "shift-B"]); + let mut cx = cx.binding(["c", "shift-b"]); cx.assert("Test test-test |test", "Test |test"); } #[gpui::test] async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["c", "shift-$"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["c", "$"]).mode_after(Mode::Insert); cx.assert( indoc! {" The q|uick @@ -347,7 +347,7 @@ mod test { #[gpui::test] async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["c", "shift-G"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["c", "shift-g"]).mode_after(Mode::Insert); cx.assert( indoc! {" The quick diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index c5c823c79e349effb0a641d485cd2c6d7b8cab89..ca2c27cf7028cff6a8f1fe44f818adc4a985133c 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -109,7 +109,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["d", "shift-W"]); + let mut cx = cx.binding(["d", "shift-w"]); cx.assert("Test te|st-test test", "Test te|test"); } @@ -144,7 +144,7 @@ mod test { test"}, ); - let mut cx = cx.binding(["d", "shift-E"]); + let mut cx = cx.binding(["d", "shift-e"]); cx.assert("Test te|st-test test", "Test te| test"); } @@ -176,14 +176,14 @@ mod test { test"}, ); - let mut cx = cx.binding(["d", "shift-B"]); + let mut cx = cx.binding(["d", "shift-b"]); cx.assert("Test test-test |test", "Test |test"); } #[gpui::test] async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["d", "shift-$"]); + let mut cx = cx.binding(["d", "$"]); cx.assert( indoc! {" The q|uick @@ -304,7 +304,7 @@ mod test { #[gpui::test] async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["d", "shift-G"]); + let mut cx = cx.binding(["d", "shift-g"]); cx.assert( indoc! {" The quick diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index cb6a736c6344d0c91cfdb7b5b22458ac0e9fed2e..6f682f61462ab731f79d961d3ad031df8c20c202 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -17,6 +17,7 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut Mut clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, + first_line_indent: buffer.indent_size_for_line(start.row).len, }); } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index fedb999cad2718b7877ff72af40c041a7e30420d..76fea2e2051ae80899ae9d66d2f787dc8f9ecd5f 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::SelectionGoal; +use language::{AutoindentMode, SelectionGoal}; use workspace::Workspace; use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; @@ -254,7 +254,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { @@ -422,7 +422,7 @@ mod test { #[gpui::test] async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-V", "x"]); + let mut cx = cx.binding(["shift-v", "x"]); cx.assert( indoc! {" The qu|ick brown @@ -457,7 +457,7 @@ mod test { The quick brown fox ju|mps over"}, ); - let mut cx = cx.binding(["shift-V", "j", "x"]); + let mut cx = cx.binding(["shift-v", "j", "x"]); cx.assert( indoc! {" The qu|ick brown @@ -558,7 +558,7 @@ mod test { #[gpui::test] async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert); cx.assert( indoc! {" The qu|ick brown @@ -597,7 +597,7 @@ mod test { fox jumps over |"}, ); - let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert); + let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert); cx.assert( indoc! {" The qu|ick brown diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1b96cd9a12eec54bb48e672ba7b8178dcde87410..7ba8badc9dce7d39d85c552574433a45cd558e53 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -873,6 +873,13 @@ impl Pane { }; let is_pane_active = self.is_active; + + let tab_styles = match is_pane_active { + true => theme.workspace.tab_bar.active_pane.clone(), + false => theme.workspace.tab_bar.inactive_pane.clone(), + }; + let filler_style = tab_styles.inactive_tab.clone(); + let mut row = Flex::row().scrollable::(1, autoscroll, cx); for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() { let item_id = item.id(); @@ -890,12 +897,11 @@ impl Pane { }; row.add_child({ - let mut tab_style = match (is_pane_active, is_tab_active) { - (true, true) => theme.workspace.active_pane_active_tab.clone(), - (true, false) => theme.workspace.active_pane_inactive_tab.clone(), - (false, true) => theme.workspace.inactive_pane_active_tab.clone(), - (false, false) => theme.workspace.inactive_pane_inactive_tab.clone(), + let mut tab_style = match is_tab_active { + true => tab_styles.active_tab.clone(), + false => tab_styles.inactive_tab.clone(), }; + let title = item.tab_content(detail, &tab_style, cx); if ix == 0 { @@ -1003,17 +1009,11 @@ impl Pane { }) } - let filler_style = if is_pane_active { - &theme.workspace.active_pane_inactive_tab - } else { - &theme.workspace.inactive_pane_inactive_tab - }; - row.add_child( Empty::new() .contained() .with_style(filler_style.container) - .with_border(theme.workspace.active_pane_active_tab.container.border) + .with_border(filler_style.container.border) .flex(0., true) .named("filler"), ); @@ -1088,7 +1088,8 @@ impl View for Pane { 0, cx, |mouse_state, cx| { - let theme = &cx.global::().theme.workspace; + let theme = + &cx.global::().theme.workspace.tab_bar; let style = theme.pane_button.style_for(mouse_state, false); Svg::new("icons/split_12.svg") @@ -1118,13 +1119,7 @@ impl View for Pane { tab_row .constrained() - .with_height( - cx.global::() - .theme - .workspace - .active_pane_active_tab - .height, - ) + .with_height(cx.global::().theme.workspace.tab_bar.height) .boxed() }) .with_child(ChildView::new(&self.toolbar).boxed()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0321e770cb795eb860a41bab9604ae90047d7c02..c060f57072692767305e12e55fed7c2c80ce7481 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -949,11 +949,11 @@ impl Workspace { &mut self, cx: &mut ViewContext, app_state: Arc, - mut callback: F, + callback: F, ) -> T where T: 'static, - F: FnMut(&mut Workspace, &mut ViewContext) -> T, + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, { if self.project.read(cx).is_local() { callback(self, cx) @@ -1811,7 +1811,7 @@ impl Workspace { match &*self.client.status().borrow() { client::Status::ConnectionError | client::Status::ConnectionLost - | client::Status::Reauthenticating + | client::Status::Reauthenticating { .. } | client::Status::Reconnecting { .. } | client::Status::ReconnectionError { .. } => Some( Container::new( @@ -2821,7 +2821,9 @@ mod tests { project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), - project.entry_for_path(&(worktree_id, "one.txt").into(), cx) + project + .entry_for_path(&(worktree_id, "one.txt").into(), cx) + .map(|e| e.id) ); }); assert_eq!( @@ -2838,7 +2840,9 @@ mod tests { project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), - project.entry_for_path(&(worktree_id, "two.txt").into(), cx) + project + .entry_for_path(&(worktree_id, "two.txt").into(), cx) + .map(|e| e.id) ); }); @@ -2856,7 +2860,9 @@ mod tests { project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), - project.entry_for_path(&(worktree_id, "one.txt").into(), cx) + project + .entry_for_path(&(worktree_id, "one.txt").into(), cx) + .map(|e| e.id) ); }); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 0fe39b4096d61ae0cc42b868c18a1a81616800f5..be20be2fd7fb0e9df29956cf6344b10c7325d1be 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.48.1" +version = "0.49.1" [lib] name = "zed" diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 54554beaf6c52a47d98b9e16aa8be3dac766a5b8..a4db4b9a755d030de8d98039beea31fa5312e109 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,34 +249,37 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::MutableAppContext; - use language::{Buffer, IndentSize}; + use language::{AutoindentMode, Buffer}; + use settings::Settings; use std::sync::Arc; #[gpui::test] fn test_c_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); let language = crate::languages::language("c", tree_sitter_c::language(), None); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); // empty function - buffer.edit_with_autoindent([(0..0, "int main() {}")], size, cx); + buffer.edit([(0..0, "int main() {}")], None, cx); // indent inside braces let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], size, cx); + buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n.c")], size, cx); + buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index ca0b24bda78f024701ecdec2582cb0225a9dc9da..801c7c96f9539af14d8013f9292291e6ee42b54f 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -147,20 +147,23 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, MutableAppContext}; - use language::{Buffer, IndentSize}; + use language::{AutoindentMode, Buffer}; + use settings::Settings; use std::sync::Arc; #[gpui::test] fn test_python_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("python", tree_sitter_python::language(), None); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit_with_autoindent([(ix..ix, text)], size, cx); + buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); }; // indent after "def():" @@ -204,7 +207,11 @@ mod tests { // dedent the closing paren if it is shifted to the beginning of the line let argument_ix = buffer.text().find("1").unwrap(); - buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], size, cx); + buffer.edit( + [(argument_ix..argument_ix + 1, "")], + Some(AutoindentMode::EachLine), + cx, + ); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )" @@ -219,7 +226,11 @@ mod tests { // manually outdent the last line let end_whitespace_ix = buffer.len() - 4; - buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], size, cx); + buffer.edit( + [(end_whitespace_ix..buffer.len(), "")], + Some(AutoindentMode::EachLine), + cx, + ); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )\n" @@ -233,7 +244,7 @@ mod tests { ); // reset to a simple if statement - buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], cx); + buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx); // dedent "else" on the line after a closing paren append(&mut buffer, "\n else:\n", cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 18d49f78d478840876caadec32a53e5e9b5ee544..adbe4312796ed5407942a5ca084eabd17a3c4deb 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -257,6 +257,7 @@ mod tests { use super::*; use crate::languages::{language, CachedLspAdapter}; use gpui::{color::Color, MutableAppContext}; + use settings::Settings; use theme::SyntaxTheme; #[gpui::test] @@ -433,37 +434,39 @@ mod tests { fn test_rust_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("rust", tree_sitter_rust::language(), None); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n")], size, cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "b\n.c")], size, cx); + buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], size, cx); + buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, ";\ne")], size, cx); + buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -472,17 +475,17 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], size, cx); + buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit_with_autoindent([(ix..ix, "\n")], size, cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 337e3afc2dc03d04d4753377c31f584067fc7871..ed718272d8fb50be0d4e96da9434ed9e4c854836 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -28,15 +28,7 @@ use project::{Fs, ProjectStore}; use serde_json::json; use settings::{self, KeymapFileContent, Settings, SettingsFileContent}; use smol::process::Command; -use std::{ - env, - ffi::OsStr, - fs, panic, - path::{Path, PathBuf}, - sync::Arc, - thread, - time::Duration, -}; +use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration}; use terminal; use theme::ThemeRegistry; use util::{ResultExt, TryFutureExt}; @@ -50,20 +42,17 @@ use zed::{ fn main() { let http = http::client(); - let home_dir = dirs::home_dir().expect("could not find home dir"); - let db_dir_path = home_dir.join("Library/Application Support/Zed"); - let logs_dir_path = home_dir.join("Library/Logs/Zed"); - fs::create_dir_all(&db_dir_path).expect("could not create database path"); - fs::create_dir_all(&logs_dir_path).expect("could not create logs path"); - init_logger(&logs_dir_path); + init_paths(); + init_logger(); + log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); let app_version = ZED_APP_VERSION .or_else(|| app.platform().app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); - init_panic_hook(logs_dir_path, app_version, http.clone(), app.background()); + init_panic_hook(app_version, http.clone(), app.background()); let db = app.background().spawn(async move { - project::Db::open(db_dir_path.join("zed.db")) + project::Db::open(&*zed::paths::DB) .log_err() .unwrap_or(project::Db::null()) }); @@ -99,7 +88,7 @@ fn main() { app.run(move |cx| { let client = client::Client::new(http.clone()); let mut languages = LanguageRegistry::new(login_shell_env_loaded); - languages.set_language_server_download_dir(zed::ROOT_PATH.clone()); + languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); let init_languages = cx .background() @@ -204,35 +193,57 @@ fn main() { }); } -fn init_logger(logs_dir_path: &Path) { +fn init_paths() { + fs::create_dir_all(&*zed::paths::CONFIG_DIR).expect("could not create config path"); + fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); + fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); + fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); + + // Copy setting files from legacy locations. TODO: remove this after a few releases. + thread::spawn(|| { + if fs::metadata(&*zed::paths::legacy::SETTINGS).is_ok() + && fs::metadata(&*zed::paths::SETTINGS).is_err() + { + fs::copy(&*zed::paths::legacy::SETTINGS, &*zed::paths::SETTINGS).log_err(); + } + + if fs::metadata(&*zed::paths::legacy::KEYMAP).is_ok() + && fs::metadata(&*zed::paths::KEYMAP).is_err() + { + fs::copy(&*zed::paths::legacy::KEYMAP, &*zed::paths::KEYMAP).log_err(); + } + }); +} + +fn init_logger() { if stdout_is_a_pty() { env_logger::init(); } else { let level = LevelFilter::Info; - let log_file_path = logs_dir_path.join("Zed.log"); + + // Prevent log file from becoming too large. + const MAX_LOG_BYTES: u64 = 1 * 1024 * 1024; + if fs::metadata(&*zed::paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) + { + let _ = fs::rename(&*zed::paths::LOG, &*zed::paths::OLD_LOG); + } + let log_file = OpenOptions::new() .create(true) .append(true) - .open(log_file_path) + .open(&*zed::paths::LOG) .expect("could not open logfile"); simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file) .expect("could not initialize logger"); } } -fn init_panic_hook( - logs_dir_path: PathBuf, - app_version: String, - http: Arc, - background: Arc, -) { +fn init_panic_hook(app_version: String, http: Arc, background: Arc) { background .spawn({ - let logs_dir_path = logs_dir_path.clone(); - async move { let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); - let mut children = smol::fs::read_dir(&logs_dir_path).await?; + let mut children = smol::fs::read_dir(&*zed::paths::LOGS_DIR).await?; while let Some(child) = children.next().await { let child = child?; let child_path = child.path(); @@ -322,7 +333,7 @@ fn init_panic_hook( let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); fs::write( - logs_dir_path.join(format!("zed-{}-{}.panic", app_version, panic_filename)), + zed::paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)), &message, ) .context("error writing panic to disk") @@ -456,8 +467,8 @@ fn load_config_files( .clone() .spawn(async move { let settings_file = - WatchedJsonFile::new(fs.clone(), &executor, zed::SETTINGS_PATH.clone()).await; - let keymap_file = WatchedJsonFile::new(fs, &executor, zed::KEYMAP_PATH.clone()).await; + WatchedJsonFile::new(fs.clone(), &executor, zed::paths::SETTINGS.clone()).await; + let keymap_file = WatchedJsonFile::new(fs, &executor, zed::paths::KEYMAP.clone()).await; tx.send((settings_file, keymap_file)).ok() }) .detach(); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index e8297a172708ab6f3fee75282c49d4fd0bc9d038..73817ca2e39a8aa6d758bbbd1156850d27efcfdc 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -274,6 +274,10 @@ pub fn menus() -> Vec> { name: "Go to Definition", action: Box::new(editor::GoToDefinition), }, + MenuItem::Action { + name: "Go to Type Definition", + action: Box::new(editor::GoToTypeDefinition), + }, MenuItem::Action { name: "Go to References", action: Box::new(editor::FindAllReferences), diff --git a/crates/zed/src/paths.rs b/crates/zed/src/paths.rs new file mode 100644 index 0000000000000000000000000000000000000000..6643cc0fe6d40c7d91038e0634eab8b8f27de0cf --- /dev/null +++ b/crates/zed/src/paths.rs @@ -0,0 +1,24 @@ +use std::path::PathBuf; + +lazy_static::lazy_static! { + static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); + pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); + pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); + pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); + pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); + pub static ref DB: PathBuf = DB_DIR.join("zed.db"); + pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); + pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); + pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); + pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); +} + +pub mod legacy { + use std::path::PathBuf; + + lazy_static::lazy_static! { + static ref CONFIG_DIR: PathBuf = super::HOME.join(".zed"); + pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); + pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 867913fc7ba914ed18461cc212b4fed91e72d44c..2f683d8e8e4093a728117d6a8f637f317957e33c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,6 +1,7 @@ mod feedback; pub mod languages; pub mod menus; +pub mod paths; pub mod settings_file; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -9,6 +10,7 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; +use collections::VecDeque; pub use contacts_panel; use contacts_panel::ContactsPanel; pub use editor; @@ -21,7 +23,6 @@ use gpui::{ AssetSource, AsyncAppContext, ViewContext, }; use language::Rope; -use lazy_static::lazy_static; pub use lsp; pub use project::{self, fs}; use project_panel::ProjectPanel; @@ -29,11 +30,7 @@ use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; -use std::{ - path::{Path, PathBuf}, - str, - sync::Arc, -}; +use std::{env, path::Path, str, sync::Arc}; use util::ResultExt; pub use workspace; use workspace::{sidebar::Side, AppState, Workspace}; @@ -52,6 +49,7 @@ actions!( Quit, DebugElements, OpenSettings, + OpenLog, OpenKeymap, OpenDefaultSettings, OpenDefaultKeymap, @@ -64,14 +62,6 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; -lazy_static! { - pub static ref ROOT_PATH: PathBuf = dirs::home_dir() - .expect("failed to determine home directory") - .join(".zed"); - pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json"); - pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json"); -} - pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action(about); cx.add_global_action(quit); @@ -108,7 +98,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - open_config_file(&SETTINGS_PATH, app_state.clone(), cx, || { + open_config_file(&paths::SETTINGS, app_state.clone(), cx, || { str::from_utf8( Assets .load("settings/initial_user_settings.json") @@ -120,10 +110,16 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }); } }); + cx.add_action({ + let app_state = app_state.clone(); + move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { + open_log_file(workspace, app_state.clone(), cx); + } + }); cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - open_config_file(&KEYMAP_PATH, app_state.clone(), cx, || Default::default()); + open_config_file(&paths::KEYMAP, app_state.clone(), cx, || Default::default()); } }); cx.add_action({ @@ -227,11 +223,11 @@ pub fn initialize_workspace( }, "schemas": [ { - "fileMatch": [".zed/settings.json"], + "fileMatch": [schema_file_match(&*paths::SETTINGS)], "schema": settings_file_json_schema(theme_names, language_names), }, { - "fileMatch": [".zed/keymap.json"], + "fileMatch": [schema_file_match(&*paths::KEYMAP)], "schema": keymap_file_json_schema(&action_names), } ] @@ -389,7 +385,6 @@ fn open_config_file( cx.spawn(|workspace, mut cx| async move { let fs = &app_state.fs; if !fs.is_file(path).await { - fs.create_dir(&ROOT_PATH).await?; fs.create_file(path, Default::default()).await?; fs.save(path, &default_content(), Default::default()) .await?; @@ -407,6 +402,60 @@ fn open_config_file( .detach_and_log_err(cx) } +fn open_log_file( + workspace: &mut Workspace, + app_state: Arc, + cx: &mut ViewContext, +) { + const MAX_LINES: usize = 1000; + + workspace.with_local_workspace(cx, app_state.clone(), |_, cx| { + cx.spawn_weak(|workspace, mut cx| async move { + let (old_log, new_log) = futures::join!( + app_state.fs.load(&paths::OLD_LOG), + app_state.fs.load(&paths::LOG) + ); + + if let Some(workspace) = workspace.upgrade(&cx) { + let mut lines = VecDeque::with_capacity(MAX_LINES); + for line in old_log + .iter() + .flat_map(|log| log.lines()) + .chain(new_log.iter().flat_map(|log| log.lines())) + { + if lines.len() == MAX_LINES { + lines.pop_front(); + } + lines.push_back(line); + } + let log = lines + .into_iter() + .flat_map(|line| [line, "\n"]) + .collect::(); + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx)); + + let buffer = cx.add_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Log".into()) + }); + workspace.add_item( + Box::new( + cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)), + ), + cx, + ); + }); + } + }) + .detach(); + }); +} + fn open_bundled_config_file( workspace: &mut Workspace, app_state: Arc, @@ -431,6 +480,11 @@ fn open_bundled_config_file( }); } +fn schema_file_match(path: &Path) -> &Path { + path.strip_prefix(path.parent().unwrap().parent().unwrap()) + .unwrap() +} + #[cfg(test)] mod tests { use super::*; diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index a7c69d7660c695c5d2c439fdcd94c9a327b49d4f..0244641ec77ec62d21160ef719ac14b7d3b0c895 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/styles/src/styleTree/contextMenu.ts @@ -18,7 +18,7 @@ export default function contextMenu(theme: Theme) { item: { padding: { left: 4, right: 4, top: 2, bottom: 2 }, cornerRadius: 6, - label: text(theme, "sans", "secondary", { size: "sm" }), + label: text(theme, "sans", "primary", { size: "sm" }), keystroke: { ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }), padding: { left: 3, right: 3 }, @@ -29,7 +29,7 @@ export default function contextMenu(theme: Theme) { }, active: { background: backgroundColor(theme, 300, "active"), - text: text(theme, "sans", "primary", { size: "sm" }), + text: text(theme, "sans", "active", { size: "sm" }), }, activeHover: { background: backgroundColor(theme, 300, "hovered"), diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 507eb74d8d39543f8aaa8859cf43c5eb9056f0e4..e3c1844974fa1bc56c7bb7c402250cf81fece7e7 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -55,7 +55,7 @@ export default function editor(theme: Theme) { textColor: theme.syntax.primary.color, background: backgroundColor(theme, 500), activeLineBackground: theme.editor.line.active, - codeActionsIndicator: iconColor(theme, "muted"), + codeActionsIndicator: iconColor(theme, "secondary"), diffBackgroundDeleted: backgroundColor(theme, "error"), diffBackgroundInserted: backgroundColor(theme, "ok"), documentHighlightReadBackground: theme.editor.highlight.occurrence, @@ -107,7 +107,7 @@ export default function editor(theme: Theme) { top: true, }), code: { - ...text(theme, "mono", "muted", { size: "sm" }), + ...text(theme, "mono", "secondary", { size: "sm" }), margin: { left: 10, }, @@ -135,17 +135,17 @@ export default function editor(theme: Theme) { warningDiagnostic: diagnostic(theme, "warning"), informationDiagnostic: diagnostic(theme, "info"), hintDiagnostic: diagnostic(theme, "info"), - invalidErrorDiagnostic: diagnostic(theme, "muted"), - invalidHintDiagnostic: diagnostic(theme, "muted"), - invalidInformationDiagnostic: diagnostic(theme, "muted"), - invalidWarningDiagnostic: diagnostic(theme, "muted"), + invalidErrorDiagnostic: diagnostic(theme, "secondary"), + invalidHintDiagnostic: diagnostic(theme, "secondary"), + invalidInformationDiagnostic: diagnostic(theme, "secondary"), + invalidWarningDiagnostic: diagnostic(theme, "secondary"), hoverPopover: hoverPopover(theme), linkDefinition: { color: theme.syntax.linkUri.color, underline: theme.syntax.linkUri.underline, }, jumpIcon: { - color: iconColor(theme, "muted"), + color: iconColor(theme, "secondary"), iconWidth: 20, buttonWidth: 20, cornerRadius: 6, @@ -157,7 +157,7 @@ export default function editor(theme: Theme) { }, hover: { color: iconColor(theme, "active"), - background: backgroundColor(theme, "on500", "base"), + background: backgroundColor(theme, "on500"), }, }, compositionMark: { diff --git a/styles/src/styleTree/picker.ts b/styles/src/styleTree/picker.ts index 84a72aabcc4b91977247b8d1cc299ab22338004c..514deb4c339efee074a01ee4ee425e9243a06087 100644 --- a/styles/src/styleTree/picker.ts +++ b/styles/src/styleTree/picker.ts @@ -24,7 +24,7 @@ export default function picker(theme: Theme) { highlightText: text(theme, "sans", "feature", { weight: "bold" }), active: { background: backgroundColor(theme, 300, "active"), - text: text(theme, "sans", "primary"), + text: text(theme, "sans", "active"), }, hover: { background: backgroundColor(theme, 300, "hovered"), @@ -32,7 +32,7 @@ export default function picker(theme: Theme) { }, border: border(theme, "primary"), empty: { - text: text(theme, "sans", "placeholder"), + text: text(theme, "sans", "muted"), padding: { bottom: 4, left: 12, diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 7007a5da2d5d7d525ed879ea7ab9be1fdda0051a..26f88c6784111bcf1a0b2f33aa44a4d07baaf71a 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -12,25 +12,24 @@ export default function projectPanel(theme: Theme) { iconColor: iconColor(theme, "muted"), iconSize: 8, iconSpacing: 8, - text: text(theme, "mono", "muted", { size: "sm" }), + text: text(theme, "mono", "secondary", { size: "sm" }), hover: { background: backgroundColor(theme, 300, "hovered"), - text: text(theme, "mono", "primary", { size: "sm" }), }, active: { background: backgroundColor(theme, 300, "active"), - text: text(theme, "mono", "primary", { size: "sm" }), + text: text(theme, "mono", "active", { size: "sm" }), }, activeHover: { - background: backgroundColor(theme, 300, "hovered"), + background: backgroundColor(theme, 300, "active"), text: text(theme, "mono", "active", { size: "sm" }), }, }, cutEntryFade: 0.4, ignoredEntryFade: 0.6, filenameEditor: { - background: backgroundColor(theme, 500, "active"), - text: text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, "on300"), + text: text(theme, "mono", "active", { size: "sm" }), selection: player(theme, 1).selection, }, }; diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index d5d63fdadf6befdf6120d29ae6bc6dffd4235479..2a4c6d7f70736aa0aa39c3f1596d10beb25aef2b 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -23,28 +23,28 @@ export default function statusBar(theme: Theme) { right: 6, }, border: border(theme, "primary", { top: true, overlay: true }), - cursorPosition: text(theme, "sans", "muted"), - autoUpdateProgressMessage: text(theme, "sans", "muted"), - autoUpdateDoneMessage: text(theme, "sans", "muted"), + cursorPosition: text(theme, "sans", "secondary"), + autoUpdateProgressMessage: text(theme, "sans", "secondary"), + autoUpdateDoneMessage: text(theme, "sans", "secondary"), lspStatus: { ...diagnosticStatusContainer, iconSpacing: 4, iconWidth: 14, height: 18, - message: text(theme, "sans", "muted"), + message: text(theme, "sans", "secondary"), iconColor: iconColor(theme, "muted"), hover: { message: text(theme, "sans", "primary"), - iconColor: iconColor(theme, "active"), + iconColor: iconColor(theme, "primary"), background: backgroundColor(theme, 300, "hovered"), }, }, diagnosticMessage: { - ...text(theme, "sans", "muted"), - hover: text(theme, "sans", "secondary"), + ...text(theme, "sans", "secondary"), + hover: text(theme, "sans", "active"), }, feedback: { - ...text(theme, "sans", "muted"), + ...text(theme, "sans", "secondary"), hover: text(theme, "sans", "active"), }, diagnosticSummary: { @@ -53,7 +53,7 @@ export default function statusBar(theme: Theme) { iconSpacing: 2, summarySpacing: 6, text: text(theme, "sans", "primary", { size: "sm" }), - iconColorOk: iconColor(theme, "secondary"), + iconColorOk: iconColor(theme, "muted"), iconColorWarning: iconColor(theme, "warning"), iconColorError: iconColor(theme, "error"), containerOk: { @@ -95,7 +95,7 @@ export default function statusBar(theme: Theme) { item: { ...statusContainer, iconSize: 16, - iconColor: iconColor(theme, "secondary"), + iconColor: iconColor(theme, "muted"), hover: { iconColor: iconColor(theme, "active"), background: backgroundColor(theme, 300, "hovered"), diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts new file mode 100644 index 0000000000000000000000000000000000000000..66da26d7ed7c3134a5a9b9a089ff78136e9d52de --- /dev/null +++ b/styles/src/styleTree/tabBar.ts @@ -0,0 +1,87 @@ +import Theme from "../themes/common/theme"; +import { iconColor, text, border, backgroundColor } from "./components"; + +export default function tabBar(theme: Theme) { + const height = 32; + + const tab = { + height, + background: backgroundColor(theme, 300), + border: border(theme, "primary", { + left: true, + bottom: true, + overlay: true, + }), + iconClose: iconColor(theme, "muted"), + iconCloseActive: iconColor(theme, "active"), + iconConflict: iconColor(theme, "warning"), + iconDirty: iconColor(theme, "info"), + iconWidth: 8, + spacing: 8, + text: text(theme, "sans", "secondary", { size: "sm" }), + padding: { + left: 8, + right: 8, + }, + description: { + margin: { left: 6, top: 1 }, + ...text(theme, "sans", "muted", { size: "2xs" }) + } + }; + + const activePaneActiveTab = { + ...tab, + background: backgroundColor(theme, 500), + text: text(theme, "sans", "active", { size: "sm" }), + border: { + ...tab.border, + bottom: false + }, + }; + + const inactivePaneInactiveTab = { + ...tab, + background: backgroundColor(theme, 300), + text: text(theme, "sans", "muted", { size: "sm" }), + }; + + const inactivePaneActiveTab = { + ...tab, + background: backgroundColor(theme, 500), + text: text(theme, "sans", "secondary", { size: "sm" }), + border: { + ...tab.border, + bottom: false + }, + } + + return { + height, + background: backgroundColor(theme, 300), + border: border(theme, "primary", { + left: true, + bottom: true, + overlay: true, + }), + activePane: { + activeTab: activePaneActiveTab, + inactiveTab: tab, + }, + inactivePane: { + activeTab: inactivePaneActiveTab, + inactiveTab: inactivePaneInactiveTab, + }, + paneButton: { + color: iconColor(theme, "secondary"), + border: { + ...tab.border, + }, + iconWidth: 12, + buttonWidth: activePaneActiveTab.height, + hover: { + color: iconColor(theme, "active"), + background: backgroundColor(theme, 300), + }, + }, + } +} \ No newline at end of file diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts index 8d1bb8078dd32b3bdfbfcf8b6b8d0d33bc82d463..7545ad762ef0f8bf9564864e0b8f08317ee53ba9 100644 --- a/styles/src/styleTree/tooltip.ts +++ b/styles/src/styleTree/tooltip.ts @@ -9,13 +9,13 @@ export default function tooltip(theme: Theme) { margin: { top: 6, left: 6 }, shadow: popoverShadow(theme), cornerRadius: 6, - text: text(theme, "sans", "secondary", { size: "xs", weight: "bold" }), + text: text(theme, "sans", "primary", { size: "xs" }), keystroke: { background: backgroundColor(theme, "on500"), cornerRadius: 4, margin: { left: 6 }, padding: { left: 4, right: 4 }, - ...text(theme, "mono", "muted", { size: "xs", weight: "bold" }), + ...text(theme, "mono", "secondary", { size: "xs", weight: "bold" }), }, maxTextWidth: 200, }; diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index c86dce72262112c5f5b49301d2ed481c01036604..875b2bb2d58c966bb7b63d5d05ee6baadec59c99 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -8,58 +8,13 @@ import { text, } from "./components"; import statusBar from "./statusBar"; +import tabBar from "./tabBar"; export function workspaceBackground(theme: Theme) { return backgroundColor(theme, 300); } export default function workspace(theme: Theme) { - const activePaneInactiveTab = { - height: 32, - background: workspaceBackground(theme), - iconClose: iconColor(theme, "muted"), - iconCloseActive: iconColor(theme, "active"), - iconConflict: iconColor(theme, "warning"), - iconDirty: iconColor(theme, "info"), - iconWidth: 8, - spacing: 8, - text: text(theme, "sans", "secondary", { size: "sm" }), - border: border(theme, "primary", { - left: true, - bottom: true, - overlay: true, - }), - padding: { - left: 8, - right: 8, - }, - description: { - margin: { left: 6, top: 1 }, - ...text(theme, "sans", "muted", { size: "2xs" }) - } - }; - - const activePaneActiveTab = { - ...activePaneInactiveTab, - background: backgroundColor(theme, 500), - text: text(theme, "sans", "active", { size: "sm" }), - border: { - ...activePaneInactiveTab.border, - bottom: false, - }, - }; - - const inactivePaneInactiveTab = { - ...activePaneInactiveTab, - background: backgroundColor(theme, 100), - text: text(theme, "sans", "placeholder", { size: "sm" }), - }; - - const inactivePaneActiveTab = { - ...activePaneInactiveTab, - text: text(theme, "sans", "placeholder", { size: "sm" }), - } - const titlebarPadding = 6; return { @@ -74,22 +29,7 @@ export default function workspace(theme: Theme) { }, leaderBorderOpacity: 0.7, leaderBorderWidth: 2.0, - activePaneActiveTab, - activePaneInactiveTab, - inactivePaneActiveTab, - inactivePaneInactiveTab, - paneButton: { - color: iconColor(theme, "secondary"), - border: { - ...activePaneActiveTab.border, - }, - iconWidth: 12, - buttonWidth: activePaneActiveTab.height, - hover: { - color: iconColor(theme, "active"), - background: backgroundColor(theme, 300), - }, - }, + tabBar: tabBar(theme), modal: { margin: { bottom: 52, @@ -188,7 +128,7 @@ export default function workspace(theme: Theme) { cornerRadius: 6, hover: { color: iconColor(theme, "active"), - background: backgroundColor(theme, 300), + background: backgroundColor(theme, "on500", "hovered"), }, disabled: { color: withOpacity(iconColor(theme, "muted"), 0.6), diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 321184d40d77976058e952c90f840c8088aa6f2f..7aa72ef1377ea40656a45e50bc4155f28ac7f8a5 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -130,8 +130,8 @@ export function createTheme( const textColor = { primary: sample(ramps.neutral, 6), secondary: sample(ramps.neutral, 5), - muted: sample(ramps.neutral, 5), - placeholder: sample(ramps.neutral, 4), + muted: sample(ramps.neutral, 4), + placeholder: sample(ramps.neutral, 3), active: sample(ramps.neutral, 7), feature: sample(ramps.blue, 0.5), ok: sample(ramps.green, 0.5),