Merge branch 'main' into in-app-feedback

Joseph Lyons created

Change summary

crates/collab/src/tests/randomized_integration_tests.rs | 68 ++++++++++
crates/editor/src/editor.rs                             | 45 +++++++
crates/editor/src/editor_tests.rs                       | 14 ++
crates/language/src/buffer.rs                           |  1 
crates/project/src/project.rs                           | 29 ++-
crates/workspace/src/workspace.rs                       |  7 
6 files changed, 146 insertions(+), 18 deletions(-)

Detailed changes

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -14,8 +14,11 @@ use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf1
 use lsp::FakeLanguageServer;
 use parking_lot::Mutex;
 use project::{search::SearchQuery, Project};
-use rand::prelude::*;
-use std::{env, path::PathBuf, sync::Arc};
+use rand::{
+    distributions::{Alphanumeric, DistString},
+    prelude::*,
+};
+use std::{env, ffi::OsStr, path::PathBuf, sync::Arc};
 
 #[gpui::test(iterations = 100)]
 async fn test_random_collaboration(
@@ -382,9 +385,15 @@ async fn test_random_collaboration(
                         );
                     }
                     (None, None) => {}
-                    (None, _) => panic!("host's file is None, guest's isn't "),
-                    (_, None) => panic!("guest's file is None, hosts's isn't "),
+                    (None, _) => panic!("host's file is None, guest's isn't"),
+                    (_, None) => panic!("guest's file is None, hosts's isn't"),
                 }
+
+                let host_diff_base =
+                    host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
+                let guest_diff_base = guest_buffer
+                    .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
+                assert_eq!(guest_diff_base, host_diff_base);
             }
         }
     }
@@ -543,9 +552,10 @@ async fn randomly_mutate_client(
         50..=59 if !client.local_projects.is_empty() || !client.remote_projects.is_empty() => {
             randomly_mutate_worktrees(client, &rng, cx).await?;
         }
-        60..=84 if !client.local_projects.is_empty() || !client.remote_projects.is_empty() => {
+        60..=74 if !client.local_projects.is_empty() || !client.remote_projects.is_empty() => {
             randomly_query_and_mutate_buffers(client, &rng, cx).await?;
         }
+        75..=84 => randomly_mutate_git(client, &rng).await,
         _ => randomly_mutate_fs(client, &rng).await,
     }
 
@@ -605,6 +615,54 @@ async fn randomly_mutate_active_call(
     Ok(())
 }
 
+async fn randomly_mutate_git(client: &mut TestClient, rng: &Mutex<StdRng>) {
+    let directories = client.fs.directories().await;
+    let mut dir_path = directories.choose(&mut *rng.lock()).unwrap().clone();
+    if dir_path.file_name() == Some(OsStr::new(".git")) {
+        dir_path.pop();
+    }
+    let mut git_dir_path = dir_path.clone();
+    git_dir_path.push(".git");
+
+    if !directories.contains(&git_dir_path) {
+        log::info!(
+            "{}: creating git directory at {:?}",
+            client.username,
+            git_dir_path
+        );
+        client.fs.create_dir(&git_dir_path).await.unwrap();
+    }
+
+    let mut child_paths = client.fs.read_dir(&dir_path).await.unwrap();
+    let mut child_file_paths = Vec::new();
+    while let Some(child_path) = child_paths.next().await {
+        let child_path = child_path.unwrap();
+        if client.fs.is_file(&child_path).await {
+            child_file_paths.push(child_path);
+        }
+    }
+    let count = rng.lock().gen_range(0..=child_file_paths.len());
+    child_file_paths.shuffle(&mut *rng.lock());
+    child_file_paths.truncate(count);
+
+    let mut new_index = Vec::new();
+    for abs_child_file_path in &child_file_paths {
+        let child_file_path = abs_child_file_path.strip_prefix(&dir_path).unwrap();
+        let new_base = Alphanumeric.sample_string(&mut *rng.lock(), 16);
+        new_index.push((child_file_path, new_base));
+    }
+    log::info!(
+        "{}: updating Git index at {:?}: {:#?}",
+        client.username,
+        git_dir_path,
+        new_index
+    );
+    client
+        .fs
+        .set_index_for_repo(&git_dir_path, &new_index)
+        .await;
+}
+
 async fn randomly_mutate_fs(client: &mut TestClient, rng: &Mutex<StdRng>) {
     let is_dir = rng.lock().gen::<bool>();
     let mut new_path = client

crates/editor/src/editor.rs 🔗

@@ -828,6 +828,23 @@ impl CompletionsMenu {
                 })
                 .collect()
         };
