From 56462ef7936b91c0b9b682a33be953b2c0cd36bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Oct 2023 10:59:29 +0200 Subject: [PATCH] Checkpoint --- Cargo.lock | 53 +- Cargo.toml | 3 + crates/fuzzy2/Cargo.toml | 13 + crates/fuzzy2/src/char_bag.rs | 63 + crates/fuzzy2/src/fuzzy2.rs | 10 + crates/fuzzy2/src/matcher.rs | 464 +++ crates/fuzzy2/src/paths.rs | 257 ++ crates/fuzzy2/src/strings.rs | 159 + crates/gpui2/src/executor.rs | 14 + crates/language2/Cargo.toml | 10 +- crates/language2/src/buffer.rs | 13 +- crates/language2/src/buffer_tests.rs | 4884 ++++++++++++------------ crates/language2/src/diagnostic_set.rs | 8 +- crates/language2/src/highlight_map.rs | 62 +- crates/language2/src/language2.rs | 83 +- crates/language2/src/outline.rs | 6 +- crates/language2/src/proto.rs | 2 +- crates/lsp2/Cargo.toml | 38 + crates/lsp2/src/lsp2.rs | 1167 ++++++ crates/theme2/Cargo.toml | 35 + crates/theme2/src/theme2.rs | 21 + crates/welcome/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- 23 files changed, 4833 insertions(+), 2536 deletions(-) create mode 100644 crates/fuzzy2/Cargo.toml create mode 100644 crates/fuzzy2/src/char_bag.rs create mode 100644 crates/fuzzy2/src/fuzzy2.rs create mode 100644 crates/fuzzy2/src/matcher.rs create mode 100644 crates/fuzzy2/src/paths.rs create mode 100644 crates/fuzzy2/src/strings.rs create mode 100644 crates/lsp2/Cargo.toml create mode 100644 crates/lsp2/src/lsp2.rs create mode 100644 crates/theme2/Cargo.toml create mode 100644 crates/theme2/src/theme2.rs diff --git a/Cargo.lock b/Cargo.lock index 676c9b563ab32c496fc2f19ef396def1ece8593a..81489e28902ca444eaca20666a185d73c56bedb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3129,6 +3129,14 @@ dependencies = [ "util", ] +[[package]] +name = "fuzzy2" +version = "0.1.0" +dependencies = [ + "gpui2", + "util", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -4174,14 +4182,14 @@ dependencies = [ "env_logger 0.9.3", "fs", "futures 0.3.28", - "fuzzy", + "fuzzy2", "git", "globset", "gpui2", "indoc", "lazy_static", "log", - "lsp", + "lsp2", "parking_lot 0.11.2", "postage", "rand 0.8.5", @@ -4198,6 +4206,7 @@ dependencies = [ "smol", "sum_tree", "text", + "theme2", "tree-sitter", "tree-sitter-elixir", "tree-sitter-embedded-template", @@ -4492,6 +4501,29 @@ dependencies = [ "url", ] +[[package]] +name = "lsp2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-pipe", + "collections", + "ctor", + "env_logger 0.9.3", + "futures 0.3.28", + "gpui2", + "log", + "lsp-types", + "parking_lot 0.11.2", + "postage", + "serde", + "serde_derive", + "serde_json", + "smol", + "unindent", + "util", +] + [[package]] name = "mach" version = "0.3.2" @@ -8343,6 +8375,23 @@ dependencies = [ "util", ] +[[package]] +name = "theme2" +version = "0.1.0" +dependencies = [ + "anyhow", + "fs", + "gpui2", + "indexmap 1.9.3", + "parking_lot 0.11.2", + "serde", + "serde_derive", + "serde_json", + "settings2", + "toml 0.5.11", + "util", +] + [[package]] name = "theme_selector" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 07e138c924b4486a1e6b44660e5637317a87e548..9090b830e2d9f40be2a391be5a5ead8a84aa64ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "crates/fs", "crates/fsevent", "crates/fuzzy", + "crates/fuzzy2", "crates/git", "crates/go_to_line", "crates/gpui", @@ -49,6 +50,7 @@ members = [ "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", + "crates/lsp2", "crates/media", "crates/menu", "crates/node_runtime", @@ -76,6 +78,7 @@ members = [ "crates/terminal", "crates/text", "crates/theme", + "crates/theme2", "crates/theme_selector", "crates/ui2", "crates/util", diff --git a/crates/fuzzy2/Cargo.toml b/crates/fuzzy2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..5b92a27a27fedaff8a1c05d99714238c29f5267b --- /dev/null +++ b/crates/fuzzy2/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fuzzy2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/fuzzy2.rs" +doctest = false + +[dependencies] +gpui2 = { path = "../gpui2" } +util = { path = "../util" } diff --git a/crates/fuzzy2/src/char_bag.rs b/crates/fuzzy2/src/char_bag.rs new file mode 100644 index 0000000000000000000000000000000000000000..8fc36368a159b671e8199248cbdef19929961459 --- /dev/null +++ b/crates/fuzzy2/src/char_bag.rs @@ -0,0 +1,63 @@ +use std::iter::FromIterator; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct CharBag(u64); + +impl CharBag { + pub fn is_superset(self, other: CharBag) -> bool { + self.0 & other.0 == other.0 + } + + fn insert(&mut self, c: char) { + let c = c.to_ascii_lowercase(); + if ('a'..='z').contains(&c) { + let mut count = self.0; + let idx = c as u8 - b'a'; + count >>= idx * 2; + count = ((count << 1) | 1) & 3; + count <<= idx * 2; + self.0 |= count; + } else if ('0'..='9').contains(&c) { + let idx = c as u8 - b'0'; + self.0 |= 1 << (idx + 52); + } else if c == '-' { + self.0 |= 1 << 62; + } + } +} + +impl Extend for CharBag { + fn extend>(&mut self, iter: T) { + for c in iter { + self.insert(c); + } + } +} + +impl FromIterator for CharBag { + fn from_iter>(iter: T) -> Self { + let mut result = Self::default(); + result.extend(iter); + result + } +} + +impl From<&str> for CharBag { + fn from(s: &str) -> Self { + let mut bag = Self(0); + for c in s.chars() { + bag.insert(c); + } + bag + } +} + +impl From<&[char]> for CharBag { + fn from(chars: &[char]) -> Self { + let mut bag = Self(0); + for c in chars { + bag.insert(*c); + } + bag + } +} diff --git a/crates/fuzzy2/src/fuzzy2.rs b/crates/fuzzy2/src/fuzzy2.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9595df61f2e46432c53705fee9360683039bac5 --- /dev/null +++ b/crates/fuzzy2/src/fuzzy2.rs @@ -0,0 +1,10 @@ +mod char_bag; +mod matcher; +mod paths; +mod strings; + +pub use char_bag::CharBag; +pub use paths::{ + match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet, +}; +pub use strings::{match_strings, StringMatch, StringMatchCandidate}; diff --git a/crates/fuzzy2/src/matcher.rs b/crates/fuzzy2/src/matcher.rs new file mode 100644 index 0000000000000000000000000000000000000000..e808a4886f91152894dbaf4686fa51a786926d29 --- /dev/null +++ b/crates/fuzzy2/src/matcher.rs @@ -0,0 +1,464 @@ +use std::{ + borrow::Cow, + sync::atomic::{self, AtomicBool}, +}; + +use crate::CharBag; + +const BASE_DISTANCE_PENALTY: f64 = 0.6; +const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; +const MIN_DISTANCE_PENALTY: f64 = 0.2; + +pub struct Matcher<'a> { + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + min_score: f64, + match_positions: Vec, + last_positions: Vec, + score_matrix: Vec>, + best_position_matrix: Vec, +} + +pub trait Match: Ord { + fn score(&self) -> f64; + fn set_positions(&mut self, positions: Vec); +} + +pub trait MatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool; + fn to_string(&self) -> Cow<'_, str>; +} + +impl<'a> Matcher<'a> { + pub fn new( + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + ) -> Self { + Self { + query, + lowercase_query, + query_char_bag, + min_score: 0.0, + last_positions: vec![0; query.len()], + match_positions: vec![0; query.len()], + score_matrix: Vec::new(), + best_position_matrix: Vec::new(), + smart_case, + max_results, + } + } + + pub fn match_candidates( + &mut self, + prefix: &[char], + lowercase_prefix: &[char], + candidates: impl Iterator, + results: &mut Vec, + cancel_flag: &AtomicBool, + build_match: F, + ) where + R: Match, + F: Fn(&C, f64) -> R, + { + let mut candidate_chars = Vec::new(); + let mut lowercase_candidate_chars = Vec::new(); + + for candidate in candidates { + if !candidate.has_chars(self.query_char_bag) { + continue; + } + + if cancel_flag.load(atomic::Ordering::Relaxed) { + break; + } + + candidate_chars.clear(); + lowercase_candidate_chars.clear(); + for c in candidate.to_string().chars() { + candidate_chars.push(c); + lowercase_candidate_chars.push(c.to_ascii_lowercase()); + } + + if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) { + continue; + } + + let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len()); + self.score_matrix.clear(); + self.score_matrix.resize(matrix_len, None); + self.best_position_matrix.clear(); + self.best_position_matrix.resize(matrix_len, 0); + + let score = self.score_match( + &candidate_chars, + &lowercase_candidate_chars, + prefix, + lowercase_prefix, + ); + + if score > 0.0 { + let mut mat = build_match(&candidate, score); + if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) { + if results.len() < self.max_results { + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } else if i < results.len() { + results.pop(); + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } + if results.len() == self.max_results { + self.min_score = results.last().unwrap().score(); + } + } + } + } + } + + fn find_last_positions( + &mut self, + lowercase_prefix: &[char], + lowercase_candidate: &[char], + ) -> bool { + let mut lowercase_prefix = lowercase_prefix.iter(); + let mut lowercase_candidate = lowercase_candidate.iter(); + for (i, char) in self.lowercase_query.iter().enumerate().rev() { + if let Some(j) = lowercase_candidate.rposition(|c| c == char) { + self.last_positions[i] = j + lowercase_prefix.len(); + } else if let Some(j) = lowercase_prefix.rposition(|c| c == char) { + self.last_positions[i] = j; + } else { + return false; + } + } + true + } + + fn score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + ) -> f64 { + let score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + 0, + 0, + self.query.len() as f64, + ) * self.query.len() as f64; + + if score <= 0.0 { + return 0.0; + } + + let path_len = prefix.len() + path.len(); + let mut cur_start = 0; + let mut byte_ix = 0; + let mut char_ix = 0; + for i in 0..self.query.len() { + let match_char_ix = self.best_position_matrix[i * path_len + cur_start]; + while char_ix < match_char_ix { + let ch = prefix + .get(char_ix) + .or_else(|| path.get(char_ix - prefix.len())) + .unwrap(); + byte_ix += ch.len_utf8(); + char_ix += 1; + } + cur_start = match_char_ix + 1; + self.match_positions[i] = byte_ix; + } + + score + } + + #[allow(clippy::too_many_arguments)] + fn recursive_score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + query_idx: usize, + path_idx: usize, + cur_score: f64, + ) -> f64 { + if query_idx == self.query.len() { + return 1.0; + } + + let path_len = prefix.len() + path.len(); + + if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] { + return memoized; + } + + let mut score = 0.0; + let mut best_position = 0; + + let query_char = self.lowercase_query[query_idx]; + let limit = self.last_positions[query_idx]; + + let mut last_slash = 0; + for j in path_idx..=limit { + let path_char = if j < prefix.len() { + lowercase_prefix[j] + } else { + path_cased[j - prefix.len()] + }; + let is_path_sep = path_char == '/' || path_char == '\\'; + + if query_idx == 0 && is_path_sep { + last_slash = j; + } + + if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + let curr = if j < prefix.len() { + prefix[j] + } else { + path[j - prefix.len()] + }; + + let mut char_score = 1.0; + if j > path_idx { + let last = if j - 1 < prefix.len() { + prefix[j - 1] + } else { + path[j - 1 - prefix.len()] + }; + + if last == '/' { + char_score = 0.9; + } else if (last == '-' || last == '_' || last == ' ' || last.is_numeric()) + || (last.is_lowercase() && curr.is_uppercase()) + { + char_score = 0.8; + } else if last == '.' { + char_score = 0.7; + } else if query_idx == 0 { + char_score = BASE_DISTANCE_PENALTY; + } else { + char_score = MIN_DISTANCE_PENALTY.max( + BASE_DISTANCE_PENALTY + - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, + ); + } + } + + // Apply a severe penalty if the case doesn't match. + // This will make the exact matches have higher score than the case-insensitive and the + // path insensitive matches. + if (self.smart_case || curr == '/') && self.query[query_idx] != curr { + char_score *= 0.001; + } + + let mut multiplier = char_score; + + // Scale the score based on how deep within the path we found the match. + if query_idx == 0 { + multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; + } + + let mut next_score = 1.0; + if self.min_score > 0.0 { + next_score = cur_score * multiplier; + // Scores only decrease. If we can't pass the previous best, bail + if next_score < self.min_score { + // Ensure that score is non-zero so we use it in the memo table. + if score == 0.0 { + score = 1e-18; + } + continue; + } + } + + let new_score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + query_idx + 1, + j + 1, + next_score, + ) * multiplier; + + if new_score > score { + score = new_score; + best_position = j; + // Optimization: can't score better than 1. + if new_score == 1.0 { + break; + } + } + } + } + + if best_position != 0 { + self.best_position_matrix[query_idx * path_len + path_idx] = best_position; + } + + self.score_matrix[query_idx * path_len + path_idx] = Some(score); + score + } +} + +#[cfg(test)] +mod tests { + use crate::{PathMatch, PathMatchCandidate}; + + use super::*; + use std::{ + path::{Path, PathBuf}, + sync::Arc, + }; + + #[test] + fn test_get_last_positions() { + let mut query: &[char] = &['d', 'c']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); + assert!(!result); + + query = &['c', 'd']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); + assert!(result); + assert_eq!(matcher.last_positions, vec![2, 4]); + + query = &['z', '/', 'z', 'f']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']); + assert!(result); + assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]); + } + + #[test] + fn test_match_path_entries() { + let paths = vec![ + "", + "a", + "ab", + "abC", + "abcd", + "alphabravocharlie", + "AlphaBravoCharlie", + "thisisatestdir", + "/////ThisIsATestDir", + "/this/is/a/test/dir", + "/test/tiatd", + ]; + + assert_eq!( + match_single_path_query("abc", false, &paths), + vec![ + ("abC", vec![0, 1, 2]), + ("abcd", vec![0, 1, 2]), + ("AlphaBravoCharlie", vec![0, 5, 10]), + ("alphabravocharlie", vec![4, 5, 10]), + ] + ); + assert_eq!( + match_single_path_query("t/i/a/t/d", false, &paths), + vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),] + ); + + assert_eq!( + match_single_path_query("tiatd", false, &paths), + vec![ + ("/test/tiatd", vec![6, 7, 8, 9, 10]), + ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]), + ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]), + ("thisisatestdir", vec![0, 2, 6, 7, 11]), + ] + ); + } + + #[test] + fn test_match_multibyte_path_entries() { + let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"]; + assert_eq!("1️⃣".len(), 7); + assert_eq!( + match_single_path_query("bcd", false, &paths), + vec![ + ("αβγδ/bcde", vec![9, 10, 11]), + ("aαbβ/cγdδ", vec![3, 7, 10]), + ] + ); + assert_eq!( + match_single_path_query("cde", false, &paths), + vec![ + ("αβγδ/bcde", vec![10, 11, 12]), + ("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]), + ] + ); + } + + fn match_single_path_query<'a>( + query: &str, + smart_case: bool, + paths: &[&'a str], + ) -> Vec<(&'a str, Vec)> { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let query_chars = CharBag::from(&lowercase_query[..]); + + let path_arcs: Vec> = paths + .iter() + .map(|path| Arc::from(PathBuf::from(path))) + .collect::>(); + let mut path_entries = Vec::new(); + for (i, path) in paths.iter().enumerate() { + let lowercase_path = path.to_lowercase().chars().collect::>(); + let char_bag = CharBag::from(lowercase_path.as_slice()); + path_entries.push(PathMatchCandidate { + char_bag, + path: &path_arcs[i], + }); + } + + let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100); + + let cancel_flag = AtomicBool::new(false); + let mut results = Vec::new(); + + matcher.match_candidates( + &[], + &[], + path_entries.into_iter(), + &mut results, + &cancel_flag, + |candidate, score| PathMatch { + score, + worktree_id: 0, + positions: Vec::new(), + path: Arc::from(candidate.path), + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }, + ); + + results + .into_iter() + .map(|result| { + ( + paths + .iter() + .copied() + .find(|p| result.path.as_ref() == Path::new(p)) + .unwrap(), + result.positions, + ) + }) + .collect() + } +} diff --git a/crates/fuzzy2/src/paths.rs b/crates/fuzzy2/src/paths.rs new file mode 100644 index 0000000000000000000000000000000000000000..f6c5fba6c9f641d4b1b132bf2ae932fa5d69bb02 --- /dev/null +++ b/crates/fuzzy2/src/paths.rs @@ -0,0 +1,257 @@ +use gpui2::Executor; +use std::{ + borrow::Cow, + cmp::{self, Ordering}, + path::Path, + sync::{atomic::AtomicBool, Arc}, +}; + +use crate::{ + matcher::{Match, MatchCandidate, Matcher}, + CharBag, +}; + +#[derive(Clone, Debug)] +pub struct PathMatchCandidate<'a> { + pub path: &'a Path, + pub char_bag: CharBag, +} + +#[derive(Clone, Debug)] +pub struct PathMatch { + pub score: f64, + pub positions: Vec, + pub worktree_id: usize, + pub path: Arc, + pub path_prefix: Arc, + /// Number of steps removed from a shared parent with the relative path + /// Used to order closer paths first in the search list + pub distance_to_relative_ancestor: usize, +} + +pub trait PathMatchCandidateSet<'a>: Send + Sync { + type Candidates: Iterator>; + fn id(&self) -> usize; + fn len(&self) -> usize; + fn is_empty(&self) -> bool { + self.len() == 0 + } + fn prefix(&self) -> Arc; + fn candidates(&'a self, start: usize) -> Self::Candidates; +} + +impl Match for PathMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl<'a> MatchCandidate for PathMatchCandidate<'a> { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.path.to_string_lossy() + } +} + +impl PartialEq for PathMatch { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for PathMatch {} + +impl PartialOrd for PathMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PathMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .partial_cmp(&other.score) + .unwrap_or(Ordering::Equal) + .then_with(|| self.worktree_id.cmp(&other.worktree_id)) + .then_with(|| { + other + .distance_to_relative_ancestor + .cmp(&self.distance_to_relative_ancestor) + }) + .then_with(|| self.path.cmp(&other.path)) + } +} + +pub fn match_fixed_path_set( + candidates: Vec, + worktree_id: usize, + query: &str, + smart_case: bool, + max_results: usize, +) -> Vec { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let mut matcher = Matcher::new( + &query, + &lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + + let mut results = Vec::new(); + matcher.match_candidates( + &[], + &[], + candidates.into_iter(), + &mut results, + &AtomicBool::new(false), + |candidate, score| PathMatch { + score, + worktree_id, + positions: Vec::new(), + path: Arc::from(candidate.path), + path_prefix: Arc::from(""), + distance_to_relative_ancestor: usize::MAX, + }, + ); + results +} + +pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( + candidate_sets: &'a [Set], + query: &str, + relative_to: Option>, + smart_case: bool, + max_results: usize, + cancel_flag: &AtomicBool, + executor: Executor, +) -> Vec { + let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum(); + if path_count == 0 { + return Vec::new(); + } + + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + + let lowercase_query = &lowercase_query; + let query = &query; + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let num_cpus = executor.num_cpus().min(path_count); + let segment_size = (path_count + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) + .map(|_| Vec::with_capacity(max_results)) + .collect::>(); + + executor + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let relative_to = relative_to.clone(); + scope.spawn(async move { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + + let mut tree_start = 0; + for candidate_set in candidate_sets { + let tree_end = tree_start + candidate_set.len(); + + if tree_start < segment_end && segment_start < tree_end { + let start = cmp::max(tree_start, segment_start) - tree_start; + let end = cmp::min(tree_end, segment_end) - tree_start; + let candidates = candidate_set.candidates(start).take(end - start); + + let worktree_id = candidate_set.id(); + let prefix = candidate_set.prefix().chars().collect::>(); + let lowercase_prefix = prefix + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect::>(); + matcher.match_candidates( + &prefix, + &lowercase_prefix, + candidates, + results, + cancel_flag, + |candidate, score| PathMatch { + score, + worktree_id, + positions: Vec::new(), + path: Arc::from(candidate.path), + path_prefix: candidate_set.prefix(), + distance_to_relative_ancestor: relative_to.as_ref().map_or( + usize::MAX, + |relative_to| { + distance_between_paths( + candidate.path.as_ref(), + relative_to.as_ref(), + ) + }, + ), + }, + ); + } + if tree_end >= segment_end { + break; + } + tree_start = tree_end; + } + }) + } + }) + .await; + + let mut results = Vec::new(); + for segment_result in segment_results { + if results.is_empty() { + results = segment_result; + } else { + util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a)); + } + } + results +} + +/// Compute the distance from a given path to some other path +/// If there is no shared path, returns usize::MAX +fn distance_between_paths(path: &Path, relative_to: &Path) -> usize { + let mut path_components = path.components(); + let mut relative_components = relative_to.components(); + + while path_components + .next() + .zip(relative_components.next()) + .map(|(path_component, relative_component)| path_component == relative_component) + .unwrap_or_default() + {} + path_components.count() + relative_components.count() + 1 +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::distance_between_paths; + + #[test] + fn test_distance_between_paths_empty() { + distance_between_paths(Path::new(""), Path::new("")); + } +} diff --git a/crates/fuzzy2/src/strings.rs b/crates/fuzzy2/src/strings.rs new file mode 100644 index 0000000000000000000000000000000000000000..6f7533ddd0a7b4d0a9e1c4cb190274d233ebdb50 --- /dev/null +++ b/crates/fuzzy2/src/strings.rs @@ -0,0 +1,159 @@ +use crate::{ + matcher::{Match, MatchCandidate, Matcher}, + CharBag, +}; +use gpui2::Executor; +use std::{ + borrow::Cow, + cmp::{self, Ordering}, + sync::atomic::AtomicBool, +}; + +#[derive(Clone, Debug)] +pub struct StringMatchCandidate { + pub id: usize, + pub string: String, + pub char_bag: CharBag, +} + +impl Match for StringMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl StringMatchCandidate { + pub fn new(id: usize, string: String) -> Self { + Self { + id, + char_bag: CharBag::from(string.as_str()), + string, + } + } +} + +impl<'a> MatchCandidate for &'a StringMatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.string.as_str().into() + } +} + +#[derive(Clone, Debug)] +pub struct StringMatch { + pub candidate_id: usize, + pub score: f64, + pub positions: Vec, + pub string: String, +} + +impl PartialEq for StringMatch { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for StringMatch {} + +impl PartialOrd for StringMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for StringMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .partial_cmp(&other.score) + .unwrap_or(Ordering::Equal) + .then_with(|| self.candidate_id.cmp(&other.candidate_id)) + } +} + +pub async fn match_strings( + candidates: &[StringMatchCandidate], + query: &str, + smart_case: bool, + max_results: usize, + cancel_flag: &AtomicBool, + executor: Executor, +) -> Vec { + if candidates.is_empty() || max_results == 0 { + return Default::default(); + } + + if query.is_empty() { + return candidates + .iter() + .map(|candidate| StringMatch { + candidate_id: candidate.id, + score: 0., + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect(); + } + + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + + let lowercase_query = &lowercase_query; + let query = &query; + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let num_cpus = executor.num_cpus().min(candidates.len()); + let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) + .map(|_| Vec::with_capacity(max_results.min(candidates.len()))) + .collect::>(); + + executor + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let cancel_flag = &cancel_flag; + scope.spawn(async move { + let segment_start = cmp::min(segment_idx * segment_size, candidates.len()); + let segment_end = cmp::min(segment_start + segment_size, candidates.len()); + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + + matcher.match_candidates( + &[], + &[], + candidates[segment_start..segment_end].iter(), + results, + cancel_flag, + |candidate, score| StringMatch { + candidate_id: candidate.id, + score, + positions: Vec::new(), + string: candidate.string.to_string(), + }, + ); + }); + } + }) + .await; + + let mut results = Vec::new(); + for segment_result in segment_results { + if results.is_empty() { + results = segment_result; + } else { + util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a)); + } + } + results +} diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 89fdd2315f5374bf6228422c5b0dc92b7593a2f2..d241b773f801558c73fa4107c97b6b4c2b9eccb8 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -202,6 +202,20 @@ impl Executor { Task::Spawned(task) } + #[cfg(any(test, feature = "test-support"))] + pub fn start_waiting(&self) { + todo!("start_waiting") + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn simulate_random_delay(&self) { + todo!("simulate_random_delay") + } + + pub fn num_cpus(&self) -> usize { + num_cpus::get() + } + pub fn is_main_thread(&self) -> bool { self.dispatcher.is_main_thread() } diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index 6c481e943a15901fcd77c2897e074f3343a8eafc..77b195293bd975f51fa4d7d3834d299aeae55c37 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -13,7 +13,7 @@ test-support = [ "rand", "client2/test-support", "collections/test-support", - "lsp/test-support", + "lsp2/test-support", "text/test-support", "tree-sitter-rust", "tree-sitter-typescript", @@ -24,16 +24,16 @@ test-support = [ [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -fuzzy = { path = "../fuzzy" } +fuzzy2 = { path = "../fuzzy2" } fs = { path = "../fs" } git = { path = "../git" } gpui2 = { path = "../gpui2" } -lsp = { path = "../lsp" } +lsp2 = { path = "../lsp2" } rpc = { path = "../rpc" } settings2 = { path = "../settings2" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } -# theme = { path = "../theme" } +theme2 = { path = "../theme2" } util = { path = "../util" } anyhow.workspace = true @@ -64,7 +64,7 @@ tree-sitter-typescript = { workspace = true, optional = true } client2 = { path = "../client2", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } gpui2 = { path = "../gpui2", features = ["test-support"] } -lsp = { path = "../lsp", features = ["test-support"] } +lsp2 = { path = "../lsp2", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 7d0c0d392e8b967d49d9e8759f5e73dad1b50d1a..d2f13ba17be7d9cfe2d12d748209e047946bb36a 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -17,7 +17,7 @@ use anyhow::{anyhow, Result}; pub use clock::ReplicaId; use futures::FutureExt as _; use gpui2::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task}; -use lsp::LanguageServerId; +use lsp2::LanguageServerId; use parking_lot::Mutex; use similar::{ChangeTag, TextDiff}; use smallvec::SmallVec; @@ -40,6 +40,7 @@ use std::{ use sum_tree::TreeMap; use text::operation_queue::OperationQueue; pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *}; +use theme2::SyntaxTheme; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; use util::{RangeExt, TryFutureExt as _}; @@ -47,7 +48,7 @@ use util::{RangeExt, TryFutureExt as _}; #[cfg(any(test, feature = "test-support"))] pub use {tree_sitter_rust, tree_sitter_typescript}; -pub use lsp::DiagnosticSeverity; +pub use lsp2::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, @@ -148,14 +149,14 @@ pub struct Completion { pub new_text: String, pub label: CodeLabel, pub server_id: LanguageServerId, - pub lsp_completion: lsp::CompletionItem, + pub lsp_completion: lsp2::CompletionItem, } #[derive(Clone, Debug)] pub struct CodeAction { pub server_id: LanguageServerId, pub range: Range, - pub lsp_action: lsp::CodeAction, + pub lsp_action: lsp2::CodeAction, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -3000,14 +3001,14 @@ impl IndentSize { impl Completion { pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { - Some(lsp::CompletionItemKind::VARIABLE) => 0, + Some(lsp2::CompletionItemKind::VARIABLE) => 0, _ => 1, }; (kind_key, &self.label.text[self.label.filter_range.clone()]) } pub fn is_snippet(&self) -> bool { - self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) + self.lsp_completion.insert_text_format == Some(lsp2::InsertTextFormat::SNIPPET) } } diff --git a/crates/language2/src/buffer_tests.rs b/crates/language2/src/buffer_tests.rs index b7e9001c6ab5233fba3c80ca527f2068fc1e02df..054b3fc782217de4af69f3ff724f82c37f44c80f 100644 --- a/crates/language2/src/buffer_tests.rs +++ b/crates/language2/src/buffer_tests.rs @@ -1,2442 +1,2442 @@ -use crate::language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, -}; - -use super::*; -use clock::ReplicaId; -use collections::BTreeMap; -use gpui::{AppContext, ModelHandle}; -use indoc::indoc; -use proto::deserialize_operation; -use rand::prelude::*; -use regex::RegexBuilder; -use settings::SettingsStore; -use std::{ - cell::RefCell, - env, - ops::Range, - rc::Rc, - time::{Duration, Instant}, -}; -use text::network::Network; -use text::LineEnding; -use unindent::Unindent as _; -use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; - -lazy_static! { - static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$") - .multi_line(true) - .build() - .unwrap(); -} - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} - -#[gpui::test] -fn test_line_endings(cx: &mut gpui::AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree") - .with_language(Arc::new(rust_lang()), cx); - assert_eq!(buffer.text(), "one\ntwo\nthree"); - assert_eq!(buffer.line_ending(), LineEnding::Windows); - - buffer.check_invariants(); - buffer.edit( - [(buffer.len()..buffer.len(), "\r\nfour")], - Some(AutoindentMode::EachLine), - 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(); - - buffer - }); -} - -#[gpui::test] -fn test_select_language() { - let registry = Arc::new(LanguageRegistry::test()); - registry.add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); - registry.add(Arc::new(Language::new( - LanguageConfig { - name: "Make".into(), - path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); - - // matching file extension - assert_eq!( - registry - .language_for_file("zed/lib.rs", None) - .now_or_never() - .and_then(|l| Some(l.ok()?.name())), - Some("Rust".into()) - ); - assert_eq!( - registry - .language_for_file("zed/lib.mk", None) - .now_or_never() - .and_then(|l| Some(l.ok()?.name())), - Some("Make".into()) - ); - - // matching filename - assert_eq!( - registry - .language_for_file("zed/Makefile", None) - .now_or_never() - .and_then(|l| Some(l.ok()?.name())), - Some("Make".into()) - ); - - // matching suffix that is not the full file extension or filename - assert_eq!( - registry - .language_for_file("zed/cars", None) - .now_or_never() - .and_then(|l| Some(l.ok()?.name())), - None - ); - assert_eq!( - registry - .language_for_file("zed/a.cars", None) - .now_or_never() - .and_then(|l| Some(l.ok()?.name())), - None - ); - assert_eq!( - registry - .language_for_file("zed/sumk", None) - .now_or_never() - .and_then(|l| Some(l.ok()?.name())), - None - ); -} - -#[gpui::test] -fn test_edit_events(cx: &mut gpui::AppContext) { - let mut now = Instant::now(); - let buffer_1_events = Rc::new(RefCell::new(Vec::new())); - let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - - let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef")); - let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef")); - let buffer1_ops = Rc::new(RefCell::new(Vec::new())); - buffer1.update(cx, { - let buffer1_ops = buffer1_ops.clone(); - |buffer, cx| { - let buffer_1_events = buffer_1_events.clone(); - cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { - Event::Operation(op) => buffer1_ops.borrow_mut().push(op), - event => buffer_1_events.borrow_mut().push(event), - }) - .detach(); - let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, _, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }) - .detach(); - - // 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")], None, cx); - - // An empty transaction does not emit any events. - buffer.start_transaction(); - buffer.end_transaction(cx); - - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(now); - 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. - buffer.undo(cx); - } - }); - - // Incorporating a set of remote ops emits a single edited event, - // followed by a dirty changed event. - buffer2.update(cx, |buffer, cx| { - buffer - .apply_ops(buffer1_ops.borrow_mut().drain(..), cx) - .unwrap(); - }); - assert_eq!( - mem::take(&mut *buffer_1_events.borrow_mut()), - vec![ - Event::Edited, - Event::DirtyChanged, - Event::Edited, - Event::Edited, - ] - ); - assert_eq!( - mem::take(&mut *buffer_2_events.borrow_mut()), - vec![Event::Edited, Event::DirtyChanged] - ); - - buffer1.update(cx, |buffer, cx| { - // Undoing the first transaction emits edited event, followed by a - // dirty changed event, since the buffer is again in a clean state. - buffer.undo(cx); - }); - // Incorporating the remote ops again emits a single edited event, - // followed by a dirty changed event. - buffer2.update(cx, |buffer, cx| { - buffer - .apply_ops(buffer1_ops.borrow_mut().drain(..), cx) - .unwrap(); - }); - assert_eq!( - mem::take(&mut *buffer_1_events.borrow_mut()), - vec![Event::Edited, Event::DirtyChanged,] - ); - assert_eq!( - mem::take(&mut *buffer_2_events.borrow_mut()), - vec![Event::Edited, Event::DirtyChanged] - ); -} - -#[gpui::test] -async fn test_apply_diff(cx: &mut gpui::TestAppContext) { - let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); - let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); - - let text = "a\nccc\ndddd\nffffff\n"; - let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(cx, |buffer, cx| { - buffer.apply_diff(diff, cx).unwrap(); - assert_eq!(buffer.text(), text); - assert_eq!(anchor.to_point(buffer), Point::new(2, 3)); - }); - - let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; - let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(cx, |buffer, cx| { - buffer.apply_diff(diff, cx).unwrap(); - assert_eq!(buffer.text(), text); - assert_eq!(anchor.to_point(buffer), Point::new(4, 4)); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { - let text = [ - "zero", // - "one ", // 2 trailing spaces - "two", // - "three ", // 3 trailing spaces - "four", // - "five ", // 4 trailing spaces - ] - .join("\n"); - - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); - - // Spawn a task to format the buffer's whitespace. - // Pause so that the foratting task starts running. - let format = buffer.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx)); - smol::future::yield_now().await; - - // Edit the buffer while the normalization task is running. - let version_before_edit = buffer.read_with(cx, |buffer, _| buffer.version()); - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(0, 1)..Point::new(0, 1), "EE"), - (Point::new(3, 5)..Point::new(3, 5), "EEE"), - ], - None, - cx, - ); - }); - - let format_diff = format.await; - buffer.update(cx, |buffer, cx| { - let version_before_format = format_diff.base_version.clone(); - buffer.apply_diff(format_diff, cx); - - // The outcome depends on the order of concurrent taks. - // - // If the edit occurred while searching for trailing whitespace ranges, - // then the trailing whitespace region touched by the edit is left intact. - if version_before_format == version_before_edit { - assert_eq!( - buffer.text(), - [ - "zEEero", // - "one", // - "two", // - "threeEEE ", // - "four", // - "five", // - ] - .join("\n") - ); - } - // Otherwise, all trailing whitespace is removed. - else { - assert_eq!( - buffer.text(), - [ - "zEEero", // - "one", // - "two", // - "threeEEE", // - "four", // - "five", // - ] - .join("\n") - ); - } - }); -} - -#[gpui::test] -async fn test_reparse(cx: &mut gpui::TestAppContext) { - let text = "fn a() {}"; - let buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) - }); - - // Wait for the initial text to parse - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!( - get_tree_sexp(&buffer, cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters) ", - "body: (block)))" - ) - ); - - buffer.update(cx, |buffer, _| { - buffer.set_sync_parse_timeout(Duration::ZERO) - }); - - // Perform some edits (add parameter and variable reference) - // Parsing doesn't begin until the transaction is complete - buffer.update(cx, |buf, cx| { - buf.start_transaction(); - - let offset = buf.text().find(')').unwrap(); - buf.edit([(offset..offset, "b: C")], None, cx); - assert!(!buf.is_parsing()); - - let offset = buf.text().find('}').unwrap(); - buf.edit([(offset..offset, " d; ")], None, cx); - assert!(!buf.is_parsing()); - - buf.end_transaction(cx); - assert_eq!(buf.text(), "fn a(b: C) { d; }"); - assert!(buf.is_parsing()); - }); - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!( - get_tree_sexp(&buffer, cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (expression_statement (identifier)))))" - ) - ); - - // Perform a series of edits without waiting for the current parse to complete: - // * turn identifier into a field expression - // * turn field expression into a method call - // * add a turbofish to the method call - buffer.update(cx, |buf, cx| { - let offset = buf.text().find(';').unwrap(); - 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)")], 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, "::")], None, cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); - assert!(buf.is_parsing()); - }); - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!( - get_tree_sexp(&buffer, cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (expression_statement (call_expression ", - "function: (generic_function ", - "function: (field_expression value: (identifier) field: (field_identifier)) ", - "type_arguments: (type_arguments (type_identifier))) ", - "arguments: (arguments (identifier)))))))", - ) - ); - - buffer.update(cx, |buf, cx| { - buf.undo(cx); - buf.undo(cx); - buf.undo(cx); - buf.undo(cx); - assert_eq!(buf.text(), "fn a() {}"); - assert!(buf.is_parsing()); - }); - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!( - get_tree_sexp(&buffer, cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters) ", - "body: (block)))" - ) - ); - - buffer.update(cx, |buf, cx| { - buf.redo(cx); - buf.redo(cx); - buf.redo(cx); - buf.redo(cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); - assert!(buf.is_parsing()); - }); - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!( - get_tree_sexp(&buffer, cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (expression_statement (call_expression ", - "function: (generic_function ", - "function: (field_expression value: (identifier) field: (field_identifier)) ", - "type_arguments: (type_arguments (type_identifier))) ", - "arguments: (arguments (identifier)))))))", - ) - ); -} - -#[gpui::test] -async fn test_resetting_language(cx: &mut gpui::TestAppContext) { - let buffer = cx.add_model(|cx| { - let mut buffer = - Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx); - buffer.set_sync_parse_timeout(Duration::ZERO); - buffer - }); - - // Wait for the initial text to parse - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!( - get_tree_sexp(&buffer, cx), - "(source_file (expression_statement (block)))" - ); - - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(Arc::new(json_lang())), cx) - }); - buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; - assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))"); -} - -#[gpui::test] -async fn test_outline(cx: &mut gpui::TestAppContext) { - let text = r#" - struct Person { - name: String, - age: usize, - } - - mod module { - enum LoginState { - LoggedOut, - LoggingOn, - LoggedIn { - person: Person, - time: Instant, - } - } - } - - impl Eq for Person {} - - impl Drop for Person { - fn drop(&mut self) { - println!("bye"); - } - } - "# - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) - }); - let outline = buffer - .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) - .unwrap(); - - assert_eq!( - outline - .items - .iter() - .map(|item| (item.text.as_str(), item.depth)) - .collect::>(), - &[ - ("struct Person", 0), - ("name", 1), - ("age", 1), - ("mod module", 0), - ("enum LoginState", 1), - ("LoggedOut", 2), - ("LoggingOn", 2), - ("LoggedIn", 2), - ("person", 3), - ("time", 3), - ("impl Eq for Person", 0), - ("impl Drop for Person", 0), - ("fn drop", 1), - ] - ); - - // Without space, we only match on names - assert_eq!( - search(&outline, "oon", cx).await, - &[ - ("mod module", vec![]), // included as the parent of a match - ("enum LoginState", vec![]), // included as the parent of a match - ("LoggingOn", vec![1, 7, 8]), // matches - ("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names - ] - ); - - assert_eq!( - search(&outline, "dp p", cx).await, - &[ - ("impl Drop for Person", vec![5, 8, 9, 14]), - ("fn drop", vec![]), - ] - ); - assert_eq!( - search(&outline, "dpn", cx).await, - &[("impl Drop for Person", vec![5, 14, 19])] - ); - assert_eq!( - search(&outline, "impl ", cx).await, - &[ - ("impl Eq for Person", vec![0, 1, 2, 3, 4]), - ("impl Drop for Person", vec![0, 1, 2, 3, 4]), - ("fn drop", vec![]), - ] - ); - - async fn search<'a>( - outline: &'a Outline, - query: &'a str, - cx: &'a gpui::TestAppContext, - ) -> Vec<(&'a str, Vec)> { - let matches = cx - .read(|cx| outline.search(query, cx.background().clone())) - .await; - matches - .into_iter() - .map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions)) - .collect::>() - } -} - -#[gpui::test] -async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { - let text = r#" - impl A for B< - C - > { - }; - "# - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) - }); - let outline = buffer - .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) - .unwrap(); - - assert_eq!( - outline - .items - .iter() - .map(|item| (item.text.as_str(), item.depth)) - .collect::>(), - &[("impl A for B<", 0)] - ); -} - -#[gpui::test] -async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { - let language = javascript_lang() - .with_outline_query( - r#" - (function_declaration - "function" @context - name: (_) @name - parameters: (formal_parameters - "(" @context.extra - ")" @context.extra)) @item - "#, - ) - .unwrap(); - - let text = r#" - function a() {} - function b(c) {} - "# - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx) - }); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - // extra context nodes are included in the outline. - let outline = snapshot.outline(None).unwrap(); - assert_eq!( - outline - .items - .iter() - .map(|item| (item.text.as_str(), item.depth)) - .collect::>(), - &[("function a()", 0), ("function b( )", 0),] - ); - - // extra context nodes do not appear in breadcrumbs. - let symbols = snapshot.symbols_containing(3, None).unwrap(); - assert_eq!( - symbols - .iter() - .map(|item| (item.text.as_str(), item.depth)) - .collect::>(), - &[("function a", 0)] - ); -} - -#[gpui::test] -async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { - let text = r#" - impl Person { - fn one() { - 1 - } - - fn two() { - 2 - }fn three() { - 3 - } - } - "# - .unindent(); - - let buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) - }); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - // point is at the start of an item - assert_eq!( - symbols_containing(Point::new(1, 4), &snapshot), - vec![ - ( - "impl Person".to_string(), - Point::new(0, 0)..Point::new(10, 1) - ), - ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5)) - ] - ); - - // point is in the middle of an item - assert_eq!( - symbols_containing(Point::new(2, 8), &snapshot), - vec![ - ( - "impl Person".to_string(), - Point::new(0, 0)..Point::new(10, 1) - ), - ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5)) - ] - ); - - // point is at the end of an item - assert_eq!( - symbols_containing(Point::new(3, 5), &snapshot), - vec![ - ( - "impl Person".to_string(), - Point::new(0, 0)..Point::new(10, 1) - ), - ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5)) - ] - ); - - // point is in between two adjacent items - assert_eq!( - symbols_containing(Point::new(7, 5), &snapshot), - vec![ - ( - "impl Person".to_string(), - Point::new(0, 0)..Point::new(10, 1) - ), - ("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5)) - ] - ); - - fn symbols_containing( - position: Point, - snapshot: &BufferSnapshot, - ) -> Vec<(String, Range)> { - snapshot - .symbols_containing(position, None) - .unwrap() - .into_iter() - .map(|item| { - ( - item.text, - item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot), - ) - }) - .collect() - } -} - -#[gpui::test] -fn test_enclosing_bracket_ranges(cx: &mut AppContext) { - let mut assert = |selection_text, range_markers| { - assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx) - }; - - assert( - indoc! {" - mod x { - moˇd y { - - } - } - let foo = 1;"}, - vec![indoc! {" - mod x «{» - mod y { - - } - «}» - let foo = 1;"}], - ); - - assert( - indoc! {" - mod x { - mod y ˇ{ - - } - } - let foo = 1;"}, - vec![ - indoc! {" - mod x «{» - mod y { - - } - «}» - let foo = 1;"}, - indoc! {" - mod x { - mod y «{» - - «}» - } - let foo = 1;"}, - ], - ); - - assert( - indoc! {" - mod x { - mod y { - - }ˇ - } - let foo = 1;"}, - vec![ - indoc! {" - mod x «{» - mod y { - - } - «}» - let foo = 1;"}, - indoc! {" - mod x { - mod y «{» - - «}» - } - let foo = 1;"}, - ], - ); - - assert( - indoc! {" - mod x { - mod y { - - } - ˇ} - let foo = 1;"}, - vec![indoc! {" - mod x «{» - mod y { - - } - «}» - let foo = 1;"}], - ); - - assert( - indoc! {" - mod x { - mod y { - - } - } - let fˇoo = 1;"}, - vec![], - ); - - // Regression test: avoid crash when querying at the end of the buffer. - assert( - indoc! {" - mod x { - mod y { - - } - } - let foo = 1;ˇ"}, - vec![], - ); -} - -#[gpui::test] -fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &mut AppContext) { - let mut assert = |selection_text, bracket_pair_texts| { - assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx) - }; - - assert( - indoc! {" - for (const a in b)ˇ { - // a comment that's longer than the for-loop header - }"}, - vec![indoc! {" - for «(»const a in b«)» { - // a comment that's longer than the for-loop header - }"}], - ); - - // Regression test: even though the parent node of the parentheses (the for loop) does - // intersect the given range, the parentheses themselves do not contain the range, so - // they should not be returned. Only the curly braces contain the range. - assert( - indoc! {" - for (const a in b) {ˇ - // a comment that's longer than the for-loop header - }"}, - vec![indoc! {" - for (const a in b) «{» - // a comment that's longer than the for-loop header - «}»"}], - ); -} - -#[gpui::test] -fn test_range_for_syntax_ancestor(cx: &mut AppContext) { - cx.add_model(|cx| { - let text = "fn a() { b(|c| {}) }"; - let buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); - let snapshot = buffer.snapshot(); - - assert_eq!( - snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")), - Some(range_of(text, "|")) - ); - assert_eq!( - snapshot.range_for_syntax_ancestor(range_of(text, "|")), - Some(range_of(text, "|c|")) - ); - assert_eq!( - snapshot.range_for_syntax_ancestor(range_of(text, "|c|")), - Some(range_of(text, "|c| {}")) - ); - assert_eq!( - snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")), - Some(range_of(text, "(|c| {})")) - ); - - buffer - }); - - fn empty_range_at(text: &str, part: &str) -> Range { - let start = text.find(part).unwrap(); - start..start - } - - fn range_of(text: &str, part: &str) -> Range { - let start = text.find(part).unwrap(); - start..start + part.len() - } -} - -#[gpui::test] -fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let text = "fn a() {}"; - let mut buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); - - buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); - assert_eq!(buffer.text(), "fn a() {\n \n}"); - - buffer.edit( - [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], - 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( - [(Point::new(2, 4)..Point::new(2, 4), ".c")], - 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( - [(Point::new(2, 8)..Point::new(2, 9), "")], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); - - buffer - }); -} - -#[gpui::test] -fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { - init_settings(cx, |settings| { - settings.defaults.hard_tabs = Some(true); - }); - - cx.add_model(|cx| { - let text = "fn a() {}"; - let mut buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); - - buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); - assert_eq!(buffer.text(), "fn a() {\n\t\n}"); - - buffer.edit( - [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], - 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( - [(Point::new(2, 1)..Point::new(2, 1), ".c")], - 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( - [(Point::new(2, 2)..Point::new(2, 3), "")], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); - - buffer - }); -} - -#[gpui::test] -fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let mut buffer = Buffer::new( - 0, - cx.model_id() as u64, - " - fn a() { - c; - d; - } - " - .unindent(), - ) - .with_language(Arc::new(rust_lang()), cx); - - // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, - // their indentation is not adjusted. - buffer.edit_via_marked_text( - &" - fn a() { - c«()»; - d«()»; - } - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a() { - c(); - d(); - } - " - .unindent() - ); - - // When appending new content after these lines, the indentation is based on the - // preceding lines' actual indentation. - buffer.edit_via_marked_text( - &" - fn a() { - c« - .f - .g()»; - d« - .f - .g()»; - } - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - - assert_eq!( - buffer.text(), - " - fn a() { - c - .f - .g(); - d - .f - .g(); - } - " - .unindent() - ); - buffer - }); - - cx.add_model(|cx| { - let mut buffer = Buffer::new( - 0, - cx.model_id() as u64, - " - fn a() { - b(); - | - " - .replace("|", "") // marker to preserve trailing whitespace - .unindent(), - ) - .with_language(Arc::new(rust_lang()), cx); - - // Insert a closing brace. It is outdented. - buffer.edit_via_marked_text( - &" - fn a() { - b(); - «}» - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a() { - b(); - } - " - .unindent() - ); - - // Manually edit the leading whitespace. The edit is preserved. - buffer.edit_via_marked_text( - &" - fn a() { - b(); - « »} - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a() { - b(); - } - " - .unindent() - ); - buffer - }); -} - -#[gpui::test] -fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let mut buffer = Buffer::new( - 0, - cx.model_id() as u64, - " - fn a() { - i - } - " - .unindent(), - ) - .with_language(Arc::new(rust_lang()), cx); - - // Regression test: line does not get outdented due to syntax error - buffer.edit_via_marked_text( - &" - fn a() { - i«f let Some(x) = y» - } - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a() { - if let Some(x) = y - } - " - .unindent() - ); - - buffer.edit_via_marked_text( - &" - fn a() { - if let Some(x) = y« {» - } - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a() { - if let Some(x) = y { - } - " - .unindent() - ); - - buffer - }); -} - -#[gpui::test] -fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let mut buffer = Buffer::new( - 0, - cx.model_id() as u64, - " - fn a() {} - " - .unindent(), - ) - .with_language(Arc::new(rust_lang()), cx); - - buffer.edit_via_marked_text( - &" - fn a(« - b») {} - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a( - b) {} - " - .unindent() - ); - - // The indentation suggestion changed because `@end` node (a close paren) - // is now at the beginning of the line. - buffer.edit_via_marked_text( - &" - fn a( - ˇ) {} - " - .unindent(), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - fn a( - ) {} - " - .unindent() - ); - - buffer - }); -} - -#[gpui::test] -fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let text = "a\nb"; - let mut buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), 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_multi_line_insertion(cx: &mut AppContext) { - init_settings(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, cx.model_id() as u64, text).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 AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let text = r#" - fn a() { - b(); - } - "# - .unindent(); - let mut buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); - - // When this text was copied, both of the quotation marks were at the same - // indent level, but the indentation of the first line was not included in - // the copied text. This information is retained in the - // 'original_indent_columns' vector. - let original_indent_columns = vec![4]; - 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: original_indent_columns.clone(), - }), - cx, - ); - assert_eq!( - buffer.text(), - r#" - fn a() { - b(); - " - c - d - e - " - } - "# - .unindent() - ); - - // Grouping is disabled in tests, so we need 2 undos - buffer.undo(cx); // Undo the auto-indent - buffer.undo(cx); // Undo the original edit - - // Insert the block at a deeper indent level. The entire block is outdented. - buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); - buffer.edit( - [(Point::new(2, 8)..Point::new(2, 8), inserted_text)], - Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), - }), - cx, - ); - assert_eq!( - buffer.text(), - r#" - fn a() { - b(); - " - c - d - e - " - } - "# - .unindent() - ); - - buffer - }); -} - -#[gpui::test] -fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let text = r#" - fn a() { - if b() { - - } - } - "# - .unindent(); - let mut buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); - - // The original indent columns are not known, so this text is - // auto-indented in a block as if the first line was copied in - // its entirety. - let original_indent_columns = Vec::new(); - let inserted_text = " c\n .d()\n .e();"; - - // 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)], - Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), - }), - cx, - ); - assert_eq!( - buffer.text(), - r#" - fn a() { - if b() { - c - .d() - .e(); - } - } - "# - .unindent() - ); - - // Grouping is disabled in tests, so we need 2 undos - buffer.undo(cx); // Undo the auto-indent - buffer.undo(cx); // Undo the original edit - - // Insert the block at a deeper indent level. The entire block is outdented. - buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))], - None, - cx, - ); - buffer.edit( - [(Point::new(2, 12)..Point::new(2, 12), inserted_text)], - Some(AutoindentMode::Block { - original_indent_columns: Vec::new(), - }), - cx, - ); - assert_eq!( - buffer.text(), - r#" - fn a() { - if b() { - c - .d() - .e(); - } - } - "# - .unindent() - ); - - buffer - }); -} - -#[gpui::test] -fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let text = " - * one - - a - - b - * two - " - .unindent(); - - let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language( - Arc::new(Language::new( - LanguageConfig { - name: "Markdown".into(), - auto_indent_using_last_non_empty_line: false, - ..Default::default() - }, - Some(tree_sitter_json::language()), - )), - cx, - ); - buffer.edit( - [(Point::new(3, 0)..Point::new(3, 0), "\n")], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " - * one - - a - - b - - * two - " - .unindent() - ); - buffer - }); -} - -#[gpui::test] -fn test_autoindent_with_injected_languages(cx: &mut AppContext) { - init_settings(cx, |settings| { - settings.languages.extend([ - ( - "HTML".into(), - LanguageSettingsContent { - tab_size: Some(2.try_into().unwrap()), - ..Default::default() - }, - ), - ( - "JavaScript".into(), - LanguageSettingsContent { - tab_size: Some(8.try_into().unwrap()), - ..Default::default() - }, - ), - ]) - }); - - let html_language = Arc::new(html_lang()); - - let javascript_language = Arc::new(javascript_lang()); - - let language_registry = Arc::new(LanguageRegistry::test()); - language_registry.add(html_language.clone()); - language_registry.add(javascript_language.clone()); - - cx.add_model(|cx| { - let (text, ranges) = marked_text_ranges( - &" -
ˇ -
- - ˇ - - " - .unindent(), - false, - ); - - let mut buffer = Buffer::new(0, cx.model_id() as u64, text); - buffer.set_language_registry(language_registry); - buffer.set_language(Some(html_language), cx); - buffer.edit( - ranges.into_iter().map(|range| (range, "\na")), - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - " -
- a -
- - - a - - " - .unindent() - ); - buffer - }); -} - -#[gpui::test] -fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { - init_settings(cx, |settings| { - settings.defaults.tab_size = Some(2.try_into().unwrap()); - }); - - cx.add_model(|cx| { - let mut buffer = - Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx); - - let text = r#" - class C - def a(b, c) - puts b - puts c - rescue - puts "errored" - exit 1 - end - end - "# - .unindent(); - - buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx); - - assert_eq!( - buffer.text(), - r#" - class C - def a(b, c) - puts b - puts c - rescue - puts "errored" - exit 1 - end - end - "# - .unindent() - ); - - buffer - }); -} - -#[gpui::test] -fn test_language_scope_at_with_javascript(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let language = Language::new( - LanguageConfig { - name: "JavaScript".into(), - line_comment: Some("// ".into()), - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - newline: false, - }, - BracketPair { - start: "'".into(), - end: "'".into(), - close: true, - newline: false, - }, - ], - disabled_scopes_by_bracket_ix: vec![ - Vec::new(), // - vec!["string".into()], - ], - }, - overrides: [( - "element".into(), - LanguageConfigOverride { - line_comment: Override::Remove { remove: true }, - block_comment: Override::Set(("{/*".into(), "*/}".into())), - ..Default::default() - }, - )] - .into_iter() - .collect(), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - ) - .with_override_query( - r#" - (jsx_element) @element - (string) @string - "#, - ) - .unwrap(); - - let text = r#"a["b"] = ;"#; - - let buffer = - Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx); - let snapshot = buffer.snapshot(); - - let config = snapshot.language_scope_at(0).unwrap(); - assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// "); - // Both bracket pairs are enabled - assert_eq!( - config.brackets().map(|e| e.1).collect::>(), - &[true, true] - ); - - let string_config = snapshot.language_scope_at(3).unwrap(); - assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// "); - // Second bracket pair is disabled - assert_eq!( - string_config.brackets().map(|e| e.1).collect::>(), - &[true, false] - ); - - let element_config = snapshot.language_scope_at(10).unwrap(); - assert_eq!(element_config.line_comment_prefix(), None); - assert_eq!( - element_config.block_comment_delimiters(), - Some((&"{/*".into(), &"*/}".into())) - ); - // Both bracket pairs are enabled - assert_eq!( - element_config.brackets().map(|e| e.1).collect::>(), - &[true, true] - ); - - buffer - }); -} - -#[gpui::test] -fn test_language_scope_at_with_rust(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let language = Language::new( - LanguageConfig { - name: "Rust".into(), - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - newline: false, - }, - BracketPair { - start: "'".into(), - end: "'".into(), - close: true, - newline: false, - }, - ], - disabled_scopes_by_bracket_ix: vec![ - Vec::new(), // - vec!["string".into()], - ], - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_override_query( - r#" - (string_literal) @string - "#, - ) - .unwrap(); - - let text = r#" - const S: &'static str = "hello"; - "# - .unindent(); - - let buffer = Buffer::new(0, cx.model_id() as u64, text.clone()) - .with_language(Arc::new(language), cx); - let snapshot = buffer.snapshot(); - - // By default, all brackets are enabled - let config = snapshot.language_scope_at(0).unwrap(); - assert_eq!( - config.brackets().map(|e| e.1).collect::>(), - &[true, true] - ); - - // Within a string, the quotation brackets are disabled. - let string_config = snapshot - .language_scope_at(text.find("ello").unwrap()) - .unwrap(); - assert_eq!( - string_config.brackets().map(|e| e.1).collect::>(), - &[true, false] - ); - - buffer - }); -} - -#[gpui::test] -fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { - init_settings(cx, |_| {}); - - cx.add_model(|cx| { - let text = r#" -
    - <% people.each do |person| %> -
  1. - <%= person.name %> -
  2. - <% end %> -
- "# - .unindent(); - - let language_registry = Arc::new(LanguageRegistry::test()); - language_registry.add(Arc::new(ruby_lang())); - language_registry.add(Arc::new(html_lang())); - language_registry.add(Arc::new(erb_lang())); - - let mut buffer = Buffer::new(0, cx.model_id() as u64, text); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language( - language_registry - .language_for_name("ERB") - .now_or_never() - .unwrap() - .ok(), - cx, - ); - - let snapshot = buffer.snapshot(); - let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap(); - assert_eq!(html_config.line_comment_prefix(), None); - assert_eq!( - html_config.block_comment_delimiters(), - Some((&"".into())) - ); - - let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap(); - assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# "); - assert_eq!(ruby_config.block_comment_delimiters(), None); - - buffer - }); -} - -#[gpui::test] -fn test_serialization(cx: &mut gpui::AppContext) { - let mut now = Instant::now(); - - let buffer1 = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc"); - buffer.edit([(3..3, "D")], None, cx); - - now += Duration::from_secs(1); - buffer.start_transaction_at(now); - 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")], None, cx); - assert_eq!(buffer.text(), "abcDF"); - buffer - }); - assert_eq!(buffer1.read(cx).text(), "abcDF"); - - let state = buffer1.read(cx).to_proto(); - let ops = cx - .background() - .block(buffer1.read(cx).serialize_ops(None, cx)); - let buffer2 = cx.add_model(|cx| { - let mut buffer = Buffer::from_proto(1, state, None).unwrap(); - buffer - .apply_ops( - ops.into_iter() - .map(|op| proto::deserialize_operation(op).unwrap()), - cx, - ) - .unwrap(); - buffer - }); - assert_eq!(buffer2.read(cx).text(), "abcDF"); -} - -#[gpui::test(iterations = 100)] -fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { - let min_peers = env::var("MIN_PEERS") - .map(|i| i.parse().expect("invalid `MIN_PEERS` variable")) - .unwrap_or(1); - let max_peers = env::var("MAX_PEERS") - .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) - .unwrap_or(5); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let base_text_len = rng.gen_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let network = Rc::new(RefCell::new(Network::new(rng.clone()))); - let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str())); - - for i in 0..rng.gen_range(min_peers..=max_peers) { - let buffer = cx.add_model(|cx| { - let state = base_buffer.read(cx).to_proto(); - let ops = cx - .background() - .block(base_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap(); - buffer - .apply_ops( - ops.into_iter() - .map(|op| proto::deserialize_operation(op).unwrap()), - cx, - ) - .unwrap(); - buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); - let network = network.clone(); - cx.subscribe(&cx.handle(), move |buffer, _, event, _| { - if let Event::Operation(op) = event { - network - .borrow_mut() - .broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]); - } - }) - .detach(); - buffer - }); - buffers.push(buffer); - replica_ids.push(i as ReplicaId); - network.borrow_mut().add_peer(i as ReplicaId); - log::info!("Adding initial peer with replica id {}", i); - } - - log::info!("initial text: {:?}", base_text); - - let mut now = Instant::now(); - let mut mutation_count = operations; - let mut next_diagnostic_id = 0; - let mut active_selections = BTreeMap::default(); - loop { - let replica_index = rng.gen_range(0..replica_ids.len()); - let replica_id = replica_ids[replica_index]; - let buffer = &mut buffers[replica_index]; - let mut new_buffer = None; - match rng.gen_range(0..100) { - 0..=29 if mutation_count != 0 => { - buffer.update(cx, |buffer, cx| { - buffer.start_transaction_at(now); - buffer.randomly_edit(&mut rng, 5, cx); - buffer.end_transaction_at(now, cx); - log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text()); - }); - mutation_count -= 1; - } - 30..=39 if mutation_count != 0 => { - buffer.update(cx, |buffer, cx| { - if rng.gen_bool(0.2) { - log::info!("peer {} clearing active selections", replica_id); - active_selections.remove(&replica_id); - buffer.remove_active_selections(cx); - } else { - let mut selections = Vec::new(); - for id in 0..rng.gen_range(1..=5) { - let range = buffer.random_byte_range(0, &mut rng); - selections.push(Selection { - id, - start: buffer.anchor_before(range.start), - end: buffer.anchor_before(range.end), - reversed: false, - goal: SelectionGoal::None, - }); - } - let selections: Arc<[Selection]> = selections.into(); - log::info!( - "peer {} setting active selections: {:?}", - replica_id, - selections - ); - active_selections.insert(replica_id, selections.clone()); - buffer.set_active_selections(selections, false, Default::default(), cx); - } - }); - mutation_count -= 1; - } - 40..=49 if mutation_count != 0 && replica_id == 0 => { - let entry_count = rng.gen_range(1..=5); - buffer.update(cx, |buffer, cx| { - let diagnostics = DiagnosticSet::new( - (0..entry_count).map(|_| { - let range = buffer.random_byte_range(0, &mut rng); - let range = range.to_point_utf16(buffer); - let range = range.start..range.end; - DiagnosticEntry { - range, - diagnostic: Diagnostic { - message: post_inc(&mut next_diagnostic_id).to_string(), - ..Default::default() - }, - } - }), - buffer, - ); - log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics); - buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx); - }); - mutation_count -= 1; - } - 50..=59 if replica_ids.len() < max_peers => { - let old_buffer_state = buffer.read(cx).to_proto(); - let old_buffer_ops = cx - .background() - .block(buffer.read(cx).serialize_ops(None, cx)); - let new_replica_id = (0..=replica_ids.len() as ReplicaId) - .filter(|replica_id| *replica_id != buffer.read(cx).replica_id()) - .choose(&mut rng) - .unwrap(); - log::info!( - "Adding new replica {} (replicating from {})", - new_replica_id, - replica_id - ); - new_buffer = Some(cx.add_model(|cx| { - let mut new_buffer = - Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap(); - new_buffer - .apply_ops( - old_buffer_ops - .into_iter() - .map(|op| deserialize_operation(op).unwrap()), - cx, - ) - .unwrap(); - log::info!( - "New replica {} text: {:?}", - new_buffer.replica_id(), - new_buffer.text() - ); - new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); - let network = network.clone(); - cx.subscribe(&cx.handle(), move |buffer, _, event, _| { - if let Event::Operation(op) = event { - network.borrow_mut().broadcast( - buffer.replica_id(), - vec![proto::serialize_operation(op)], - ); - } - }) - .detach(); - new_buffer - })); - network.borrow_mut().replicate(replica_id, new_replica_id); - - if new_replica_id as usize == replica_ids.len() { - replica_ids.push(new_replica_id); - } else { - let new_buffer = new_buffer.take().unwrap(); - while network.borrow().has_unreceived(new_replica_id) { - let ops = network - .borrow_mut() - .receive(new_replica_id) - .into_iter() - .map(|op| proto::deserialize_operation(op).unwrap()); - if ops.len() > 0 { - log::info!( - "peer {} (version: {:?}) applying {} ops from the network. {:?}", - new_replica_id, - buffer.read(cx).version(), - ops.len(), - ops - ); - new_buffer.update(cx, |new_buffer, cx| { - new_buffer.apply_ops(ops, cx).unwrap(); - }); - } - } - buffers[new_replica_id as usize] = new_buffer; - } - } - 60..=69 if mutation_count != 0 => { - buffer.update(cx, |buffer, cx| { - buffer.randomly_undo_redo(&mut rng, cx); - log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text()); - }); - mutation_count -= 1; - } - _ if network.borrow().has_unreceived(replica_id) => { - let ops = network - .borrow_mut() - .receive(replica_id) - .into_iter() - .map(|op| proto::deserialize_operation(op).unwrap()); - if ops.len() > 0 { - log::info!( - "peer {} (version: {:?}) applying {} ops from the network. {:?}", - replica_id, - buffer.read(cx).version(), - ops.len(), - ops - ); - buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx).unwrap()); - } - } - _ => {} - } - - now += Duration::from_millis(rng.gen_range(0..=200)); - buffers.extend(new_buffer); - - for buffer in &buffers { - buffer.read(cx).check_invariants(); - } - - if mutation_count == 0 && network.borrow().is_idle() { - break; - } - } - - let first_buffer = buffers[0].read(cx).snapshot(); - for buffer in &buffers[1..] { - let buffer = buffer.read(cx).snapshot(); - assert_eq!( - buffer.version(), - first_buffer.version(), - "Replica {} version != Replica 0 version", - buffer.replica_id() - ); - assert_eq!( - buffer.text(), - first_buffer.text(), - "Replica {} text != Replica 0 text", - buffer.replica_id() - ); - assert_eq!( - buffer - .diagnostics_in_range::<_, usize>(0..buffer.len(), false) - .collect::>(), - first_buffer - .diagnostics_in_range::<_, usize>(0..first_buffer.len(), false) - .collect::>(), - "Replica {} diagnostics != Replica 0 diagnostics", - buffer.replica_id() - ); - } - - for buffer in &buffers { - let buffer = buffer.read(cx).snapshot(); - let actual_remote_selections = buffer - .remote_selections_in_range(Anchor::MIN..Anchor::MAX) - .map(|(replica_id, _, _, selections)| (replica_id, selections.collect::>())) - .collect::>(); - let expected_remote_selections = active_selections - .iter() - .filter(|(replica_id, _)| **replica_id != buffer.replica_id()) - .map(|(replica_id, selections)| (*replica_id, selections.iter().collect::>())) - .collect::>(); - assert_eq!( - actual_remote_selections, - expected_remote_selections, - "Replica {} remote selections != expected selections", - buffer.replica_id() - ); - } -} - -#[test] -fn test_contiguous_ranges() { - assert_eq!( - contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::>(), - &[1..4, 5..7, 9..13] - ); - - // Respects the `max_len` parameter - assert_eq!( - contiguous_ranges( - [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(), - 3 - ) - .collect::>(), - &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], - ); -} - -#[gpui::test(iterations = 500)] -fn test_trailing_whitespace_ranges(mut rng: StdRng) { - // Generate a random multi-line string containing - // some lines with trailing whitespace. - let mut text = String::new(); - for _ in 0..rng.gen_range(0..16) { - for _ in 0..rng.gen_range(0..36) { - text.push(match rng.gen_range(0..10) { - 0..=1 => ' ', - 3 => '\t', - _ => rng.gen_range('a'..'z'), - }); - } - text.push('\n'); - } - - match rng.gen_range(0..10) { - // sometimes remove the last newline - 0..=1 => drop(text.pop()), // - - // sometimes add extra newlines - 2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))), - _ => {} - } - - let rope = Rope::from(text.as_str()); - let actual_ranges = trailing_whitespace_ranges(&rope); - let expected_ranges = TRAILING_WHITESPACE_REGEX - .find_iter(&text) - .map(|m| m.range()) - .collect::>(); - assert_eq!( - actual_ranges, - expected_ranges, - "wrong ranges for text lines:\n{:?}", - text.split("\n").collect::>() - ); -} - -fn ruby_lang() -> Language { - Language::new( - LanguageConfig { - name: "Ruby".into(), - path_suffixes: vec!["rb".to_string()], - line_comment: Some("# ".into()), - ..Default::default() - }, - Some(tree_sitter_ruby::language()), - ) - .with_indents_query( - r#" - (class "end" @end) @indent - (method "end" @end) @indent - (rescue) @outdent - (then) @indent - "#, - ) - .unwrap() -} - -fn html_lang() -> Language { - Language::new( - LanguageConfig { - name: "HTML".into(), - block_comment: Some(("".into())), - ..Default::default() - }, - Some(tree_sitter_html::language()), - ) - .with_indents_query( - " - (element - (start_tag) @start - (end_tag)? @end) @indent - ", - ) - .unwrap() - .with_injection_query( - r#" - (script_element - (raw_text) @content - (#set! "language" "javascript")) - "#, - ) - .unwrap() -} - -fn erb_lang() -> Language { - Language::new( - LanguageConfig { - name: "ERB".into(), - path_suffixes: vec!["erb".to_string()], - block_comment: Some(("<%#".into(), "%>".into())), - ..Default::default() - }, - Some(tree_sitter_embedded_template::language()), - ) - .with_injection_query( - r#" - ( - (code) @content - (#set! "language" "ruby") - (#set! "combined") - ) - - ( - (content) @content - (#set! "language" "html") - (#set! "combined") - ) - "#, - ) - .unwrap() -} - -fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query( - r#" - (call_expression) @indent - (field_expression) @indent - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap() - .with_brackets_query( - r#" - ("{" @open "}" @close) - "#, - ) - .unwrap() - .with_outline_query( - r#" - (struct_item - "struct" @context - name: (_) @name) @item - (enum_item - "enum" @context - name: (_) @name) @item - (enum_variant - name: (_) @name) @item - (field_declaration - name: (_) @name) @item - (impl_item - "impl" @context - trait: (_)? @name - "for"? @context - type: (_) @name) @item - (function_item - "fn" @context - name: (_) @name) @item - (mod_item - "mod" @context - name: (_) @name) @item - "#, - ) - .unwrap() -} - -fn json_lang() -> Language { - Language::new( - LanguageConfig { - name: "Json".into(), - path_suffixes: vec!["js".to_string()], - ..Default::default() - }, - Some(tree_sitter_json::language()), - ) -} - -fn javascript_lang() -> Language { - Language::new( - LanguageConfig { - name: "JavaScript".into(), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - ) - .with_brackets_query( - r#" - ("{" @open "}" @close) - ("(" @open ")" @close) - "#, - ) - .unwrap() - .with_indents_query( - r#" - (object "}" @end) @indent - "#, - ) - .unwrap() -} - -fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { - buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let layers = snapshot.syntax.layers(buffer.as_text_snapshot()); - layers[0].node().to_sexp() - }) -} - -// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers` -fn assert_bracket_pairs( - selection_text: &'static str, - bracket_pair_texts: Vec<&'static str>, - language: Language, - cx: &mut AppContext, -) { - let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); - let buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, expected_text.clone()) - .with_language(Arc::new(language), cx) - }); - let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); - - let selection_range = selection_ranges[0].clone(); - - let bracket_pairs = bracket_pair_texts - .into_iter() - .map(|pair_text| { - let (bracket_text, ranges) = marked_text_ranges(pair_text, false); - assert_eq!(bracket_text, expected_text); - (ranges[0].clone(), ranges[1].clone()) - }) - .collect::>(); - - assert_set_eq!( - buffer.bracket_ranges(selection_range).collect::>(), - bracket_pairs - ); -} - -fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) { - cx.set_global(SettingsStore::test(cx)); - crate::init(cx); - cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, f); - }); -} +// use crate::language_settings::{ +// AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, +// }; + +// use super::*; +// use clock::ReplicaId; +// use collections::BTreeMap; +// use gpui2::{AppContext, Handle}; +// use indoc::indoc; +// use proto::deserialize_operation; +// use rand::prelude::*; +// use regex::RegexBuilder; +// use settings::SettingsStore; +// use std::{ +// cell::RefCell, +// env, +// ops::Range, +// rc::Rc, +// time::{Duration, Instant}, +// }; +// use text::network::Network; +// use text::LineEnding; +// use unindent::Unindent as _; +// use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; + +// lazy_static! { +// static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$") +// .multi_line(true) +// .build() +// .unwrap(); +// } + +// #[cfg(test)] +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// fn test_line_endings(cx: &mut gpui::AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree") +// .with_language(Arc::new(rust_lang()), cx); +// assert_eq!(buffer.text(), "one\ntwo\nthree"); +// assert_eq!(buffer.line_ending(), LineEnding::Windows); + +// buffer.check_invariants(); +// buffer.edit( +// [(buffer.len()..buffer.len(), "\r\nfour")], +// Some(AutoindentMode::EachLine), +// 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(); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_select_language() { +// let registry = Arc::new(LanguageRegistry::test()); +// registry.add(Arc::new(Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ))); +// registry.add(Arc::new(Language::new( +// LanguageConfig { +// name: "Make".into(), +// path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ))); + +// // matching file extension +// assert_eq!( +// registry +// .language_for_file("zed/lib.rs", None) +// .now_or_never() +// .and_then(|l| Some(l.ok()?.name())), +// Some("Rust".into()) +// ); +// assert_eq!( +// registry +// .language_for_file("zed/lib.mk", None) +// .now_or_never() +// .and_then(|l| Some(l.ok()?.name())), +// Some("Make".into()) +// ); + +// // matching filename +// assert_eq!( +// registry +// .language_for_file("zed/Makefile", None) +// .now_or_never() +// .and_then(|l| Some(l.ok()?.name())), +// Some("Make".into()) +// ); + +// // matching suffix that is not the full file extension or filename +// assert_eq!( +// registry +// .language_for_file("zed/cars", None) +// .now_or_never() +// .and_then(|l| Some(l.ok()?.name())), +// None +// ); +// assert_eq!( +// registry +// .language_for_file("zed/a.cars", None) +// .now_or_never() +// .and_then(|l| Some(l.ok()?.name())), +// None +// ); +// assert_eq!( +// registry +// .language_for_file("zed/sumk", None) +// .now_or_never() +// .and_then(|l| Some(l.ok()?.name())), +// None +// ); +// } + +// #[gpui::test] +// fn test_edit_events(cx: &mut gpui::AppContext) { +// let mut now = Instant::now(); +// let buffer_1_events = Rc::new(RefCell::new(Vec::new())); +// let buffer_2_events = Rc::new(RefCell::new(Vec::new())); + +// let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef")); +// let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef")); +// let buffer1_ops = Rc::new(RefCell::new(Vec::new())); +// buffer1.update(cx, { +// let buffer1_ops = buffer1_ops.clone(); +// |buffer, cx| { +// let buffer_1_events = buffer_1_events.clone(); +// cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { +// Event::Operation(op) => buffer1_ops.borrow_mut().push(op), +// event => buffer_1_events.borrow_mut().push(event), +// }) +// .detach(); +// let buffer_2_events = buffer_2_events.clone(); +// cx.subscribe(&buffer2, move |_, _, event, _| { +// buffer_2_events.borrow_mut().push(event.clone()) +// }) +// .detach(); + +// // 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")], None, cx); + +// // An empty transaction does not emit any events. +// buffer.start_transaction(); +// buffer.end_transaction(cx); + +// // A transaction containing two edits emits one edited event. +// now += Duration::from_secs(1); +// buffer.start_transaction_at(now); +// 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. +// buffer.undo(cx); +// } +// }); + +// // Incorporating a set of remote ops emits a single edited event, +// // followed by a dirty changed event. +// buffer2.update(cx, |buffer, cx| { +// buffer +// .apply_ops(buffer1_ops.borrow_mut().drain(..), cx) +// .unwrap(); +// }); +// assert_eq!( +// mem::take(&mut *buffer_1_events.borrow_mut()), +// vec![ +// Event::Edited, +// Event::DirtyChanged, +// Event::Edited, +// Event::Edited, +// ] +// ); +// assert_eq!( +// mem::take(&mut *buffer_2_events.borrow_mut()), +// vec![Event::Edited, Event::DirtyChanged] +// ); + +// buffer1.update(cx, |buffer, cx| { +// // Undoing the first transaction emits edited event, followed by a +// // dirty changed event, since the buffer is again in a clean state. +// buffer.undo(cx); +// }); +// // Incorporating the remote ops again emits a single edited event, +// // followed by a dirty changed event. +// buffer2.update(cx, |buffer, cx| { +// buffer +// .apply_ops(buffer1_ops.borrow_mut().drain(..), cx) +// .unwrap(); +// }); +// assert_eq!( +// mem::take(&mut *buffer_1_events.borrow_mut()), +// vec![Event::Edited, Event::DirtyChanged,] +// ); +// assert_eq!( +// mem::take(&mut *buffer_2_events.borrow_mut()), +// vec![Event::Edited, Event::DirtyChanged] +// ); +// } + +// #[gpui::test] +// async fn test_apply_diff(cx: &mut gpui::TestAppContext) { +// let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; +// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); +// let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); + +// let text = "a\nccc\ndddd\nffffff\n"; +// let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await; +// buffer.update(cx, |buffer, cx| { +// buffer.apply_diff(diff, cx).unwrap(); +// assert_eq!(buffer.text(), text); +// assert_eq!(anchor.to_point(buffer), Point::new(2, 3)); +// }); + +// let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; +// let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await; +// buffer.update(cx, |buffer, cx| { +// buffer.apply_diff(diff, cx).unwrap(); +// assert_eq!(buffer.text(), text); +// assert_eq!(anchor.to_point(buffer), Point::new(4, 4)); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { +// let text = [ +// "zero", // +// "one ", // 2 trailing spaces +// "two", // +// "three ", // 3 trailing spaces +// "four", // +// "five ", // 4 trailing spaces +// ] +// .join("\n"); + +// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); + +// // Spawn a task to format the buffer's whitespace. +// // Pause so that the foratting task starts running. +// let format = buffer.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx)); +// smol::future::yield_now().await; + +// // Edit the buffer while the normalization task is running. +// let version_before_edit = buffer.read_with(cx, |buffer, _| buffer.version()); +// buffer.update(cx, |buffer, cx| { +// buffer.edit( +// [ +// (Point::new(0, 1)..Point::new(0, 1), "EE"), +// (Point::new(3, 5)..Point::new(3, 5), "EEE"), +// ], +// None, +// cx, +// ); +// }); + +// let format_diff = format.await; +// buffer.update(cx, |buffer, cx| { +// let version_before_format = format_diff.base_version.clone(); +// buffer.apply_diff(format_diff, cx); + +// // The outcome depends on the order of concurrent taks. +// // +// // If the edit occurred while searching for trailing whitespace ranges, +// // then the trailing whitespace region touched by the edit is left intact. +// if version_before_format == version_before_edit { +// assert_eq!( +// buffer.text(), +// [ +// "zEEero", // +// "one", // +// "two", // +// "threeEEE ", // +// "four", // +// "five", // +// ] +// .join("\n") +// ); +// } +// // Otherwise, all trailing whitespace is removed. +// else { +// assert_eq!( +// buffer.text(), +// [ +// "zEEero", // +// "one", // +// "two", // +// "threeEEE", // +// "four", // +// "five", // +// ] +// .join("\n") +// ); +// } +// }); +// } + +// #[gpui::test] +// async fn test_reparse(cx: &mut gpui::TestAppContext) { +// let text = "fn a() {}"; +// let buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) +// }); + +// // Wait for the initial text to parse +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!( +// get_tree_sexp(&buffer, cx), +// concat!( +// "(source_file (function_item name: (identifier) ", +// "parameters: (parameters) ", +// "body: (block)))" +// ) +// ); + +// buffer.update(cx, |buffer, _| { +// buffer.set_sync_parse_timeout(Duration::ZERO) +// }); + +// // Perform some edits (add parameter and variable reference) +// // Parsing doesn't begin until the transaction is complete +// buffer.update(cx, |buf, cx| { +// buf.start_transaction(); + +// let offset = buf.text().find(')').unwrap(); +// buf.edit([(offset..offset, "b: C")], None, cx); +// assert!(!buf.is_parsing()); + +// let offset = buf.text().find('}').unwrap(); +// buf.edit([(offset..offset, " d; ")], None, cx); +// assert!(!buf.is_parsing()); + +// buf.end_transaction(cx); +// assert_eq!(buf.text(), "fn a(b: C) { d; }"); +// assert!(buf.is_parsing()); +// }); +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!( +// get_tree_sexp(&buffer, cx), +// concat!( +// "(source_file (function_item name: (identifier) ", +// "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", +// "body: (block (expression_statement (identifier)))))" +// ) +// ); + +// // Perform a series of edits without waiting for the current parse to complete: +// // * turn identifier into a field expression +// // * turn field expression into a method call +// // * add a turbofish to the method call +// buffer.update(cx, |buf, cx| { +// let offset = buf.text().find(';').unwrap(); +// 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)")], 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, "::")], None, cx); +// assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); +// assert!(buf.is_parsing()); +// }); +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!( +// get_tree_sexp(&buffer, cx), +// concat!( +// "(source_file (function_item name: (identifier) ", +// "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", +// "body: (block (expression_statement (call_expression ", +// "function: (generic_function ", +// "function: (field_expression value: (identifier) field: (field_identifier)) ", +// "type_arguments: (type_arguments (type_identifier))) ", +// "arguments: (arguments (identifier)))))))", +// ) +// ); + +// buffer.update(cx, |buf, cx| { +// buf.undo(cx); +// buf.undo(cx); +// buf.undo(cx); +// buf.undo(cx); +// assert_eq!(buf.text(), "fn a() {}"); +// assert!(buf.is_parsing()); +// }); +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!( +// get_tree_sexp(&buffer, cx), +// concat!( +// "(source_file (function_item name: (identifier) ", +// "parameters: (parameters) ", +// "body: (block)))" +// ) +// ); + +// buffer.update(cx, |buf, cx| { +// buf.redo(cx); +// buf.redo(cx); +// buf.redo(cx); +// buf.redo(cx); +// assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); +// assert!(buf.is_parsing()); +// }); +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!( +// get_tree_sexp(&buffer, cx), +// concat!( +// "(source_file (function_item name: (identifier) ", +// "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", +// "body: (block (expression_statement (call_expression ", +// "function: (generic_function ", +// "function: (field_expression value: (identifier) field: (field_identifier)) ", +// "type_arguments: (type_arguments (type_identifier))) ", +// "arguments: (arguments (identifier)))))))", +// ) +// ); +// } + +// #[gpui::test] +// async fn test_resetting_language(cx: &mut gpui::TestAppContext) { +// let buffer = cx.add_model(|cx| { +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx); +// buffer.set_sync_parse_timeout(Duration::ZERO); +// buffer +// }); + +// // Wait for the initial text to parse +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!( +// get_tree_sexp(&buffer, cx), +// "(source_file (expression_statement (block)))" +// ); + +// buffer.update(cx, |buffer, cx| { +// buffer.set_language(Some(Arc::new(json_lang())), cx) +// }); +// buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; +// assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))"); +// } + +// #[gpui::test] +// async fn test_outline(cx: &mut gpui::TestAppContext) { +// let text = r#" +// struct Person { +// name: String, +// age: usize, +// } + +// mod module { +// enum LoginState { +// LoggedOut, +// LoggingOn, +// LoggedIn { +// person: Person, +// time: Instant, +// } +// } +// } + +// impl Eq for Person {} + +// impl Drop for Person { +// fn drop(&mut self) { +// println!("bye"); +// } +// } +// "# +// .unindent(); + +// let buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) +// }); +// let outline = buffer +// .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) +// .unwrap(); + +// assert_eq!( +// outline +// .items +// .iter() +// .map(|item| (item.text.as_str(), item.depth)) +// .collect::>(), +// &[ +// ("struct Person", 0), +// ("name", 1), +// ("age", 1), +// ("mod module", 0), +// ("enum LoginState", 1), +// ("LoggedOut", 2), +// ("LoggingOn", 2), +// ("LoggedIn", 2), +// ("person", 3), +// ("time", 3), +// ("impl Eq for Person", 0), +// ("impl Drop for Person", 0), +// ("fn drop", 1), +// ] +// ); + +// // Without space, we only match on names +// assert_eq!( +// search(&outline, "oon", cx).await, +// &[ +// ("mod module", vec![]), // included as the parent of a match +// ("enum LoginState", vec![]), // included as the parent of a match +// ("LoggingOn", vec![1, 7, 8]), // matches +// ("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names +// ] +// ); + +// assert_eq!( +// search(&outline, "dp p", cx).await, +// &[ +// ("impl Drop for Person", vec![5, 8, 9, 14]), +// ("fn drop", vec![]), +// ] +// ); +// assert_eq!( +// search(&outline, "dpn", cx).await, +// &[("impl Drop for Person", vec![5, 14, 19])] +// ); +// assert_eq!( +// search(&outline, "impl ", cx).await, +// &[ +// ("impl Eq for Person", vec![0, 1, 2, 3, 4]), +// ("impl Drop for Person", vec![0, 1, 2, 3, 4]), +// ("fn drop", vec![]), +// ] +// ); + +// async fn search<'a>( +// outline: &'a Outline, +// query: &'a str, +// cx: &'a gpui::TestAppContext, +// ) -> Vec<(&'a str, Vec)> { +// let matches = cx +// .read(|cx| outline.search(query, cx.background().clone())) +// .await; +// matches +// .into_iter() +// .map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions)) +// .collect::>() +// } +// } + +// #[gpui::test] +// async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { +// let text = r#" +// impl A for B< +// C +// > { +// }; +// "# +// .unindent(); + +// let buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) +// }); +// let outline = buffer +// .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) +// .unwrap(); + +// assert_eq!( +// outline +// .items +// .iter() +// .map(|item| (item.text.as_str(), item.depth)) +// .collect::>(), +// &[("impl A for B<", 0)] +// ); +// } + +// #[gpui::test] +// async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { +// let language = javascript_lang() +// .with_outline_query( +// r#" +// (function_declaration +// "function" @context +// name: (_) @name +// parameters: (formal_parameters +// "(" @context.extra +// ")" @context.extra)) @item +// "#, +// ) +// .unwrap(); + +// let text = r#" +// function a() {} +// function b(c) {} +// "# +// .unindent(); + +// let buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx) +// }); +// let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + +// // extra context nodes are included in the outline. +// let outline = snapshot.outline(None).unwrap(); +// assert_eq!( +// outline +// .items +// .iter() +// .map(|item| (item.text.as_str(), item.depth)) +// .collect::>(), +// &[("function a()", 0), ("function b( )", 0),] +// ); + +// // extra context nodes do not appear in breadcrumbs. +// let symbols = snapshot.symbols_containing(3, None).unwrap(); +// assert_eq!( +// symbols +// .iter() +// .map(|item| (item.text.as_str(), item.depth)) +// .collect::>(), +// &[("function a", 0)] +// ); +// } + +// #[gpui::test] +// async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { +// let text = r#" +// impl Person { +// fn one() { +// 1 +// } + +// fn two() { +// 2 +// }fn three() { +// 3 +// } +// } +// "# +// .unindent(); + +// let buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) +// }); +// let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + +// // point is at the start of an item +// assert_eq!( +// symbols_containing(Point::new(1, 4), &snapshot), +// vec![ +// ( +// "impl Person".to_string(), +// Point::new(0, 0)..Point::new(10, 1) +// ), +// ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5)) +// ] +// ); + +// // point is in the middle of an item +// assert_eq!( +// symbols_containing(Point::new(2, 8), &snapshot), +// vec![ +// ( +// "impl Person".to_string(), +// Point::new(0, 0)..Point::new(10, 1) +// ), +// ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5)) +// ] +// ); + +// // point is at the end of an item +// assert_eq!( +// symbols_containing(Point::new(3, 5), &snapshot), +// vec![ +// ( +// "impl Person".to_string(), +// Point::new(0, 0)..Point::new(10, 1) +// ), +// ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5)) +// ] +// ); + +// // point is in between two adjacent items +// assert_eq!( +// symbols_containing(Point::new(7, 5), &snapshot), +// vec![ +// ( +// "impl Person".to_string(), +// Point::new(0, 0)..Point::new(10, 1) +// ), +// ("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5)) +// ] +// ); + +// fn symbols_containing( +// position: Point, +// snapshot: &BufferSnapshot, +// ) -> Vec<(String, Range)> { +// snapshot +// .symbols_containing(position, None) +// .unwrap() +// .into_iter() +// .map(|item| { +// ( +// item.text, +// item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot), +// ) +// }) +// .collect() +// } +// } + +// #[gpui::test] +// fn test_enclosing_bracket_ranges(cx: &mut AppContext) { +// let mut assert = |selection_text, range_markers| { +// assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx) +// }; + +// assert( +// indoc! {" +// mod x { +// moˇd y { + +// } +// } +// let foo = 1;"}, +// vec![indoc! {" +// mod x «{» +// mod y { + +// } +// «}» +// let foo = 1;"}], +// ); + +// assert( +// indoc! {" +// mod x { +// mod y ˇ{ + +// } +// } +// let foo = 1;"}, +// vec![ +// indoc! {" +// mod x «{» +// mod y { + +// } +// «}» +// let foo = 1;"}, +// indoc! {" +// mod x { +// mod y «{» + +// «}» +// } +// let foo = 1;"}, +// ], +// ); + +// assert( +// indoc! {" +// mod x { +// mod y { + +// }ˇ +// } +// let foo = 1;"}, +// vec![ +// indoc! {" +// mod x «{» +// mod y { + +// } +// «}» +// let foo = 1;"}, +// indoc! {" +// mod x { +// mod y «{» + +// «}» +// } +// let foo = 1;"}, +// ], +// ); + +// assert( +// indoc! {" +// mod x { +// mod y { + +// } +// ˇ} +// let foo = 1;"}, +// vec![indoc! {" +// mod x «{» +// mod y { + +// } +// «}» +// let foo = 1;"}], +// ); + +// assert( +// indoc! {" +// mod x { +// mod y { + +// } +// } +// let fˇoo = 1;"}, +// vec![], +// ); + +// // Regression test: avoid crash when querying at the end of the buffer. +// assert( +// indoc! {" +// mod x { +// mod y { + +// } +// } +// let foo = 1;ˇ"}, +// vec![], +// ); +// } + +// #[gpui::test] +// fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &mut AppContext) { +// let mut assert = |selection_text, bracket_pair_texts| { +// assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx) +// }; + +// assert( +// indoc! {" +// for (const a in b)ˇ { +// // a comment that's longer than the for-loop header +// }"}, +// vec![indoc! {" +// for «(»const a in b«)» { +// // a comment that's longer than the for-loop header +// }"}], +// ); + +// // Regression test: even though the parent node of the parentheses (the for loop) does +// // intersect the given range, the parentheses themselves do not contain the range, so +// // they should not be returned. Only the curly braces contain the range. +// assert( +// indoc! {" +// for (const a in b) {ˇ +// // a comment that's longer than the for-loop header +// }"}, +// vec![indoc! {" +// for (const a in b) «{» +// // a comment that's longer than the for-loop header +// «}»"}], +// ); +// } + +// #[gpui::test] +// fn test_range_for_syntax_ancestor(cx: &mut AppContext) { +// cx.add_model(|cx| { +// let text = "fn a() { b(|c| {}) }"; +// let buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); +// let snapshot = buffer.snapshot(); + +// assert_eq!( +// snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")), +// Some(range_of(text, "|")) +// ); +// assert_eq!( +// snapshot.range_for_syntax_ancestor(range_of(text, "|")), +// Some(range_of(text, "|c|")) +// ); +// assert_eq!( +// snapshot.range_for_syntax_ancestor(range_of(text, "|c|")), +// Some(range_of(text, "|c| {}")) +// ); +// assert_eq!( +// snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")), +// Some(range_of(text, "(|c| {})")) +// ); + +// buffer +// }); + +// fn empty_range_at(text: &str, part: &str) -> Range { +// let start = text.find(part).unwrap(); +// start..start +// } + +// fn range_of(text: &str, part: &str) -> Range { +// let start = text.find(part).unwrap(); +// start..start + part.len() +// } +// } + +// #[gpui::test] +// fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let text = "fn a() {}"; +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); + +// buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); +// assert_eq!(buffer.text(), "fn a() {\n \n}"); + +// buffer.edit( +// [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], +// 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( +// [(Point::new(2, 4)..Point::new(2, 4), ".c")], +// 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( +// [(Point::new(2, 8)..Point::new(2, 9), "")], +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { +// init_settings(cx, |settings| { +// settings.defaults.hard_tabs = Some(true); +// }); + +// cx.add_model(|cx| { +// let text = "fn a() {}"; +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); + +// buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); +// assert_eq!(buffer.text(), "fn a() {\n\t\n}"); + +// buffer.edit( +// [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], +// 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( +// [(Point::new(2, 1)..Point::new(2, 1), ".c")], +// 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( +// [(Point::new(2, 2)..Point::new(2, 3), "")], +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let mut buffer = Buffer::new( +// 0, +// cx.model_id() as u64, +// " +// fn a() { +// c; +// d; +// } +// " +// .unindent(), +// ) +// .with_language(Arc::new(rust_lang()), cx); + +// // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, +// // their indentation is not adjusted. +// buffer.edit_via_marked_text( +// &" +// fn a() { +// c«()»; +// d«()»; +// } +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a() { +// c(); +// d(); +// } +// " +// .unindent() +// ); + +// // When appending new content after these lines, the indentation is based on the +// // preceding lines' actual indentation. +// buffer.edit_via_marked_text( +// &" +// fn a() { +// c« +// .f +// .g()»; +// d« +// .f +// .g()»; +// } +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); + +// assert_eq!( +// buffer.text(), +// " +// fn a() { +// c +// .f +// .g(); +// d +// .f +// .g(); +// } +// " +// .unindent() +// ); +// buffer +// }); + +// cx.add_model(|cx| { +// let mut buffer = Buffer::new( +// 0, +// cx.model_id() as u64, +// " +// fn a() { +// b(); +// | +// " +// .replace("|", "") // marker to preserve trailing whitespace +// .unindent(), +// ) +// .with_language(Arc::new(rust_lang()), cx); + +// // Insert a closing brace. It is outdented. +// buffer.edit_via_marked_text( +// &" +// fn a() { +// b(); +// «}» +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a() { +// b(); +// } +// " +// .unindent() +// ); + +// // Manually edit the leading whitespace. The edit is preserved. +// buffer.edit_via_marked_text( +// &" +// fn a() { +// b(); +// « »} +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a() { +// b(); +// } +// " +// .unindent() +// ); +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let mut buffer = Buffer::new( +// 0, +// cx.model_id() as u64, +// " +// fn a() { +// i +// } +// " +// .unindent(), +// ) +// .with_language(Arc::new(rust_lang()), cx); + +// // Regression test: line does not get outdented due to syntax error +// buffer.edit_via_marked_text( +// &" +// fn a() { +// i«f let Some(x) = y» +// } +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a() { +// if let Some(x) = y +// } +// " +// .unindent() +// ); + +// buffer.edit_via_marked_text( +// &" +// fn a() { +// if let Some(x) = y« {» +// } +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a() { +// if let Some(x) = y { +// } +// " +// .unindent() +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let mut buffer = Buffer::new( +// 0, +// cx.model_id() as u64, +// " +// fn a() {} +// " +// .unindent(), +// ) +// .with_language(Arc::new(rust_lang()), cx); + +// buffer.edit_via_marked_text( +// &" +// fn a(« +// b») {} +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a( +// b) {} +// " +// .unindent() +// ); + +// // The indentation suggestion changed because `@end` node (a close paren) +// // is now at the beginning of the line. +// buffer.edit_via_marked_text( +// &" +// fn a( +// ˇ) {} +// " +// .unindent(), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// fn a( +// ) {} +// " +// .unindent() +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let text = "a\nb"; +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), 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_multi_line_insertion(cx: &mut AppContext) { +// init_settings(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, cx.model_id() as u64, text).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 AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let text = r#" +// fn a() { +// b(); +// } +// "# +// .unindent(); +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); + +// // When this text was copied, both of the quotation marks were at the same +// // indent level, but the indentation of the first line was not included in +// // the copied text. This information is retained in the +// // 'original_indent_columns' vector. +// let original_indent_columns = vec![4]; +// 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: original_indent_columns.clone(), +// }), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// r#" +// fn a() { +// b(); +// " +// c +// d +// e +// " +// } +// "# +// .unindent() +// ); + +// // Grouping is disabled in tests, so we need 2 undos +// buffer.undo(cx); // Undo the auto-indent +// buffer.undo(cx); // Undo the original edit + +// // Insert the block at a deeper indent level. The entire block is outdented. +// buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); +// buffer.edit( +// [(Point::new(2, 8)..Point::new(2, 8), inserted_text)], +// Some(AutoindentMode::Block { +// original_indent_columns: original_indent_columns.clone(), +// }), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// r#" +// fn a() { +// b(); +// " +// c +// d +// e +// " +// } +// "# +// .unindent() +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let text = r#" +// fn a() { +// if b() { + +// } +// } +// "# +// .unindent(); +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); + +// // The original indent columns are not known, so this text is +// // auto-indented in a block as if the first line was copied in +// // its entirety. +// let original_indent_columns = Vec::new(); +// let inserted_text = " c\n .d()\n .e();"; + +// // 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)], +// Some(AutoindentMode::Block { +// original_indent_columns: original_indent_columns.clone(), +// }), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// r#" +// fn a() { +// if b() { +// c +// .d() +// .e(); +// } +// } +// "# +// .unindent() +// ); + +// // Grouping is disabled in tests, so we need 2 undos +// buffer.undo(cx); // Undo the auto-indent +// buffer.undo(cx); // Undo the original edit + +// // Insert the block at a deeper indent level. The entire block is outdented. +// buffer.edit( +// [(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))], +// None, +// cx, +// ); +// buffer.edit( +// [(Point::new(2, 12)..Point::new(2, 12), inserted_text)], +// Some(AutoindentMode::Block { +// original_indent_columns: Vec::new(), +// }), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// r#" +// fn a() { +// if b() { +// c +// .d() +// .e(); +// } +// } +// "# +// .unindent() +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let text = " +// * one +// - a +// - b +// * two +// " +// .unindent(); + +// let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language( +// Arc::new(Language::new( +// LanguageConfig { +// name: "Markdown".into(), +// auto_indent_using_last_non_empty_line: false, +// ..Default::default() +// }, +// Some(tree_sitter_json::language()), +// )), +// cx, +// ); +// buffer.edit( +// [(Point::new(3, 0)..Point::new(3, 0), "\n")], +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +// * one +// - a +// - b + +// * two +// " +// .unindent() +// ); +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_with_injected_languages(cx: &mut AppContext) { +// init_settings(cx, |settings| { +// settings.languages.extend([ +// ( +// "HTML".into(), +// LanguageSettingsContent { +// tab_size: Some(2.try_into().unwrap()), +// ..Default::default() +// }, +// ), +// ( +// "JavaScript".into(), +// LanguageSettingsContent { +// tab_size: Some(8.try_into().unwrap()), +// ..Default::default() +// }, +// ), +// ]) +// }); + +// let html_language = Arc::new(html_lang()); + +// let javascript_language = Arc::new(javascript_lang()); + +// let language_registry = Arc::new(LanguageRegistry::test()); +// language_registry.add(html_language.clone()); +// language_registry.add(javascript_language.clone()); + +// cx.add_model(|cx| { +// let (text, ranges) = marked_text_ranges( +// &" +//
ˇ +//
+// +// ˇ +// +// " +// .unindent(), +// false, +// ); + +// let mut buffer = Buffer::new(0, cx.model_id() as u64, text); +// buffer.set_language_registry(language_registry); +// buffer.set_language(Some(html_language), cx); +// buffer.edit( +// ranges.into_iter().map(|range| (range, "\na")), +// Some(AutoindentMode::EachLine), +// cx, +// ); +// assert_eq!( +// buffer.text(), +// " +//
+// a +//
+// +// +// a +// +// " +// .unindent() +// ); +// buffer +// }); +// } + +// #[gpui::test] +// fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { +// init_settings(cx, |settings| { +// settings.defaults.tab_size = Some(2.try_into().unwrap()); +// }); + +// cx.add_model(|cx| { +// let mut buffer = +// Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx); + +// let text = r#" +// class C +// def a(b, c) +// puts b +// puts c +// rescue +// puts "errored" +// exit 1 +// end +// end +// "# +// .unindent(); + +// buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx); + +// assert_eq!( +// buffer.text(), +// r#" +// class C +// def a(b, c) +// puts b +// puts c +// rescue +// puts "errored" +// exit 1 +// end +// end +// "# +// .unindent() +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_language_scope_at_with_javascript(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let language = Language::new( +// LanguageConfig { +// name: "JavaScript".into(), +// line_comment: Some("// ".into()), +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "{".into(), +// end: "}".into(), +// close: true, +// newline: false, +// }, +// BracketPair { +// start: "'".into(), +// end: "'".into(), +// close: true, +// newline: false, +// }, +// ], +// disabled_scopes_by_bracket_ix: vec![ +// Vec::new(), // +// vec!["string".into()], +// ], +// }, +// overrides: [( +// "element".into(), +// LanguageConfigOverride { +// line_comment: Override::Remove { remove: true }, +// block_comment: Override::Set(("{/*".into(), "*/}".into())), +// ..Default::default() +// }, +// )] +// .into_iter() +// .collect(), +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_tsx()), +// ) +// .with_override_query( +// r#" +// (jsx_element) @element +// (string) @string +// "#, +// ) +// .unwrap(); + +// let text = r#"a["b"] = ;"#; + +// let buffer = +// Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx); +// let snapshot = buffer.snapshot(); + +// let config = snapshot.language_scope_at(0).unwrap(); +// assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// "); +// // Both bracket pairs are enabled +// assert_eq!( +// config.brackets().map(|e| e.1).collect::>(), +// &[true, true] +// ); + +// let string_config = snapshot.language_scope_at(3).unwrap(); +// assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// "); +// // Second bracket pair is disabled +// assert_eq!( +// string_config.brackets().map(|e| e.1).collect::>(), +// &[true, false] +// ); + +// let element_config = snapshot.language_scope_at(10).unwrap(); +// assert_eq!(element_config.line_comment_prefix(), None); +// assert_eq!( +// element_config.block_comment_delimiters(), +// Some((&"{/*".into(), &"*/}".into())) +// ); +// // Both bracket pairs are enabled +// assert_eq!( +// element_config.brackets().map(|e| e.1).collect::>(), +// &[true, true] +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_language_scope_at_with_rust(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "{".into(), +// end: "}".into(), +// close: true, +// newline: false, +// }, +// BracketPair { +// start: "'".into(), +// end: "'".into(), +// close: true, +// newline: false, +// }, +// ], +// disabled_scopes_by_bracket_ix: vec![ +// Vec::new(), // +// vec!["string".into()], +// ], +// }, +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ) +// .with_override_query( +// r#" +// (string_literal) @string +// "#, +// ) +// .unwrap(); + +// let text = r#" +// const S: &'static str = "hello"; +// "# +// .unindent(); + +// let buffer = Buffer::new(0, cx.model_id() as u64, text.clone()) +// .with_language(Arc::new(language), cx); +// let snapshot = buffer.snapshot(); + +// // By default, all brackets are enabled +// let config = snapshot.language_scope_at(0).unwrap(); +// assert_eq!( +// config.brackets().map(|e| e.1).collect::>(), +// &[true, true] +// ); + +// // Within a string, the quotation brackets are disabled. +// let string_config = snapshot +// .language_scope_at(text.find("ello").unwrap()) +// .unwrap(); +// assert_eq!( +// string_config.brackets().map(|e| e.1).collect::>(), +// &[true, false] +// ); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { +// init_settings(cx, |_| {}); + +// cx.add_model(|cx| { +// let text = r#" +//
    +// <% people.each do |person| %> +//
  1. +// <%= person.name %> +//
  2. +// <% end %> +//
