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