+
+        //Remove all candidates where the query's start does not match the start of any word in the candidate
+        if let Some(query) = query {
+            if let Some(query_start) = query.chars().next() {
+                matches.retain(|string_match| {
+                    split_words(&string_match.string).any(|word| {
+                        //Check that the first codepoint of the word as lowercase matches the first
+                        //codepoint of the query as lowercase
+                        word.chars()
+                            .flat_map(|codepoint| codepoint.to_lowercase())
+                            .zip(query_start.to_lowercase())
+                            .all(|(word_cp, query_cp)| word_cp == query_cp)
+                    })
+                });
+            }
+        }
+
         matches.sort_unstable_by_key(|mat| {
             let completion = &self.completions[mat.candidate_id];
             (
@@ -6811,6 +6828,34 @@ pub fn styled_runs_for_code_label<'a>(
         })
 }
 
+pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
+    let mut index = 0;
+    let mut codepoints = text.char_indices().peekable();
+
+    std::iter::from_fn(move || {
+        let start_index = index;
+        while let Some((new_index, codepoint)) = codepoints.next() {
+            index = new_index + codepoint.len_utf8();
+            let current_upper = codepoint.is_uppercase();
+            let next_upper = codepoints
+                .peek()
+                .map(|(_, c)| c.is_uppercase())
+                .unwrap_or(false);
+
+            if !current_upper && next_upper {
+                return Some(&text[start_index..index]);
+            }
+        }
+
+        index = text.len();
+        if start_index < text.len() {
+            return Some(&text[start_index..]);
+        }
+        None
+    })
+    .flat_map(|word| word.split_inclusive('_'))
+}
+
 trait RangeExt<T> {
     fn sorted(&self) -> Range<T>;
     fn to_inclusive(&self) -> RangeInclusive<T>;

crates/editor/src/editor_tests.rs 🔗

@@ -5445,6 +5445,20 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
     );
 }
 
+#[test]
+fn test_split_words() {
+    fn split<'a>(text: &'a str) -> Vec<&'a str> {
+        split_words(text).collect()
+    }
+
+    assert_eq!(split("HelloWorld"), &["Hello", "World"]);
+    assert_eq!(split("hello_world"), &["hello_", "world"]);
+    assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
+    assert_eq!(split("Hello_World"), &["Hello_", "World"]);
+    assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
+    assert_eq!(split("helloworld"), &["helloworld"]);
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/language/src/buffer.rs 🔗

@@ -682,7 +682,6 @@ impl Buffer {
         task
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn diff_base(&self) -> Option<&str> {
         self.diff_base.as_deref()
     }

crates/project/src/project.rs 🔗

@@ -5189,20 +5189,27 @@ impl Project {
 
                     let operations = buffer.serialize_ops(Some(remote_version), cx);
                     let client = this.client.clone();
-                    let file = buffer.file().cloned();
+                    if let Some(file) = buffer.file() {
+                        client
+                            .send(proto::UpdateBufferFile {
+                                project_id,
+                                buffer_id: buffer_id as u64,
+                                file: Some(file.to_proto()),
+                            })
+                            .log_err();
+                    }
+
+                    client
+                        .send(proto::UpdateDiffBase {
+                            project_id,
+                            buffer_id: buffer_id as u64,
+                            diff_base: buffer.diff_base().map(Into::into),
+                        })
+                        .log_err();
+
                     cx.background()
                         .spawn(
                             async move {
-                                if let Some(file) = file {
-                                    client
-                                        .send(proto::UpdateBufferFile {
-                                            project_id,
-                                            buffer_id: buffer_id as u64,
-                                            file: Some(file.to_proto()),
-                                        })
-                                        .log_err();
-                                }
-
                                 let operations = operations.await;
                                 for chunk in split_operations(operations) {
                                     client

crates/workspace/src/workspace.rs 🔗

@@ -2143,7 +2143,6 @@ impl Workspace {
         let call = self.active_call()?;
         let room = call.read(cx).room()?.read(cx);
         let participant = room.remote_participant_for_peer_id(leader_id)?;
-
         let mut items_to_add = Vec::new();
         match participant.location {
             call::ParticipantLocation::SharedProject { project_id } => {
@@ -2154,6 +2153,12 @@ impl Workspace {
                             .and_then(|id| state.items_by_leader_view_id.get(&id))
                         {
                             items_to_add.push((pane.clone(), item.boxed_clone()));
+                        } else {
+                            if let Some(shared_screen) =
+                                self.shared_screen_for_peer(leader_id, pane, cx)
+                            {
+                                items_to_add.push((pane.clone(), Box::new(shared_screen)));
+                            }
                         }
                     }
                 }