+// "# +// .unindent(); + +// let language_registry = Arc::new(LanguageRegistry::test()); +// language_registry.add(Arc::new(ruby_lang())); +// language_registry.add(Arc::new(html_lang())); +// language_registry.add(Arc::new(erb_lang())); + +// let mut buffer = Buffer::new(0, cx.model_id() as u64, text); +// buffer.set_language_registry(language_registry.clone()); +// buffer.set_language( +// language_registry +// .language_for_name("ERB") +// .now_or_never() +// .unwrap() +// .ok(), +// cx, +// ); + +// let snapshot = buffer.snapshot(); +// let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap(); +// assert_eq!(html_config.line_comment_prefix(), None); +// assert_eq!( +// html_config.block_comment_delimiters(), +// Some((&"".into())) +// ); + +// let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap(); +// assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# "); +// assert_eq!(ruby_config.block_comment_delimiters(), None); + +// buffer +// }); +// } + +// #[gpui::test] +// fn test_serialization(cx: &mut gpui::AppContext) { +// let mut now = Instant::now(); + +// let buffer1 = cx.add_model(|cx| { +// let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc"); +// buffer.edit([(3..3, "D")], None, cx); + +// now += Duration::from_secs(1); +// buffer.start_transaction_at(now); +// 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")], None, cx); +// assert_eq!(buffer.text(), "abcDF"); +// buffer +// }); +// assert_eq!(buffer1.read(cx).text(), "abcDF"); + +// let state = buffer1.read(cx).to_proto(); +// let ops = cx +// .background() +// .block(buffer1.read(cx).serialize_ops(None, cx)); +// let buffer2 = cx.add_model(|cx| { +// let mut buffer = Buffer::from_proto(1, state, None).unwrap(); +// buffer +// .apply_ops( +// ops.into_iter() +// .map(|op| proto::deserialize_operation(op).unwrap()), +// cx, +// ) +// .unwrap(); +// buffer +// }); +// assert_eq!(buffer2.read(cx).text(), "abcDF"); +// } + +// #[gpui::test(iterations = 100)] +// fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { +// let min_peers = env::var("MIN_PEERS") +// .map(|i| i.parse().expect("invalid `MIN_PEERS` variable")) +// .unwrap_or(1); +// let max_peers = env::var("MAX_PEERS") +// .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) +// .unwrap_or(5); +// let operations = env::var("OPERATIONS") +// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) +// .unwrap_or(10); + +// let base_text_len = rng.gen_range(0..10); +// let base_text = RandomCharIter::new(&mut rng) +// .take(base_text_len) +// .collect::(); +// let mut replica_ids = Vec::new(); +// let mut buffers = Vec::new(); +// let network = Rc::new(RefCell::new(Network::new(rng.clone()))); +// let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str())); + +// for i in 0..rng.gen_range(min_peers..=max_peers) { +// let buffer = cx.add_model(|cx| { +// let state = base_buffer.read(cx).to_proto(); +// let ops = cx +// .background() +// .block(base_buffer.read(cx).serialize_ops(None, cx)); +// let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap(); +// buffer +// .apply_ops( +// ops.into_iter() +// .map(|op| proto::deserialize_operation(op).unwrap()), +// cx, +// ) +// .unwrap(); +// buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); +// let network = network.clone(); +// cx.subscribe(&cx.handle(), move |buffer, _, event, _| { +// if let Event::Operation(op) = event { +// network +// .borrow_mut() +// .broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]); +// } +// }) +// .detach(); +// buffer +// }); +// buffers.push(buffer); +// replica_ids.push(i as ReplicaId); +// network.borrow_mut().add_peer(i as ReplicaId); +// log::info!("Adding initial peer with replica id {}", i); +// } + +// log::info!("initial text: {:?}", base_text); + +// let mut now = Instant::now(); +// let mut mutation_count = operations; +// let mut next_diagnostic_id = 0; +// let mut active_selections = BTreeMap::default(); +// loop { +// let replica_index = rng.gen_range(0..replica_ids.len()); +// let replica_id = replica_ids[replica_index]; +// let buffer = &mut buffers[replica_index]; +// let mut new_buffer = None; +// match rng.gen_range(0..100) { +// 0..=29 if mutation_count != 0 => { +// buffer.update(cx, |buffer, cx| { +// buffer.start_transaction_at(now); +// buffer.randomly_edit(&mut rng, 5, cx); +// buffer.end_transaction_at(now, cx); +// log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text()); +// }); +// mutation_count -= 1; +// } +// 30..=39 if mutation_count != 0 => { +// buffer.update(cx, |buffer, cx| { +// if rng.gen_bool(0.2) { +// log::info!("peer {} clearing active selections", replica_id); +// active_selections.remove(&replica_id); +// buffer.remove_active_selections(cx); +// } else { +// let mut selections = Vec::new(); +// for id in 0..rng.gen_range(1..=5) { +// let range = buffer.random_byte_range(0, &mut rng); +// selections.push(Selection { +// id, +// start: buffer.anchor_before(range.start), +// end: buffer.anchor_before(range.end), +// reversed: false, +// goal: SelectionGoal::None, +// }); +// } +// let selections: Arc<[Selection]> = selections.into(); +// log::info!( +// "peer {} setting active selections: {:?}", +// replica_id, +// selections +// ); +// active_selections.insert(replica_id, selections.clone()); +// buffer.set_active_selections(selections, false, Default::default(), cx); +// } +// }); +// mutation_count -= 1; +// } +// 40..=49 if mutation_count != 0 && replica_id == 0 => { +// let entry_count = rng.gen_range(1..=5); +// buffer.update(cx, |buffer, cx| { +// let diagnostics = DiagnosticSet::new( +// (0..entry_count).map(|_| { +// let range = buffer.random_byte_range(0, &mut rng); +// let range = range.to_point_utf16(buffer); +// let range = range.start..range.end; +// DiagnosticEntry { +// range, +// diagnostic: Diagnostic { +// message: post_inc(&mut next_diagnostic_id).to_string(), +// ..Default::default() +// }, +// } +// }), +// buffer, +// ); +// log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics); +// buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx); +// }); +// mutation_count -= 1; +// } +// 50..=59 if replica_ids.len() < max_peers => { +// let old_buffer_state = buffer.read(cx).to_proto(); +// let old_buffer_ops = cx +// .background() +// .block(buffer.read(cx).serialize_ops(None, cx)); +// let new_replica_id = (0..=replica_ids.len() as ReplicaId) +// .filter(|replica_id| *replica_id != buffer.read(cx).replica_id()) +// .choose(&mut rng) +// .unwrap(); +// log::info!( +// "Adding new replica {} (replicating from {})", +// new_replica_id, +// replica_id +// ); +// new_buffer = Some(cx.add_model(|cx| { +// let mut new_buffer = +// Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap(); +// new_buffer +// .apply_ops( +// old_buffer_ops +// .into_iter() +// .map(|op| deserialize_operation(op).unwrap()), +// cx, +// ) +// .unwrap(); +// log::info!( +// "New replica {} text: {:?}", +// new_buffer.replica_id(), +// new_buffer.text() +// ); +// new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); +// let network = network.clone(); +// cx.subscribe(&cx.handle(), move |buffer, _, event, _| { +// if let Event::Operation(op) = event { +// network.borrow_mut().broadcast( +// buffer.replica_id(), +// vec![proto::serialize_operation(op)], +// ); +// } +// }) +// .detach(); +// new_buffer +// })); +// network.borrow_mut().replicate(replica_id, new_replica_id); + +// if new_replica_id as usize == replica_ids.len() { +// replica_ids.push(new_replica_id); +// } else { +// let new_buffer = new_buffer.take().unwrap(); +// while network.borrow().has_unreceived(new_replica_id) { +// let ops = network +// .borrow_mut() +// .receive(new_replica_id) +// .into_iter() +// .map(|op| proto::deserialize_operation(op).unwrap()); +// if ops.len() > 0 { +// log::info!( +// "peer {} (version: {:?}) applying {} ops from the network. {:?}", +// new_replica_id, +// buffer.read(cx).version(), +// ops.len(), +// ops +// ); +// new_buffer.update(cx, |new_buffer, cx| { +// new_buffer.apply_ops(ops, cx).unwrap(); +// }); +// } +// } +// buffers[new_replica_id as usize] = new_buffer; +// } +// } +// 60..=69 if mutation_count != 0 => { +// buffer.update(cx, |buffer, cx| { +// buffer.randomly_undo_redo(&mut rng, cx); +// log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text()); +// }); +// mutation_count -= 1; +// } +// _ if network.borrow().has_unreceived(replica_id) => { +// let ops = network +// .borrow_mut() +// .receive(replica_id) +// .into_iter() +// .map(|op| proto::deserialize_operation(op).unwrap()); +// if ops.len() > 0 { +// log::info!( +// "peer {} (version: {:?}) applying {} ops from the network. {:?}", +// replica_id, +// buffer.read(cx).version(), +// ops.len(), +// ops +// ); +// buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx).unwrap()); +// } +// } +// _ => {} +// } + +// now += Duration::from_millis(rng.gen_range(0..=200)); +// buffers.extend(new_buffer); + +// for buffer in &buffers { +// buffer.read(cx).check_invariants(); +// } + +// if mutation_count == 0 && network.borrow().is_idle() { +// break; +// } +// } + +// let first_buffer = buffers[0].read(cx).snapshot(); +// for buffer in &buffers[1..] { +// let buffer = buffer.read(cx).snapshot(); +// assert_eq!( +// buffer.version(), +// first_buffer.version(), +// "Replica {} version != Replica 0 version", +// buffer.replica_id() +// ); +// assert_eq!( +// buffer.text(), +// first_buffer.text(), +// "Replica {} text != Replica 0 text", +// buffer.replica_id() +// ); +// assert_eq!( +// buffer +// .diagnostics_in_range::<_, usize>(0..buffer.len(), false) +// .collect::>(), +// first_buffer +// .diagnostics_in_range::<_, usize>(0..first_buffer.len(), false) +// .collect::>(), +// "Replica {} diagnostics != Replica 0 diagnostics", +// buffer.replica_id() +// ); +// } + +// for buffer in &buffers { +// let buffer = buffer.read(cx).snapshot(); +// let actual_remote_selections = buffer +// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) +// .map(|(replica_id, _, _, selections)| (replica_id, selections.collect::>())) +// .collect::>(); +// let expected_remote_selections = active_selections +// .iter() +// .filter(|(replica_id, _)| **replica_id != buffer.replica_id()) +// .map(|(replica_id, selections)| (*replica_id, selections.iter().collect::>())) +// .collect::>(); +// assert_eq!( +// actual_remote_selections, +// expected_remote_selections, +// "Replica {} remote selections != expected selections", +// buffer.replica_id() +// ); +// } +// } + +// #[test] +// fn test_contiguous_ranges() { +// assert_eq!( +// contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::>(), +// &[1..4, 5..7, 9..13] +// ); + +// // Respects the `max_len` parameter +// assert_eq!( +// contiguous_ranges( +// [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(), +// 3 +// ) +// .collect::>(), +// &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], +// ); +// } + +// #[gpui::test(iterations = 500)] +// fn test_trailing_whitespace_ranges(mut rng: StdRng) { +// // Generate a random multi-line string containing +// // some lines with trailing whitespace. +// let mut text = String::new(); +// for _ in 0..rng.gen_range(0..16) { +// for _ in 0..rng.gen_range(0..36) { +// text.push(match rng.gen_range(0..10) { +// 0..=1 => ' ', +// 3 => '\t', +// _ => rng.gen_range('a'..'z'), +// }); +// } +// text.push('\n'); +// } + +// match rng.gen_range(0..10) { +// // sometimes remove the last newline +// 0..=1 => drop(text.pop()), // + +// // sometimes add extra newlines +// 2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))), +// _ => {} +// } + +// let rope = Rope::from(text.as_str()); +// let actual_ranges = trailing_whitespace_ranges(&rope); +// let expected_ranges = TRAILING_WHITESPACE_REGEX +// .find_iter(&text) +// .map(|m| m.range()) +// .collect::>(); +// assert_eq!( +// actual_ranges, +// expected_ranges, +// "wrong ranges for text lines:\n{:?}", +// text.split("\n").collect::>() +// ); +// } + +// fn ruby_lang() -> Language { +// Language::new( +// LanguageConfig { +// name: "Ruby".into(), +// path_suffixes: vec!["rb".to_string()], +// line_comment: Some("# ".into()), +// ..Default::default() +// }, +// Some(tree_sitter_ruby::language()), +// ) +// .with_indents_query( +// r#" +// (class "end" @end) @indent +// (method "end" @end) @indent +// (rescue) @outdent +// (then) @indent +// "#, +// ) +// .unwrap() +// } + +// fn html_lang() -> Language { +// Language::new( +// LanguageConfig { +// name: "HTML".into(), +// block_comment: Some(("".into())), +// ..Default::default() +// }, +// Some(tree_sitter_html::language()), +// ) +// .with_indents_query( +// " +// (element +// (start_tag) @start +// (end_tag)? @end) @indent +// ", +// ) +// .unwrap() +// .with_injection_query( +// r#" +// (script_element +// (raw_text) @content +// (#set! "language" "javascript")) +// "#, +// ) +// .unwrap() +// } + +// fn erb_lang() -> Language { +// Language::new( +// LanguageConfig { +// name: "ERB".into(), +// path_suffixes: vec!["erb".to_string()], +// block_comment: Some(("<%#".into(), "%>".into())), +// ..Default::default() +// }, +// Some(tree_sitter_embedded_template::language()), +// ) +// .with_injection_query( +// r#" +// ( +// (code) @content +// (#set! "language" "ruby") +// (#set! "combined") +// ) + +// ( +// (content) @content +// (#set! "language" "html") +// (#set! "combined") +// ) +// "#, +// ) +// .unwrap() +// } + +// fn rust_lang() -> Language { +// Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ) +// .with_indents_query( +// r#" +// (call_expression) @indent +// (field_expression) @indent +// (_ "(" ")" @end) @indent +// (_ "{" "}" @end) @indent +// "#, +// ) +// .unwrap() +// .with_brackets_query( +// r#" +// ("{" @open "}" @close) +// "#, +// ) +// .unwrap() +// .with_outline_query( +// r#" +// (struct_item +// "struct" @context +// name: (_) @name) @item +// (enum_item +// "enum" @context +// name: (_) @name) @item +// (enum_variant +// name: (_) @name) @item +// (field_declaration +// name: (_) @name) @item +// (impl_item +// "impl" @context +// trait: (_)? @name +// "for"? @context +// type: (_) @name) @item +// (function_item +// "fn" @context +// name: (_) @name) @item +// (mod_item +// "mod" @context +// name: (_) @name) @item +// "#, +// ) +// .unwrap() +// } + +// fn json_lang() -> Language { +// Language::new( +// LanguageConfig { +// name: "Json".into(), +// path_suffixes: vec!["js".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_json::language()), +// ) +// } + +// fn javascript_lang() -> Language { +// Language::new( +// LanguageConfig { +// name: "JavaScript".into(), +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_tsx()), +// ) +// .with_brackets_query( +// r#" +// ("{" @open "}" @close) +// ("(" @open ")" @close) +// "#, +// ) +// .unwrap() +// .with_indents_query( +// r#" +// (object "}" @end) @indent +// "#, +// ) +// .unwrap() +// } + +// fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { +// buffer.read_with(cx, |buffer, _| { +// let snapshot = buffer.snapshot(); +// let layers = snapshot.syntax.layers(buffer.as_text_snapshot()); +// layers[0].node().to_sexp() +// }) +// } + +// // Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers` +// fn assert_bracket_pairs( +// selection_text: &'static str, +// bracket_pair_texts: Vec<&'static str>, +// language: Language, +// cx: &mut AppContext, +// ) { +// let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); +// let buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, expected_text.clone()) +// .with_language(Arc::new(language), cx) +// }); +// let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); + +// let selection_range = selection_ranges[0].clone(); + +// let bracket_pairs = bracket_pair_texts +// .into_iter() +// .map(|pair_text| { +// let (bracket_text, ranges) = marked_text_ranges(pair_text, false); +// assert_eq!(bracket_text, expected_text); +// (ranges[0].clone(), ranges[1].clone()) +// }) +// .collect::>(); + +// assert_set_eq!( +// buffer.bracket_ranges(selection_range).collect::>(), +// bracket_pairs +// ); +// } + +// fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) { +// cx.set_global(SettingsStore::test(cx)); +// crate::init(cx); +// cx.update_global::(|settings, cx| { +// settings.update_user_settings::(cx, f); +// }); +// } diff --git a/crates/language2/src/diagnostic_set.rs b/crates/language2/src/diagnostic_set.rs index f269fce88d694c8efe2f255e302bbef7aed865ea..5247af285e7fd71f0f07890fac2e3eb5b54c6de1 100644 --- a/crates/language2/src/diagnostic_set.rs +++ b/crates/language2/src/diagnostic_set.rs @@ -1,6 +1,6 @@ use crate::Diagnostic; use collections::HashMap; -use lsp::LanguageServerId; +use lsp2::LanguageServerId; use std::{ cmp::{Ordering, Reverse}, iter, @@ -37,14 +37,14 @@ pub struct Summary { impl DiagnosticEntry { // Used to provide diagnostic context to lsp codeAction request - pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { + pub fn to_lsp_diagnostic_stub(&self) -> lsp2::Diagnostic { let code = self .diagnostic .code .clone() - .map(lsp::NumberOrString::String); + .map(lsp2::NumberOrString::String); - lsp::Diagnostic { + lsp2::Diagnostic { code, severity: Some(self.diagnostic.severity), ..Default::default() diff --git a/crates/language2/src/highlight_map.rs b/crates/language2/src/highlight_map.rs index abf7331e91898a81fd434ec1c63d2b8ea7ef0086..8953fa571bbdd2424372ecc18004c58819ab5cc9 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -1,6 +1,6 @@ use gpui2::HighlightStyle; use std::sync::Arc; -use theme::SyntaxTheme; +use theme2::SyntaxTheme; #[derive(Clone, Debug)] pub struct HighlightMap(Arc<[HighlightId]>); @@ -76,36 +76,36 @@ impl Default for HighlightId { } } -#[cfg(test)] -mod tests { - use super::*; - use gpui::color::Color; +// #[cfg(test)] +// mod tests { +// use super::*; +// use gpui2::color::Color; - #[test] - fn test_highlight_map() { - let theme = SyntaxTheme::new( - [ - ("function", Color::from_u32(0x100000ff)), - ("function.method", Color::from_u32(0x200000ff)), - ("function.async", Color::from_u32(0x300000ff)), - ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), - ("variable.builtin", Color::from_u32(0x500000ff)), - ("variable", Color::from_u32(0x600000ff)), - ] - .iter() - .map(|(name, color)| (name.to_string(), (*color).into())) - .collect(), - ); +// #[test] +// fn test_highlight_map() { +// let theme = SyntaxTheme::new( +// [ +// ("function", Color::from_u32(0x100000ff)), +// ("function.method", Color::from_u32(0x200000ff)), +// ("function.async", Color::from_u32(0x300000ff)), +// ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), +// ("variable.builtin", Color::from_u32(0x500000ff)), +// ("variable", Color::from_u32(0x600000ff)), +// ] +// .iter() +// .map(|(name, color)| (name.to_string(), (*color).into())) +// .collect(), +// ); - let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), - ]; +// let capture_names = &[ +// "function.special".to_string(), +// "function.async.rust".to_string(), +// "variable.builtin.self".to_string(), +// ]; - let map = HighlightMap::new(capture_names, &theme); - assert_eq!(map.get(0).name(&theme), Some("function")); - assert_eq!(map.get(1).name(&theme), Some("function.async")); - assert_eq!(map.get(2).name(&theme), Some("variable.builtin")); - } -} +// let map = HighlightMap::new(capture_names, &theme); +// assert_eq!(map.get(0).name(&theme), Some("function")); +// assert_eq!(map.get(1).name(&theme), Some("function.async")); +// assert_eq!(map.get(2).name(&theme), Some("variable.builtin")); +// } +// } diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 88ca6a2c7a586d6ecb50fc311fa070c7d1214599..6f267c802f8e69829153cef9ace80d9aa76abbe4 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -20,7 +20,7 @@ use futures::{ use gpui2::{AppContext, AsyncAppContext, Executor, Task}; pub use highlight_map::HighlightMap; use lazy_static::lazy_static; -use lsp::{CodeActionKind, LanguageServerBinary}; +use lsp2::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; use postage::watch; use regex::Regex; @@ -42,7 +42,7 @@ use std::{ }, }; use syntax_map::SyntaxSnapshot; -use theme::{SyntaxTheme, Theme}; +use theme2::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; @@ -51,7 +51,7 @@ use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; pub use buffer::Operation; pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; -pub use lsp::LanguageServerId; +pub use lsp2::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; pub use text::LineEnding; @@ -98,7 +98,7 @@ lazy_static! { } pub trait ToLspPosition { - fn to_lsp_position(self) -> lsp::Position; + fn to_lsp_position(self) -> lsp2::Position; } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -201,17 +201,17 @@ impl CachedLspAdapter { self.adapter.workspace_configuration(cx) } - pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { + pub fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) { self.adapter.process_diagnostics(params) } - pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { + pub async fn process_completion(&self, completion_item: &mut lsp2::CompletionItem) { self.adapter.process_completion(completion_item).await } pub async fn label_for_completion( &self, - completion_item: &lsp::CompletionItem, + completion_item: &lsp2::CompletionItem, language: &Arc, ) -> Option { self.adapter @@ -222,7 +222,7 @@ impl CachedLspAdapter { pub async fn label_for_symbol( &self, name: &str, - kind: lsp::SymbolKind, + kind: lsp2::SymbolKind, language: &Arc, ) -> Option { self.adapter.label_for_symbol(name, kind, language).await @@ -287,13 +287,13 @@ pub trait LspAdapter: 'static + Send + Sync { container_dir: PathBuf, ) -> Option; - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + fn process_diagnostics(&self, _: &mut lsp2::PublishDiagnosticsParams) {} - async fn process_completion(&self, _: &mut lsp::CompletionItem) {} + async fn process_completion(&self, _: &mut lsp2::CompletionItem) {} async fn label_for_completion( &self, - _: &lsp::CompletionItem, + _: &lsp2::CompletionItem, _: &Arc, ) -> Option { None @@ -302,7 +302,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn label_for_symbol( &self, _: &str, - _: lsp::SymbolKind, + _: lsp2::SymbolKind, _: &Arc, ) -> Option { None @@ -494,8 +494,8 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result, D pub struct FakeLspAdapter { pub name: &'static str, pub initialization_options: Option, - pub capabilities: lsp::ServerCapabilities, - pub initializer: Option>, + pub capabilities: lsp2::ServerCapabilities, + pub initializer: Option>, pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, pub enabled_formatters: Vec, @@ -550,7 +550,7 @@ pub struct Language { #[cfg(any(test, feature = "test-support"))] fake_adapter: Option<( - mpsc::UnboundedSender, + mpsc::UnboundedSender, Arc, )>, } @@ -667,7 +667,7 @@ struct LanguageRegistryState { pub struct PendingLanguageServer { pub server_id: LanguageServerId, - pub task: Task>>, + pub task: Task>>, pub container_dir: Option>, } @@ -922,7 +922,7 @@ impl LanguageRegistry { if language.fake_adapter.is_some() { let task = cx.spawn(|cx| async move { let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); - let (server, mut fake_server) = lsp::LanguageServer::fake( + let (server, mut fake_server) = lsp2::LanguageServer::fake( fake_adapter.name.to_string(), fake_adapter.capabilities.clone(), cx.clone(), @@ -933,10 +933,10 @@ impl LanguageRegistry { } let servers_tx = servers_tx.clone(); - cx.background() + cx.executor() .spawn(async move { if fake_server - .try_receive_notification::() + .try_receive_notification::() .await .is_some() { @@ -970,18 +970,22 @@ impl LanguageRegistry { let task = { let container_dir = container_dir.clone(); - cx.spawn(|mut cx| async move { + cx.spawn(move |mut cx| async move { login_shell_env_loaded.await; - let mut lock = this.lsp_binary_paths.lock(); - let entry = lock + let entry = this + .lsp_binary_paths + .lock() .entry(adapter.name.clone()) .or_insert_with(|| { + let adapter = adapter.clone(); + let language = language.clone(); + let delegate = delegate.clone(); cx.spawn(|cx| { get_binary( - adapter.clone(), - language.clone(), - delegate.clone(), + adapter, + language, + delegate, container_dir, lsp_binary_statuses, cx, @@ -991,9 +995,8 @@ impl LanguageRegistry { .shared() }) .clone(); - drop(lock); - let binary = match entry.clone().await.log_err() { + let binary = match entry.await.log_err() { Some(binary) => binary, None => return Ok(None), }; @@ -1004,7 +1007,7 @@ impl LanguageRegistry { } } - Ok(Some(lsp::LanguageServer::new( + Ok(Some(lsp2::LanguageServer::new( server_id, binary, &root_path, @@ -1486,7 +1489,7 @@ impl Language { pub async fn set_fake_lsp_adapter( &mut self, fake_lsp_adapter: Arc, - ) -> mpsc::UnboundedReceiver { + ) -> mpsc::UnboundedReceiver { let (servers_tx, servers_rx) = mpsc::unbounded(); self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await; @@ -1516,7 +1519,7 @@ impl Language { None } - pub async fn process_completion(self: &Arc, completion: &mut lsp::CompletionItem) { + pub async fn process_completion(self: &Arc, completion: &mut lsp2::CompletionItem) { for adapter in &self.adapters { adapter.process_completion(completion).await; } @@ -1524,7 +1527,7 @@ impl Language { pub async fn label_for_completion( self: &Arc, - completion: &lsp::CompletionItem, + completion: &lsp2::CompletionItem, ) -> Option { self.adapters .first() @@ -1536,7 +1539,7 @@ impl Language { pub async fn label_for_symbol( self: &Arc, name: &str, - kind: lsp::SymbolKind, + kind: lsp2::SymbolKind, ) -> Option { self.adapters .first() @@ -1756,7 +1759,7 @@ impl Default for FakeLspAdapter { fn default() -> Self { Self { name: "the-fake-language-server", - capabilities: lsp::LanguageServer::full_capabilities(), + capabilities: lsp2::LanguageServer::full_capabilities(), initializer: None, disk_based_diagnostics_progress_token: None, initialization_options: None, @@ -1805,7 +1808,7 @@ impl LspAdapter for Arc { unreachable!(); } - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + fn process_diagnostics(&self, _: &mut lsp2::PublishDiagnosticsParams) {} async fn disk_based_diagnostic_sources(&self) -> Vec { self.disk_based_diagnostics_sources.clone() @@ -1835,22 +1838,22 @@ fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option)]) } } -pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { - lsp::Position::new(point.row, point.column) +pub fn point_to_lsp(point: PointUtf16) -> lsp2::Position { + lsp2::Position::new(point.row, point.column) } -pub fn point_from_lsp(point: lsp::Position) -> Unclipped { +pub fn point_from_lsp(point: lsp2::Position) -> Unclipped { Unclipped(PointUtf16::new(point.line, point.character)) } -pub fn range_to_lsp(range: Range) -> lsp::Range { - lsp::Range { +pub fn range_to_lsp(range: Range) -> lsp2::Range { + lsp2::Range { start: point_to_lsp(range.start), end: point_to_lsp(range.end), } } -pub fn range_from_lsp(range: lsp::Range) -> Range> { +pub fn range_from_lsp(range: lsp2::Range) -> Range> { let mut start = point_from_lsp(range.start); let mut end = point_from_lsp(range.end); if start > end { diff --git a/crates/language2/src/outline.rs b/crates/language2/src/outline.rs index 47c29f57bd0debd69a4109382402c909ad940f3c..dd3a4acf6bd5906dfde6dd908f298b02ed70c743 100644 --- a/crates/language2/src/outline.rs +++ b/crates/language2/src/outline.rs @@ -1,6 +1,6 @@ -use fuzzy::{StringMatch, StringMatchCandidate}; +use fuzzy2::{StringMatch, StringMatchCandidate}; use gpui2::{Executor, HighlightStyle}; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; #[derive(Debug)] pub struct Outline { @@ -61,7 +61,7 @@ impl Outline { let query = query.trim_start(); let is_path_query = query.contains(' '); let smart_case = query.chars().any(|c| c.is_uppercase()); - let mut matches = fuzzy::match_strings( + let mut matches = fuzzy2::match_strings( if is_path_query { &self.path_candidates } else { diff --git a/crates/language2/src/proto.rs b/crates/language2/src/proto.rs index c4abe39d4782aafbe90594e3a0bc5de70787fa03..e23711e32867cafb74c9acfde54b2617bd67ac96 100644 --- a/crates/language2/src/proto.rs +++ b/crates/language2/src/proto.rs @@ -4,7 +4,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use clock::ReplicaId; -use lsp::{DiagnosticSeverity, LanguageServerId}; +use lsp2::{DiagnosticSeverity, LanguageServerId}; use rpc::proto; use std::{ops::Range, sync::Arc}; use text::*; diff --git a/crates/lsp2/Cargo.toml b/crates/lsp2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..a32dd2b6b28307d9bdb2ee280a5038aa05669542 --- /dev/null +++ b/crates/lsp2/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "lsp2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/lsp2.rs" +doctest = false + +[features] +test-support = ["async-pipe"] + +[dependencies] +collections = { path = "../collections" } +gpui2 = { path = "../gpui2" } +util = { path = "../util" } + +anyhow.workspace = true +async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true } +futures.workspace = true +log.workspace = true +lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" } +parking_lot.workspace = true +postage.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +smol.workspace = true + +[dev-dependencies] +gpui2 = { path = "../gpui2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } + +async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } +ctor.workspace = true +env_logger.workspace = true +unindent.workspace = true diff --git a/crates/lsp2/src/lsp2.rs b/crates/lsp2/src/lsp2.rs new file mode 100644 index 0000000000000000000000000000000000000000..f874d9f118d763b88b767b93829f061c6a937cb1 --- /dev/null +++ b/crates/lsp2/src/lsp2.rs @@ -0,0 +1,1167 @@ +use log::warn; +pub use lsp_types::request::*; +pub use lsp_types::*; + +use anyhow::{anyhow, Context, Result}; +use collections::HashMap; +use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt}; +use gpui2::{AsyncAppContext, Executor, Task}; +use parking_lot::Mutex; +use postage::{barrier, prelude::Stream}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::{json, value::RawValue, Value}; +use smol::{ + channel, + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, + process::{self, Child}, +}; +use std::{ + ffi::OsString, + fmt, + future::Future, + io::Write, + path::PathBuf, + str::{self, FromStr as _}, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, Weak, + }, + time::{Duration, Instant}, +}; +use std::{path::Path, process::Stdio}; +use util::{ResultExt, TryFutureExt}; + +const JSON_RPC_VERSION: &str = "2.0"; +const CONTENT_LEN_HEADER: &str = "Content-Length: "; +const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); + +type NotificationHandler = Box, &str, AsyncAppContext)>; +type ResponseHandler = Box)>; +type IoHandler = Box; + +#[derive(Debug, Clone, Copy)] +pub enum IoKind { + StdOut, + StdIn, + StdErr, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LanguageServerBinary { + pub path: PathBuf, + pub arguments: Vec, +} + +pub struct LanguageServer { + server_id: LanguageServerId, + next_id: AtomicUsize, + outbound_tx: channel::Sender, + name: String, + capabilities: ServerCapabilities, + code_action_kinds: Option>, + notification_handlers: Arc>>, + response_handlers: Arc>>>, + io_handlers: Arc>>, + executor: Executor, + #[allow(clippy::type_complexity)] + io_tasks: Mutex>, Task>)>>, + output_done_rx: Mutex>, + root_path: PathBuf, + _server: Option>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct LanguageServerId(pub usize); + +pub enum Subscription { + Notification { + method: &'static str, + notification_handlers: Option>>>, + }, + Io { + id: usize, + io_handlers: Option>>>, + }, +} + +#[derive(Serialize, Deserialize)] +pub struct Request<'a, T> { + jsonrpc: &'static str, + id: usize, + method: &'a str, + params: T, +} + +#[derive(Serialize, Deserialize)] +struct AnyResponse<'a> { + jsonrpc: &'a str, + id: usize, + #[serde(default)] + error: Option, + #[serde(borrow)] + result: Option<&'a RawValue>, +} + +#[derive(Serialize)] +struct Response { + jsonrpc: &'static str, + id: usize, + result: Option, + error: Option, +} + +#[derive(Serialize, Deserialize)] +struct Notification<'a, T> { + jsonrpc: &'static str, + #[serde(borrow)] + method: &'a str, + params: T, +} + +#[derive(Debug, Clone, Deserialize)] +struct AnyNotification<'a> { + #[serde(default)] + id: Option, + #[serde(borrow)] + method: &'a str, + #[serde(borrow, default)] + params: Option<&'a RawValue>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Error { + message: String, +} + +impl LanguageServer { + pub fn new( + server_id: LanguageServerId, + binary: LanguageServerBinary, + root_path: &Path, + code_action_kinds: Option>, + cx: AsyncAppContext, + ) -> Result { + let working_dir = if root_path.is_dir() { + root_path + } else { + root_path.parent().unwrap_or_else(|| Path::new("/")) + }; + + let mut server = process::Command::new(&binary.path) + .current_dir(working_dir) + .args(binary.arguments) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + let stdin = server.stdin.take().unwrap(); + let stdout = server.stdout.take().unwrap(); + let stderr = server.stderr.take().unwrap(); + let mut server = Self::new_internal( + server_id.clone(), + stdin, + stdout, + Some(stderr), + Some(server), + root_path, + code_action_kinds, + cx, + move |notification| { + log::info!( + "{} unhandled notification {}:\n{}", + server_id, + notification.method, + serde_json::to_string_pretty( + ¬ification + .params + .and_then(|params| Value::from_str(params.get()).ok()) + .unwrap_or(Value::Null) + ) + .unwrap(), + ); + }, + ); + + if let Some(name) = binary.path.file_name() { + server.name = name.to_string_lossy().to_string(); + } + + Ok(server) + } + + fn new_internal( + server_id: LanguageServerId, + stdin: Stdin, + stdout: Stdout, + stderr: Option, + server: Option, + root_path: &Path, + code_action_kinds: Option>, + cx: AsyncAppContext, + on_unhandled_notification: F, + ) -> Self + where + Stdin: AsyncWrite + Unpin + Send + 'static, + Stdout: AsyncRead + Unpin + Send + 'static, + Stderr: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send + Sync + Clone, + { + let (outbound_tx, outbound_rx) = channel::unbounded::(); + let (output_done_tx, output_done_rx) = barrier::channel(); + let notification_handlers = + Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); + let response_handlers = + Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default()))); + let io_handlers = Arc::new(Mutex::new(HashMap::default())); + + let stdout_input_task = cx.spawn({ + let on_unhandled_notification = on_unhandled_notification.clone(); + let notification_handlers = notification_handlers.clone(); + let response_handlers = response_handlers.clone(); + let io_handlers = io_handlers.clone(); + move |cx| { + Self::handle_input( + stdout, + on_unhandled_notification, + notification_handlers, + response_handlers, + io_handlers, + cx, + ) + .log_err() + } + }); + let stderr_input_task = stderr + .map(|stderr| { + let io_handlers = io_handlers.clone(); + cx.spawn(|_| Self::handle_stderr(stderr, io_handlers).log_err()) + }) + .unwrap_or_else(|| Task::Ready(Some(None))); + let input_task = cx.spawn(|_| async move { + let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); + stdout.or(stderr) + }); + let output_task = cx.executor().spawn({ + Self::handle_output( + stdin, + outbound_rx, + output_done_tx, + response_handlers.clone(), + io_handlers.clone(), + ) + .log_err() + }); + + Self { + server_id, + notification_handlers, + response_handlers, + io_handlers, + name: Default::default(), + capabilities: Default::default(), + code_action_kinds, + next_id: Default::default(), + outbound_tx, + executor: cx.executor().clone(), + io_tasks: Mutex::new(Some((input_task, output_task))), + output_done_rx: Mutex::new(Some(output_done_rx)), + root_path: root_path.to_path_buf(), + _server: server.map(|server| Mutex::new(server)), + } + } + + pub fn code_action_kinds(&self) -> Option> { + self.code_action_kinds.clone() + } + + async fn handle_input( + stdout: Stdout, + mut on_unhandled_notification: F, + notification_handlers: Arc>>, + response_handlers: Arc>>>, + io_handlers: Arc>>, + cx: AsyncAppContext, + ) -> anyhow::Result<()> + where + Stdout: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send, + { + let mut stdout = BufReader::new(stdout); + let _clear_response_handlers = util::defer({ + let response_handlers = response_handlers.clone(); + move || { + response_handlers.lock().take(); + } + }); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stdout.read_until(b'\n', &mut buffer).await?; + stdout.read_until(b'\n', &mut buffer).await?; + let header = std::str::from_utf8(&buffer)?; + let message_len: usize = header + .strip_prefix(CONTENT_LEN_HEADER) + .ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))? + .trim_end() + .parse()?; + + buffer.resize(message_len, 0); + stdout.read_exact(&mut buffer).await?; + + if let Ok(message) = str::from_utf8(&buffer) { + log::trace!("incoming message: {}", message); + for handler in io_handlers.lock().values_mut() { + handler(IoKind::StdOut, message); + } + } + + if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { + handler( + msg.id, + &msg.params.map(|params| params.get()).unwrap_or("null"), + cx.clone(), + ); + } else { + on_unhandled_notification(msg); + } + } else if let Ok(AnyResponse { + id, error, result, .. + }) = serde_json::from_slice(&buffer) + { + if let Some(handler) = response_handlers + .lock() + .as_mut() + .and_then(|handlers| handlers.remove(&id)) + { + if let Some(error) = error { + handler(Err(error)); + } else if let Some(result) = result { + handler(Ok(result.get().into())); + } else { + handler(Ok("null".into())); + } + } + } else { + warn!( + "failed to deserialize LSP message:\n{}", + std::str::from_utf8(&buffer)? + ); + } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; + } + } + + async fn handle_stderr( + stderr: Stderr, + io_handlers: Arc>>, + ) -> anyhow::Result<()> + where + Stderr: AsyncRead + Unpin + Send + 'static, + { + let mut stderr = BufReader::new(stderr); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stderr.read_until(b'\n', &mut buffer).await?; + if let Ok(message) = str::from_utf8(&buffer) { + log::trace!("incoming stderr message:{message}"); + for handler in io_handlers.lock().values_mut() { + handler(IoKind::StdErr, message); + } + } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; + } + } + + async fn handle_output( + stdin: Stdin, + outbound_rx: channel::Receiver, + output_done_tx: barrier::Sender, + response_handlers: Arc>>>, + io_handlers: Arc>>, + ) -> anyhow::Result<()> + where + Stdin: AsyncWrite + Unpin + Send + 'static, + { + let mut stdin = BufWriter::new(stdin); + let _clear_response_handlers = util::defer({ + let response_handlers = response_handlers.clone(); + move || { + response_handlers.lock().take(); + } + }); + let mut content_len_buffer = Vec::new(); + while let Ok(message) = outbound_rx.recv().await { + log::trace!("outgoing message:{}", message); + for handler in io_handlers.lock().values_mut() { + handler(IoKind::StdIn, &message); + } + + content_len_buffer.clear(); + write!(content_len_buffer, "{}", message.len()).unwrap(); + stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; + stdin.write_all(&content_len_buffer).await?; + stdin.write_all("\r\n\r\n".as_bytes()).await?; + stdin.write_all(message.as_bytes()).await?; + stdin.flush().await?; + } + drop(output_done_tx); + Ok(()) + } + + /// Initializes a language server. + /// Note that `options` is used directly to construct [`InitializeParams`], + /// which is why it is owned. + pub async fn initialize(mut self, options: Option) -> Result> { + let root_uri = Url::from_file_path(&self.root_path).unwrap(); + #[allow(deprecated)] + let params = InitializeParams { + process_id: Default::default(), + root_path: Default::default(), + root_uri: Some(root_uri.clone()), + initialization_options: options, + capabilities: ClientCapabilities { + workspace: Some(WorkspaceClientCapabilities { + configuration: Some(true), + did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities { + dynamic_registration: Some(true), + relative_pattern_support: Some(true), + }), + did_change_configuration: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: Some(true), + }), + workspace_folders: Some(true), + symbol: Some(WorkspaceSymbolClientCapabilities { + resolve_support: None, + ..WorkspaceSymbolClientCapabilities::default() + }), + inlay_hint: Some(InlayHintWorkspaceClientCapabilities { + refresh_support: Some(true), + }), + ..Default::default() + }), + text_document: Some(TextDocumentClientCapabilities { + definition: Some(GotoCapability { + link_support: Some(true), + ..Default::default() + }), + code_action: Some(CodeActionClientCapabilities { + code_action_literal_support: Some(CodeActionLiteralSupport { + code_action_kind: CodeActionKindLiteralSupport { + value_set: vec![ + CodeActionKind::REFACTOR.as_str().into(), + CodeActionKind::QUICKFIX.as_str().into(), + CodeActionKind::SOURCE.as_str().into(), + ], + }, + }), + data_support: Some(true), + resolve_support: Some(CodeActionCapabilityResolveSupport { + properties: vec!["edit".to_string(), "command".to_string()], + }), + ..Default::default() + }), + completion: Some(CompletionClientCapabilities { + completion_item: Some(CompletionItemCapability { + snippet_support: Some(true), + resolve_support: Some(CompletionItemCapabilityResolveSupport { + properties: vec!["additionalTextEdits".to_string()], + }), + ..Default::default() + }), + completion_list: Some(CompletionListCapability { + item_defaults: Some(vec![ + "commitCharacters".to_owned(), + "editRange".to_owned(), + "insertTextMode".to_owned(), + "data".to_owned(), + ]), + }), + ..Default::default() + }), + rename: Some(RenameClientCapabilities { + prepare_support: Some(true), + ..Default::default() + }), + hover: Some(HoverClientCapabilities { + content_format: Some(vec![MarkupKind::Markdown]), + ..Default::default() + }), + inlay_hint: Some(InlayHintClientCapabilities { + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec![ + "textEdits".to_string(), + "tooltip".to_string(), + "label.tooltip".to_string(), + "label.location".to_string(), + "label.command".to_string(), + ], + }), + dynamic_registration: Some(false), + }), + ..Default::default() + }), + experimental: Some(json!({ + "serverStatusNotification": true, + })), + window: Some(WindowClientCapabilities { + work_done_progress: Some(true), + ..Default::default() + }), + ..Default::default() + }, + trace: Default::default(), + workspace_folders: Some(vec![WorkspaceFolder { + uri: root_uri, + name: Default::default(), + }]), + client_info: Default::default(), + locale: Default::default(), + }; + + let response = self.request::(params).await?; + if let Some(info) = response.server_info { + self.name = info.name; + } + self.capabilities = response.capabilities; + + self.notify::(InitializedParams {})?; + Ok(Arc::new(self)) + } + + pub fn shutdown(&self) -> Option>> { + if let Some(tasks) = self.io_tasks.lock().take() { + let response_handlers = self.response_handlers.clone(); + let next_id = AtomicUsize::new(self.next_id.load(SeqCst)); + let outbound_tx = self.outbound_tx.clone(); + let executor = self.executor.clone(); + let mut output_done = self.output_done_rx.lock().take().unwrap(); + let shutdown_request = Self::request_internal::( + &next_id, + &response_handlers, + &outbound_tx, + &executor, + (), + ); + let exit = Self::notify_internal::(&outbound_tx, ()); + outbound_tx.close(); + Some( + async move { + log::debug!("language server shutdown started"); + shutdown_request.await?; + response_handlers.lock().take(); + exit?; + output_done.recv().await; + log::debug!("language server shutdown finished"); + drop(tasks); + anyhow::Ok(()) + } + .log_err(), + ) + } else { + None + } + } + + #[must_use] + pub fn on_notification(&self, f: F) -> Subscription + where + T: notification::Notification, + F: 'static + Send + FnMut(T::Params, AsyncAppContext), + { + self.on_custom_notification(T::METHOD, f) + } + + #[must_use] + pub fn on_request(&self, f: F) -> Subscription + where + T: request::Request, + T::Params: 'static + Send, + F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut, + Fut: 'static + Future> + Send, + { + self.on_custom_request(T::METHOD, f) + } + + #[must_use] + pub fn on_io(&self, f: F) -> Subscription + where + F: 'static + Send + FnMut(IoKind, &str), + { + let id = self.next_id.fetch_add(1, SeqCst); + self.io_handlers.lock().insert(id, Box::new(f)); + Subscription::Io { + id, + io_handlers: Some(Arc::downgrade(&self.io_handlers)), + } + } + + pub fn remove_request_handler(&self) { + self.notification_handlers.lock().remove(T::METHOD); + } + + pub fn remove_notification_handler(&self) { + self.notification_handlers.lock().remove(T::METHOD); + } + + pub fn has_notification_handler(&self) -> bool { + self.notification_handlers.lock().contains_key(T::METHOD) + } + + #[must_use] + pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription + where + F: 'static + Send + FnMut(Params, AsyncAppContext), + Params: DeserializeOwned, + { + let prev_handler = self.notification_handlers.lock().insert( + method, + Box::new(move |_, params, cx| { + if let Some(params) = serde_json::from_str(params).log_err() { + f(params, cx); + } + }), + ); + assert!( + prev_handler.is_none(), + "registered multiple handlers for the same LSP method" + ); + Subscription::Notification { + method, + notification_handlers: Some(self.notification_handlers.clone()), + } + } + + #[must_use] + pub fn on_custom_request( + &self, + method: &'static str, + mut f: F, + ) -> Subscription + where + F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut, + Fut: 'static + Future> + Send, + Params: DeserializeOwned + Send + 'static, + Res: Serialize, + { + let outbound_tx = self.outbound_tx.clone(); + let prev_handler = self.notification_handlers.lock().insert( + method, + Box::new(move |id, params, cx| { + if let Some(id) = id { + match serde_json::from_str(params) { + Ok(params) => { + let response = f(params, cx.clone()); + cx.executor() + .spawn_on_main({ + let outbound_tx = outbound_tx.clone(); + move || async move { + let response = match response.await { + Ok(result) => Response { + jsonrpc: JSON_RPC_VERSION, + id, + result: Some(result), + error: None, + }, + Err(error) => Response { + jsonrpc: JSON_RPC_VERSION, + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }, + }; + if let Some(response) = + serde_json::to_string(&response).log_err() + { + outbound_tx.try_send(response).ok(); + } + } + }) + .detach(); + } + + Err(error) => { + log::error!( + "error deserializing {} request: {:?}, message: {:?}", + method, + error, + params + ); + let response = AnyResponse { + jsonrpc: JSON_RPC_VERSION, + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }; + if let Some(response) = serde_json::to_string(&response).log_err() { + outbound_tx.try_send(response).ok(); + } + } + } + } + }), + ); + assert!( + prev_handler.is_none(), + "registered multiple handlers for the same LSP method" + ); + Subscription::Notification { + method, + notification_handlers: Some(self.notification_handlers.clone()), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn capabilities(&self) -> &ServerCapabilities { + &self.capabilities + } + + pub fn server_id(&self) -> LanguageServerId { + self.server_id + } + + pub fn root_path(&self) -> &PathBuf { + &self.root_path + } + + pub fn request( + &self, + params: T::Params, + ) -> impl Future> + where + T::Result: 'static + Send, + { + Self::request_internal::( + &self.next_id, + &self.response_handlers, + &self.outbound_tx, + &self.executor, + params, + ) + } + + fn request_internal( + next_id: &AtomicUsize, + response_handlers: &Mutex>>, + outbound_tx: &channel::Sender, + executor: &Executor, + params: T::Params, + ) -> impl 'static + Future> + where + T::Result: 'static + Send, + { + let id = next_id.fetch_add(1, SeqCst); + let message = serde_json::to_string(&Request { + jsonrpc: JSON_RPC_VERSION, + id, + method: T::METHOD, + params, + }) + .unwrap(); + + let (tx, rx) = oneshot::channel(); + let handle_response = response_handlers + .lock() + .as_mut() + .ok_or_else(|| anyhow!("server shut down")) + .map(|handlers| { + let executor = executor.clone(); + handlers.insert( + id, + Box::new(move |result| { + executor + .spawn(async move { + let response = match result { + Ok(response) => serde_json::from_str(&response) + .context("failed to deserialize response"), + Err(error) => Err(anyhow!("{}", error.message)), + }; + _ = tx.send(response); + }) + .detach(); + }), + ); + }); + + let send = outbound_tx + .try_send(message) + .context("failed to write to language server's stdin"); + + let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse(); + let started = Instant::now(); + async move { + handle_response?; + send?; + + let method = T::METHOD; + futures::select! { + response = rx.fuse() => { + let elapsed = started.elapsed(); + log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}"); + response? + } + + _ = timeout => { + log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}"); + anyhow::bail!("LSP request timeout"); + } + } + } + } + + pub fn notify(&self, params: T::Params) -> Result<()> { + Self::notify_internal::(&self.outbound_tx, params) + } + + fn notify_internal( + outbound_tx: &channel::Sender, + params: T::Params, + ) -> Result<()> { + let message = serde_json::to_string(&Notification { + jsonrpc: JSON_RPC_VERSION, + method: T::METHOD, + params, + }) + .unwrap(); + outbound_tx.try_send(message)?; + Ok(()) + } +} + +impl Drop for LanguageServer { + fn drop(&mut self) { + if let Some(shutdown) = self.shutdown() { + self.executor.spawn(shutdown).detach(); + } + } +} + +impl Subscription { + pub fn detach(&mut self) { + match self { + Subscription::Notification { + notification_handlers, + .. + } => *notification_handlers = None, + Subscription::Io { io_handlers, .. } => *io_handlers = None, + } + } +} + +impl fmt::Display for LanguageServerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Debug for LanguageServer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LanguageServer") + .field("id", &self.server_id.0) + .field("name", &self.name) + .finish_non_exhaustive() + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + match self { + Subscription::Notification { + method, + notification_handlers, + } => { + if let Some(handlers) = notification_handlers { + handlers.lock().remove(method); + } + } + Subscription::Io { id, io_handlers } => { + if let Some(io_handlers) = io_handlers.as_ref().and_then(|h| h.upgrade()) { + io_handlers.lock().remove(id); + } + } + } + } +} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] +pub struct FakeLanguageServer { + pub server: Arc, + notifications_rx: channel::Receiver<(String, String)>, +} + +#[cfg(any(test, feature = "test-support"))] +impl LanguageServer { + pub fn full_capabilities() -> ServerCapabilities { + ServerCapabilities { + document_highlight_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + document_formatting_provider: Some(OneOf::Left(true)), + document_range_formatting_provider: Some(OneOf::Left(true)), + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), + ..Default::default() + } + } + + pub fn fake( + name: String, + capabilities: ServerCapabilities, + cx: AsyncAppContext, + ) -> (Self, FakeLanguageServer) { + let (stdin_writer, stdin_reader) = async_pipe::pipe(); + let (stdout_writer, stdout_reader) = async_pipe::pipe(); + let (notifications_tx, notifications_rx) = channel::unbounded(); + + let server = Self::new_internal( + LanguageServerId(0), + stdin_writer, + stdout_reader, + None::, + None, + Path::new("/"), + None, + cx.clone(), + |_| {}, + ); + let fake = FakeLanguageServer { + server: Arc::new(Self::new_internal( + LanguageServerId(0), + stdout_writer, + stdin_reader, + None::, + None, + Path::new("/"), + None, + cx, + move |msg| { + notifications_tx + .try_send(( + msg.method.to_string(), + msg.params + .map(|raw_value| raw_value.get()) + .unwrap_or("null") + .to_string(), + )) + .ok(); + }, + )), + notifications_rx, + }; + fake.handle_request::({ + let capabilities = capabilities; + move |_, _| { + let capabilities = capabilities.clone(); + let name = name.clone(); + async move { + Ok(InitializeResult { + capabilities, + server_info: Some(ServerInfo { + name, + ..Default::default() + }), + }) + } + } + }); + + (server, fake) + } +} + +#[cfg(any(test, feature = "test-support"))] +impl FakeLanguageServer { + pub fn notify(&self, params: T::Params) { + self.server.notify::(params).ok(); + } + + pub async fn request(&self, params: T::Params) -> Result + where + T: request::Request, + T::Result: 'static + Send, + { + self.server.executor.start_waiting(); + self.server.request::(params).await + } + + pub async fn receive_notification(&mut self) -> T::Params { + self.server.executor.start_waiting(); + self.try_receive_notification::().await.unwrap() + } + + pub async fn try_receive_notification( + &mut self, + ) -> Option { + use futures::StreamExt as _; + + loop { + let (method, params) = self.notifications_rx.next().await?; + if method == T::METHOD { + return Some(serde_json::from_str::(¶ms).unwrap()); + } else { + log::info!("skipping message in fake language server {:?}", params); + } + } + } + + pub fn handle_request( + &self, + mut handler: F, + ) -> futures::channel::mpsc::UnboundedReceiver<()> + where + T: 'static + request::Request, + T::Params: 'static + Send, + F: 'static + Send + FnMut(T::Params, gpui2::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future>, + { + let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); + self.server.remove_request_handler::(); + self.server + .on_request::(move |params, cx| { + let result = handler(params, cx.clone()); + let responded_tx = responded_tx.clone(); + async move { + cx.executor().simulate_random_delay().await; + let result = result.await; + responded_tx.unbounded_send(()).ok(); + result + } + }) + .detach(); + responded_rx + } + + pub fn handle_notification( + &self, + mut handler: F, + ) -> futures::channel::mpsc::UnboundedReceiver<()> + where + T: 'static + notification::Notification, + T::Params: 'static + Send, + F: 'static + Send + FnMut(T::Params, gpui2::AsyncAppContext), + { + let (handled_tx, handled_rx) = futures::channel::mpsc::unbounded(); + self.server.remove_notification_handler::(); + self.server + .on_notification::(move |params, cx| { + handler(params, cx.clone()); + handled_tx.unbounded_send(()).ok(); + }) + .detach(); + handled_rx + } + + pub fn remove_request_handler(&mut self) + where + T: 'static + request::Request, + { + self.server.remove_request_handler::(); + } + + pub async fn start_progress(&self, token: impl Into) { + let token = token.into(); + self.request::(WorkDoneProgressCreateParams { + token: NumberOrString::String(token.clone()), + }) + .await + .unwrap(); + self.notify::(ProgressParams { + token: NumberOrString::String(token), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())), + }); + } + + pub fn end_progress(&self, token: impl Into) { + self.notify::(ProgressParams { + token: NumberOrString::String(token.into()), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), + }); + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use gpui::TestAppContext; + +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// async fn test_fake(cx: &mut TestAppContext) { +// let (server, mut fake) = +// LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async()); + +// let (message_tx, message_rx) = channel::unbounded(); +// let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); +// server +// .on_notification::(move |params, _| { +// message_tx.try_send(params).unwrap() +// }) +// .detach(); +// server +// .on_notification::(move |params, _| { +// diagnostics_tx.try_send(params).unwrap() +// }) +// .detach(); + +// let server = server.initialize(None).await.unwrap(); +// server +// .notify::(DidOpenTextDocumentParams { +// text_document: TextDocumentItem::new( +// Url::from_str("file://a/b").unwrap(), +// "rust".to_string(), +// 0, +// "".to_string(), +// ), +// }) +// .unwrap(); +// assert_eq!( +// fake.receive_notification::() +// .await +// .text_document +// .uri +// .as_str(), +// "file://a/b" +// ); + +// fake.notify::(ShowMessageParams { +// typ: MessageType::ERROR, +// message: "ok".to_string(), +// }); +// fake.notify::(PublishDiagnosticsParams { +// uri: Url::from_str("file://b/c").unwrap(), +// version: Some(5), +// diagnostics: vec![], +// }); +// assert_eq!(message_rx.recv().await.unwrap().message, "ok"); +// assert_eq!( +// diagnostics_rx.recv().await.unwrap().uri.as_str(), +// "file://b/c" +// ); + +// fake.handle_request::(|_, _| async move { Ok(()) }); + +// drop(server); +// fake.receive_notification::().await; +// } +// } diff --git a/crates/theme2/Cargo.toml b/crates/theme2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ed4af6b6f300c9eebdb80bbdd863a2541ae0c270 --- /dev/null +++ b/crates/theme2/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "theme2" +version = "0.1.0" +edition = "2021" +publish = false + +[features] +test-support = [ + "gpui2/test-support", + "fs/test-support", + "settings2/test-support" +] + +[lib] +path = "src/theme2.rs" +doctest = false + +[dependencies] +gpui2 = { path = "../gpui2" } +fs = { path = "../fs" } +settings2 = { path = "../settings2" } +util = { path = "../util" } + +anyhow.workspace = true +indexmap = "1.6.2" +parking_lot.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +toml.workspace = true + +[dev-dependencies] +gpui2 = { path = "../gpui2", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } +settings2 = { path = "../settings2", features = ["test-support"] } diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs new file mode 100644 index 0000000000000000000000000000000000000000..5958d8f427317a8674cd9acda8a4ade2b90c78e4 --- /dev/null +++ b/crates/theme2/src/theme2.rs @@ -0,0 +1,21 @@ +use gpui2::HighlightStyle; +use std::sync::Arc; + +pub struct Theme { + pub editor: Editor, +} + +pub struct Editor { + pub syntax: Arc, +} + +#[derive(Default)] +pub struct SyntaxTheme { + pub highlights: Vec<(String, HighlightStyle)>, +} + +impl SyntaxTheme { + pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self { + Self { highlights } + } +} diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index c7ad62f15552bd6f6dfff1e3389eef4119234fc0..a0ec82d7a3720072384f6a003f798615f12b35f1 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -14,7 +14,7 @@ test-support = [] client = { path = "../client" } editor = { path = "../editor" } fs = { path = "../fs" } -fuzzy = { path = "../fuzzy" } +fuzzy = { path = "../fuzzy2" } gpui = { path = "../gpui" } db = { path = "../db" } install_cli = { path = "../install_cli" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 582607843162b2d9718889e248cf66a53a8a6618..23551e78112e7c44d383c72f44dbe2313860fd0c 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -108,7 +108,7 @@ fn main() { handle_settings_file_changes(user_settings_file_rx, cx); // handle_keymap_file_changes(user_keymap_file_rx, cx); - let client = client2::Client::new(http.clone(), cx); + // let client = client2::Client::new(http.clone(), cx); // let mut languages = LanguageRegistry::new(login_shell_env_loaded); // let copilot_language_server_id = languages.next_language_server_id(); // languages.set_executor(cx.background().clone());