Merge branch 'main' into notifications

Max Brunsfeld created

Change summary

Cargo.lock                                             |   2 
crates/ai/Cargo.toml                                   |   1 
crates/ai/src/ai.rs                                    |   2 
crates/ai/src/completion.rs                            |   2 
crates/ai/src/embedding.rs                             |  45 +
crates/ai/src/models.rs                                |  66 ++
crates/ai/src/templates/base.rs                        | 350 ++++++++++++
crates/ai/src/templates/file_context.rs                | 160 +++++
crates/ai/src/templates/generate.rs                    |  95 +++
crates/ai/src/templates/mod.rs                         |   5 
crates/ai/src/templates/preamble.rs                    |  52 +
crates/ai/src/templates/repository_context.rs          |  94 +++
crates/assistant/src/assistant_panel.rs                |  59 +
crates/assistant/src/prompts.rs                        | 225 +------
crates/collab/src/tests/integration_tests.rs           |  10 
crates/collab_ui/src/collab_titlebar_item.rs           |   6 
crates/collab_ui/src/collab_ui.rs                      |   2 
crates/collab_ui/src/sharing_status_indicator.rs       |  62 --
crates/editor/src/display_map.rs                       | 346 ++++++++--
crates/editor/src/editor.rs                            |  95 ++
crates/editor/src/editor_tests.rs                      |  44 
crates/editor/src/element.rs                           |  64 --
crates/editor/src/inlay_hint_cache.rs                  |  10 
crates/editor/src/movement.rs                          | 342 +++++++----
crates/editor/src/selections_collection.rs             |  25 
crates/gpui/src/text_layout.rs                         |  24 
crates/language/src/buffer.rs                          |   4 
crates/language/src/language.rs                        |  46 -
crates/prettier/src/prettier.rs                        |  50 -
crates/project/src/project.rs                          |  36 
crates/search/src/project_search.rs                    |  25 
crates/semantic_index/src/semantic_index.rs            |  20 
crates/text/src/selection.rs                           |   9 
crates/vcs_menu/Cargo.toml                             |   1 
crates/vcs_menu/src/lib.rs                             | 126 ++--
crates/vim/src/motion.rs                               | 151 +++--
crates/vim/src/normal.rs                               |  56 +
crates/vim/src/normal/change.rs                        |  31 
crates/vim/src/normal/delete.rs                        |   3 
crates/vim/src/normal/increment.rs                     |  12 
crates/vim/src/normal/paste.rs                         |  11 
crates/vim/src/normal/substitute.rs                    |  18 
crates/vim/src/normal/yank.rs                          |   3 
crates/vim/src/test.rs                                 |  57 +
crates/vim/src/vim.rs                                  |  13 
crates/vim/src/visual.rs                               |  97 ++
crates/vim/test_data/test_increment_steps.json         |   1 
crates/vim/test_data/test_j.json                       |   3 
crates/vim/test_data/test_visual_block_issue_2123.json |   5 
crates/vim/test_data/test_wrapped_motions.json         |  15 
crates/zed/examples/semantic_index_eval.rs             |  18 
crates/zed/src/languages.rs                            |  29 
crates/zed/src/languages/css.rs                        |   6 
crates/zed/src/languages/css/config.toml               |   1 
crates/zed/src/languages/elixir/config.toml            |   5 
crates/zed/src/languages/erb/config.toml               |   1 
crates/zed/src/languages/heex/config.toml              |   5 
crates/zed/src/languages/heex/overrides.scm            |   4 
crates/zed/src/languages/html.rs                       |   6 
crates/zed/src/languages/html/config.toml              |   1 
crates/zed/src/languages/javascript/config.toml        |   1 
crates/zed/src/languages/json.rs                       |   8 
crates/zed/src/languages/json/config.toml              |   1 
crates/zed/src/languages/php/config.toml               |   1 
crates/zed/src/languages/svelte.rs                     |   9 
crates/zed/src/languages/svelte/config.toml            |   8 
crates/zed/src/languages/svelte/overrides.scm          |   7 
crates/zed/src/languages/tailwind.rs                   |  29 
crates/zed/src/languages/tsx/config.toml               |   1 
crates/zed/src/languages/typescript.rs                 |  10 
crates/zed/src/languages/typescript/config.toml        |   1 
crates/zed/src/languages/yaml.rs                       |   7 
crates/zed/src/languages/yaml/config.toml              |   1 
73 files changed, 2,188 insertions(+), 953 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -91,6 +91,7 @@ dependencies = [
  "futures 0.3.28",
  "gpui",
  "isahc",
+ "language",
  "lazy_static",
  "log",
  "matrixmultiply",
@@ -9146,6 +9147,7 @@ name = "vcs_menu"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "fs",
  "fuzzy",
  "gpui",
  "picker",

crates/ai/Cargo.toml πŸ”—

@@ -11,6 +11,7 @@ doctest = false
 [dependencies]
 gpui = { path = "../gpui" }
 util = { path = "../util" }
+language = { path = "../language" }
 async-trait.workspace = true
 anyhow.workspace = true
 futures.workspace = true

crates/ai/src/completion.rs πŸ”—

@@ -53,6 +53,8 @@ pub struct OpenAIRequest {
     pub model: String,
     pub messages: Vec<RequestMessage>,
     pub stream: bool,
+    pub stop: Vec<String>,
+    pub temperature: f32,
 }
 
 #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]

crates/ai/src/embedding.rs πŸ”—

@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
 use gpui::executor::Background;
-use gpui::serde_json;
+use gpui::{serde_json, ViewContext};
 use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
@@ -20,9 +20,11 @@ use std::sync::Arc;
 use std::time::{Duration, Instant};
 use tiktoken_rs::{cl100k_base, CoreBPE};
 use util::http::{HttpClient, Request};
+use util::ResultExt;
+
+use crate::completion::OPENAI_API_URL;
 
 lazy_static! {
-    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
     static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
 }
 
@@ -87,6 +89,7 @@ impl Embedding {
 
 #[derive(Clone)]
 pub struct OpenAIEmbeddings {
+    pub api_key: Option<String>,
     pub client: Arc<dyn HttpClient>,
     pub executor: Arc<Background>,
     rate_limit_count_rx: watch::Receiver<Option<Instant>>,
@@ -166,11 +169,36 @@ impl EmbeddingProvider for DummyEmbeddings {
 const OPENAI_INPUT_LIMIT: usize = 8190;
 
 impl OpenAIEmbeddings {
-    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Background>) -> Self {
+    pub fn authenticate(&mut self, cx: &mut ViewContext<Self>) {
+        if self.api_key.is_none() {
+            let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+                Some(api_key)
+            } else if let Some((_, api_key)) = cx
+                .platform()
+                .read_credentials(OPENAI_API_URL)
+                .log_err()
+                .flatten()
+            {
+                String::from_utf8(api_key).log_err()
+            } else {
+                None
+            };
+
+            if let Some(api_key) = api_key {
+                self.api_key = Some(api_key);
+            }
+        }
+    }
+    pub fn new(
+        api_key: Option<String>,
+        client: Arc<dyn HttpClient>,
+        executor: Arc<Background>,
+    ) -> Self {
         let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
         let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
 
         OpenAIEmbeddings {
+            api_key,
             client,
             executor,
             rate_limit_count_rx,
@@ -237,8 +265,9 @@ impl OpenAIEmbeddings {
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
     fn is_authenticated(&self) -> bool {
-        OPENAI_API_KEY.as_ref().is_some()
+        self.api_key.is_some()
     }
+
     fn max_tokens_per_batch(&self) -> usize {
         50000
     }
@@ -265,9 +294,9 @@ impl EmbeddingProvider for OpenAIEmbeddings {
         const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
         const MAX_RETRIES: usize = 4;
 
-        let api_key = OPENAI_API_KEY
-            .as_ref()
-            .ok_or_else(|| anyhow!("no api key"))?;
+        let Some(api_key) = self.api_key.clone() else {
+            return Err(anyhow!("no open ai key provided"));
+        };
 
         let mut request_number = 0;
         let mut rate_limiting = false;
@@ -276,7 +305,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
         while request_number < MAX_RETRIES {
             response = self
                 .send_request(
-                    api_key,
+                    &api_key,
                     spans.iter().map(|x| &**x).collect(),
                     request_timeout,
                 )

crates/ai/src/models.rs πŸ”—

@@ -0,0 +1,66 @@
+use anyhow::anyhow;
+use tiktoken_rs::CoreBPE;
+use util::ResultExt;
+
+pub trait LanguageModel {
+    fn name(&self) -> String;
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
+    fn truncate(&self, content: &str, length: usize) -> anyhow::Result<String>;
+    fn truncate_start(&self, content: &str, length: usize) -> anyhow::Result<String>;
+    fn capacity(&self) -> anyhow::Result<usize>;
+}
+
+pub struct OpenAILanguageModel {
+    name: String,
+    bpe: Option<CoreBPE>,
+}
+
+impl OpenAILanguageModel {
+    pub fn load(model_name: &str) -> Self {
+        let bpe = tiktoken_rs::get_bpe_from_model(model_name).log_err();
+        OpenAILanguageModel {
+            name: model_name.to_string(),
+            bpe,
+        }
+    }
+}
+
+impl LanguageModel for OpenAILanguageModel {
+    fn name(&self) -> String {
+        self.name.clone()
+    }
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize> {
+        if let Some(bpe) = &self.bpe {
+            anyhow::Ok(bpe.encode_with_special_tokens(content).len())
+        } else {
+            Err(anyhow!("bpe for open ai model was not retrieved"))
+        }
+    }
+    fn truncate(&self, content: &str, length: usize) -> anyhow::Result<String> {
+        if let Some(bpe) = &self.bpe {
+            let tokens = bpe.encode_with_special_tokens(content);
+            if tokens.len() > length {
+                bpe.decode(tokens[..length].to_vec())
+            } else {
+                bpe.decode(tokens)
+            }
+        } else {
+            Err(anyhow!("bpe for open ai model was not retrieved"))
+        }
+    }
+    fn truncate_start(&self, content: &str, length: usize) -> anyhow::Result<String> {
+        if let Some(bpe) = &self.bpe {
+            let tokens = bpe.encode_with_special_tokens(content);
+            if tokens.len() > length {
+                bpe.decode(tokens[length..].to_vec())
+            } else {
+                bpe.decode(tokens)
+            }
+        } else {
+            Err(anyhow!("bpe for open ai model was not retrieved"))
+        }
+    }
+    fn capacity(&self) -> anyhow::Result<usize> {
+        anyhow::Ok(tiktoken_rs::model::get_context_size(&self.name))
+    }
+}

crates/ai/src/templates/base.rs πŸ”—

@@ -0,0 +1,350 @@
+use std::cmp::Reverse;
+use std::ops::Range;
+use std::sync::Arc;
+
+use language::BufferSnapshot;
+use util::ResultExt;
+
+use crate::models::LanguageModel;
+use crate::templates::repository_context::PromptCodeSnippet;
+
+pub(crate) enum PromptFileType {
+    Text,
+    Code,
+}
+
+// TODO: Set this up to manage for defaults well
+pub struct PromptArguments {
+    pub model: Arc<dyn LanguageModel>,
+    pub user_prompt: Option<String>,
+    pub language_name: Option<String>,
+    pub project_name: Option<String>,
+    pub snippets: Vec<PromptCodeSnippet>,
+    pub reserved_tokens: usize,
+    pub buffer: Option<BufferSnapshot>,
+    pub selected_range: Option<Range<usize>>,
+}
+
+impl PromptArguments {
+    pub(crate) fn get_file_type(&self) -> PromptFileType {
+        if self
+            .language_name
+            .as_ref()
+            .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str())))
+            .unwrap_or(true)
+        {
+            PromptFileType::Code
+        } else {
+            PromptFileType::Text
+        }
+    }
+}
+
+pub trait PromptTemplate {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)>;
+}
+
+#[repr(i8)]
+#[derive(PartialEq, Eq, Ord)]
+pub enum PromptPriority {
+    Mandatory,                // Ignores truncation
+    Ordered { order: usize }, // Truncates based on priority
+}
+
+impl PartialOrd for PromptPriority {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        match (self, other) {
+            (Self::Mandatory, Self::Mandatory) => Some(std::cmp::Ordering::Equal),
+            (Self::Mandatory, Self::Ordered { .. }) => Some(std::cmp::Ordering::Greater),
+            (Self::Ordered { .. }, Self::Mandatory) => Some(std::cmp::Ordering::Less),
+            (Self::Ordered { order: a }, Self::Ordered { order: b }) => b.partial_cmp(a),
+        }
+    }
+}
+
+pub struct PromptChain {
+    args: PromptArguments,
+    templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)>,
+}
+
+impl PromptChain {
+    pub fn new(
+        args: PromptArguments,
+        templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)>,
+    ) -> Self {
+        PromptChain { args, templates }
+    }
+
+    pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> {
+        // Argsort based on Prompt Priority
+        let seperator = "\n";
+        let seperator_tokens = self.args.model.count_tokens(seperator)?;
+        let mut sorted_indices = (0..self.templates.len()).collect::<Vec<_>>();
+        sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0));
+
+        // If Truncate
+        let mut tokens_outstanding = if truncate {
+            Some(self.args.model.capacity()? - self.args.reserved_tokens)
+        } else {
+            None
+        };
+
+        let mut prompts = vec!["".to_string(); sorted_indices.len()];
+        for idx in sorted_indices {
+            let (_, template) = &self.templates[idx];
+
+            if let Some((template_prompt, prompt_token_count)) =
+                template.generate(&self.args, tokens_outstanding).log_err()
+            {
+                if template_prompt != "" {
+                    prompts[idx] = template_prompt;
+
+                    if let Some(remaining_tokens) = tokens_outstanding {
+                        let new_tokens = prompt_token_count + seperator_tokens;
+                        tokens_outstanding = if remaining_tokens > new_tokens {
+                            Some(remaining_tokens - new_tokens)
+                        } else {
+                            Some(0)
+                        };
+                    }
+                }
+            }
+        }
+
+        prompts.retain(|x| x != "");
+
+        let full_prompt = prompts.join(seperator);
+        let total_token_count = self.args.model.count_tokens(&full_prompt)?;
+        anyhow::Ok((prompts.join(seperator), total_token_count))
+    }
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+
+    #[test]
+    pub fn test_prompt_chain() {
+        struct TestPromptTemplate {}
+        impl PromptTemplate for TestPromptTemplate {
+            fn generate(
+                &self,
+                args: &PromptArguments,
+                max_token_length: Option<usize>,
+            ) -> anyhow::Result<(String, usize)> {
+                let mut content = "This is a test prompt template".to_string();
+
+                let mut token_count = args.model.count_tokens(&content)?;
+                if let Some(max_token_length) = max_token_length {
+                    if token_count > max_token_length {
+                        content = args.model.truncate(&content, max_token_length)?;
+                        token_count = max_token_length;
+                    }
+                }
+
+                anyhow::Ok((content, token_count))
+            }
+        }
+
+        struct TestLowPriorityTemplate {}
+        impl PromptTemplate for TestLowPriorityTemplate {
+            fn generate(
+                &self,
+                args: &PromptArguments,
+                max_token_length: Option<usize>,
+            ) -> anyhow::Result<(String, usize)> {
+                let mut content = "This is a low priority test prompt template".to_string();
+
+                let mut token_count = args.model.count_tokens(&content)?;
+                if let Some(max_token_length) = max_token_length {
+                    if token_count > max_token_length {
+                        content = args.model.truncate(&content, max_token_length)?;
+                        token_count = max_token_length;
+                    }
+                }
+
+                anyhow::Ok((content, token_count))
+            }
+        }
+
+        #[derive(Clone)]
+        struct DummyLanguageModel {
+            capacity: usize,
+        }
+
+        impl LanguageModel for DummyLanguageModel {
+            fn name(&self) -> String {
+                "dummy".to_string()
+            }
+            fn count_tokens(&self, content: &str) -> anyhow::Result<usize> {
+                anyhow::Ok(content.chars().collect::<Vec<char>>().len())
+            }
+            fn truncate(&self, content: &str, length: usize) -> anyhow::Result<String> {
+                anyhow::Ok(
+                    content.chars().collect::<Vec<char>>()[..length]
+                        .into_iter()
+                        .collect::<String>(),
+                )
+            }
+            fn truncate_start(&self, content: &str, length: usize) -> anyhow::Result<String> {
+                anyhow::Ok(
+                    content.chars().collect::<Vec<char>>()[length..]
+                        .into_iter()
+                        .collect::<String>(),
+                )
+            }
+            fn capacity(&self) -> anyhow::Result<usize> {
+                anyhow::Ok(self.capacity)
+            }
+        }
+
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity: 100 });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens: 0,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(false).unwrap();
+
+        assert_eq!(
+            prompt,
+            "This is a test prompt template\nThis is a low priority test prompt template"
+                .to_string()
+        );
+
+        assert_eq!(model.count_tokens(&prompt).unwrap(), token_count);
+
+        // Testing with Truncation Off
+        // Should ignore capacity and return all prompts
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity: 20 });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens: 0,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(false).unwrap();
+
+        assert_eq!(
+            prompt,
+            "This is a test prompt template\nThis is a low priority test prompt template"
+                .to_string()
+        );
+
+        assert_eq!(model.count_tokens(&prompt).unwrap(), token_count);
+
+        // Testing with Truncation Off
+        // Should ignore capacity and return all prompts
+        let capacity = 20;
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens: 0,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 2 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(true).unwrap();
+
+        assert_eq!(prompt, "This is a test promp".to_string());
+        assert_eq!(token_count, capacity);
+
+        // Change Ordering of Prompts Based on Priority
+        let capacity = 120;
+        let reserved_tokens = 10;
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Mandatory,
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(true).unwrap();
+
+        assert_eq!(
+            prompt,
+            "This is a low priority test prompt template\nThis is a test prompt template\nThis is a low priority test prompt "
+                .to_string()
+        );
+        assert_eq!(token_count, capacity - reserved_tokens);
+    }
+}

crates/ai/src/templates/file_context.rs πŸ”—

@@ -0,0 +1,160 @@
+use anyhow::anyhow;
+use language::BufferSnapshot;
+use language::ToOffset;
+
+use crate::models::LanguageModel;
+use crate::templates::base::PromptArguments;
+use crate::templates::base::PromptTemplate;
+use std::fmt::Write;
+use std::ops::Range;
+use std::sync::Arc;
+
+fn retrieve_context(
+    buffer: &BufferSnapshot,
+    selected_range: &Option<Range<usize>>,
+    model: Arc<dyn LanguageModel>,
+    max_token_count: Option<usize>,
+) -> anyhow::Result<(String, usize, bool)> {
+    let mut prompt = String::new();
+    let mut truncated = false;
+    if let Some(selected_range) = selected_range {
+        let start = selected_range.start.to_offset(buffer);
+        let end = selected_range.end.to_offset(buffer);
+
+        let start_window = buffer.text_for_range(0..start).collect::<String>();
+
+        let mut selected_window = String::new();
+        if start == end {
+            write!(selected_window, "<|START|>").unwrap();
+        } else {
+            write!(selected_window, "<|START|").unwrap();
+        }
+
+        write!(
+            selected_window,
+            "{}",
+            buffer.text_for_range(start..end).collect::<String>()
+        )
+        .unwrap();
+
+        if start != end {
+            write!(selected_window, "|END|>").unwrap();
+        }
+
+        let end_window = buffer.text_for_range(end..buffer.len()).collect::<String>();
+
+        if let Some(max_token_count) = max_token_count {
+            let selected_tokens = model.count_tokens(&selected_window)?;
+            if selected_tokens > max_token_count {
+                return Err(anyhow!(
+                    "selected range is greater than model context window, truncation not possible"
+                ));
+            };
+
+            let mut remaining_tokens = max_token_count - selected_tokens;
+            let start_window_tokens = model.count_tokens(&start_window)?;
+            let end_window_tokens = model.count_tokens(&end_window)?;
+            let outside_tokens = start_window_tokens + end_window_tokens;
+            if outside_tokens > remaining_tokens {
+                let (start_goal_tokens, end_goal_tokens) =
+                    if start_window_tokens < end_window_tokens {
+                        let start_goal_tokens = (remaining_tokens / 2).min(start_window_tokens);
+                        remaining_tokens -= start_goal_tokens;
+                        let end_goal_tokens = remaining_tokens.min(end_window_tokens);
+                        (start_goal_tokens, end_goal_tokens)
+                    } else {
+                        let end_goal_tokens = (remaining_tokens / 2).min(end_window_tokens);
+                        remaining_tokens -= end_goal_tokens;
+                        let start_goal_tokens = remaining_tokens.min(start_window_tokens);
+                        (start_goal_tokens, end_goal_tokens)
+                    };
+
+                let truncated_start_window =
+                    model.truncate_start(&start_window, start_goal_tokens)?;
+                let truncated_end_window = model.truncate(&end_window, end_goal_tokens)?;
+                writeln!(
+                    prompt,
+                    "{truncated_start_window}{selected_window}{truncated_end_window}"
+                )
+                .unwrap();
+                truncated = true;
+            } else {
+                writeln!(prompt, "{start_window}{selected_window}{end_window}").unwrap();
+            }
+        } else {
+            // If we dont have a selected range, include entire file.
+            writeln!(prompt, "{}", &buffer.text()).unwrap();
+
+            // Dumb truncation strategy
+            if let Some(max_token_count) = max_token_count {
+                if model.count_tokens(&prompt)? > max_token_count {
+                    truncated = true;
+                    prompt = model.truncate(&prompt, max_token_count)?;
+                }
+            }
+        }
+    }
+
+    let token_count = model.count_tokens(&prompt)?;
+    anyhow::Ok((prompt, token_count, truncated))
+}
+
+pub struct FileContext {}
+
+impl PromptTemplate for FileContext {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        if let Some(buffer) = &args.buffer {
+            let mut prompt = String::new();
+            // Add Initial Preamble
+            // TODO: Do we want to add the path in here?
+            writeln!(
+                prompt,
+                "The file you are currently working on has the following content:"
+            )
+            .unwrap();
+
+            let language_name = args
+                .language_name
+                .clone()
+                .unwrap_or("".to_string())
+                .to_lowercase();
+
+            let (context, _, truncated) = retrieve_context(
+                buffer,
+                &args.selected_range,
+                args.model.clone(),
+                max_token_length,
+            )?;
+            writeln!(prompt, "```{language_name}\n{context}\n```").unwrap();
+
+            if truncated {
+                writeln!(prompt, "Note the content has been truncated and only represents a portion of the file.").unwrap();
+            }
+
+            if let Some(selected_range) = &args.selected_range {
+                let start = selected_range.start.to_offset(buffer);
+                let end = selected_range.end.to_offset(buffer);
+
+                if start == end {
+                    writeln!(prompt, "In particular, the user's cursor is currently on the '<|START|>' span in the above content, with no text selected.").unwrap();
+                } else {
+                    writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
+                }
+            }
+
+            // Really dumb truncation strategy
+            if let Some(max_tokens) = max_token_length {
+                prompt = args.model.truncate(&prompt, max_tokens)?;
+            }
+
+            let token_count = args.model.count_tokens(&prompt)?;
+            anyhow::Ok((prompt, token_count))
+        } else {
+            Err(anyhow!("no buffer provided to retrieve file context from"))
+        }
+    }
+}

crates/ai/src/templates/generate.rs πŸ”—

@@ -0,0 +1,95 @@
+use crate::templates::base::{PromptArguments, PromptFileType, PromptTemplate};
+use anyhow::anyhow;
+use std::fmt::Write;
+
+pub fn capitalize(s: &str) -> String {
+    let mut c = s.chars();
+    match c.next() {
+        None => String::new(),
+        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
+    }
+}
+
+pub struct GenerateInlineContent {}
+
+impl PromptTemplate for GenerateInlineContent {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        let Some(user_prompt) = &args.user_prompt else {
+            return Err(anyhow!("user prompt not provided"));
+        };
+
+        let file_type = args.get_file_type();
+        let content_type = match &file_type {
+            PromptFileType::Code => "code",
+            PromptFileType::Text => "text",
+        };
+
+        let mut prompt = String::new();
+
+        if let Some(selected_range) = &args.selected_range {
+            if selected_range.start == selected_range.end {
+                writeln!(
+                    prompt,
+                    "Assume the cursor is located where the `<|START|>` span is."
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "{} can't be replaced, so assume your answer will be inserted at the cursor.",
+                    capitalize(content_type)
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "Generate {content_type} based on the users prompt: {user_prompt}",
+                )
+                .unwrap();
+            } else {
+                writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
+                writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
+                writeln!(prompt, "Double check that you only return code and not the '<|START|' and '|END|'> spans").unwrap();
+            }
+        } else {
+            writeln!(
+                prompt,
+                "Generate {content_type} based on the users prompt: {user_prompt}"
+            )
+            .unwrap();
+        }
+
+        if let Some(language_name) = &args.language_name {
+            writeln!(
+                prompt,
+                "Your answer MUST always and only be valid {}.",
+                language_name
+            )
+            .unwrap();
+        }
+        writeln!(prompt, "Never make remarks about the output.").unwrap();
+        writeln!(
+            prompt,
+            "Do not return anything else, except the generated {content_type}."
+        )
+        .unwrap();
+
+        match file_type {
+            PromptFileType::Code => {
+                // writeln!(prompt, "Always wrap your code in a Markdown block.").unwrap();
+            }
+            _ => {}
+        }
+
+        // Really dumb truncation strategy
+        if let Some(max_tokens) = max_token_length {
+            prompt = args.model.truncate(&prompt, max_tokens)?;
+        }
+
+        let token_count = args.model.count_tokens(&prompt)?;
+
+        anyhow::Ok((prompt, token_count))
+    }
+}

crates/ai/src/templates/preamble.rs πŸ”—

@@ -0,0 +1,52 @@
+use crate::templates::base::{PromptArguments, PromptFileType, PromptTemplate};
+use std::fmt::Write;
+
+pub struct EngineerPreamble {}
+
+impl PromptTemplate for EngineerPreamble {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        let mut prompts = Vec::new();
+
+        match args.get_file_type() {
+            PromptFileType::Code => {
+                prompts.push(format!(
+                    "You are an expert {}engineer.",
+                    args.language_name.clone().unwrap_or("".to_string()) + " "
+                ));
+            }
+            PromptFileType::Text => {
+                prompts.push("You are an expert engineer.".to_string());
+            }
+        }
+
+        if let Some(project_name) = args.project_name.clone() {
+            prompts.push(format!(
+                "You are currently working inside the '{project_name}' project in code editor Zed."
+            ));
+        }
+
+        if let Some(mut remaining_tokens) = max_token_length {
+            let mut prompt = String::new();
+            let mut total_count = 0;
+            for prompt_piece in prompts {
+                let prompt_token_count =
+                    args.model.count_tokens(&prompt_piece)? + args.model.count_tokens("\n")?;
+                if remaining_tokens > prompt_token_count {
+                    writeln!(prompt, "{prompt_piece}").unwrap();
+                    remaining_tokens -= prompt_token_count;
+                    total_count += prompt_token_count;
+                }
+            }
+
+            anyhow::Ok((prompt, total_count))
+        } else {
+            let prompt = prompts.join("\n");
+            let token_count = args.model.count_tokens(&prompt)?;
+            anyhow::Ok((prompt, token_count))
+        }
+    }
+}

crates/ai/src/templates/repository_context.rs πŸ”—

@@ -0,0 +1,94 @@
+use crate::templates::base::{PromptArguments, PromptTemplate};
+use std::fmt::Write;
+use std::{ops::Range, path::PathBuf};
+
+use gpui::{AsyncAppContext, ModelHandle};
+use language::{Anchor, Buffer};
+
+#[derive(Clone)]
+pub struct PromptCodeSnippet {
+    path: Option<PathBuf>,
+    language_name: Option<String>,
+    content: String,
+}
+
+impl PromptCodeSnippet {
+    pub fn new(buffer: ModelHandle<Buffer>, range: Range<Anchor>, cx: &AsyncAppContext) -> Self {
+        let (content, language_name, file_path) = buffer.read_with(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            let content = snapshot.text_for_range(range.clone()).collect::<String>();
+
+            let language_name = buffer
+                .language()
+                .and_then(|language| Some(language.name().to_string().to_lowercase()));
+
+            let file_path = buffer
+                .file()
+                .and_then(|file| Some(file.path().to_path_buf()));
+
+            (content, language_name, file_path)
+        });
+
+        PromptCodeSnippet {
+            path: file_path,
+            language_name,
+            content,
+        }
+    }
+}
+
+impl ToString for PromptCodeSnippet {
+    fn to_string(&self) -> String {
+        let path = self
+            .path
+            .as_ref()
+            .and_then(|path| Some(path.to_string_lossy().to_string()))
+            .unwrap_or("".to_string());
+        let language_name = self.language_name.clone().unwrap_or("".to_string());
+        let content = self.content.clone();
+
+        format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
+    }
+}
+
+pub struct RepositoryContext {}
+
+impl PromptTemplate for RepositoryContext {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
+        let template = "You are working inside a large repository, here are a few code snippets that may be useful.";
+        let mut prompt = String::new();
+
+        let mut remaining_tokens = max_token_length.clone();
+        let seperator_token_length = args.model.count_tokens("\n")?;
+        for snippet in &args.snippets {
+            let mut snippet_prompt = template.to_string();
+            let content = snippet.to_string();
+            writeln!(snippet_prompt, "{content}").unwrap();
+
+            let token_count = args.model.count_tokens(&snippet_prompt)?;
+            if token_count <= MAXIMUM_SNIPPET_TOKEN_COUNT {
+                if let Some(tokens_left) = remaining_tokens {
+                    if tokens_left >= token_count {
+                        writeln!(prompt, "{snippet_prompt}").unwrap();
+                        remaining_tokens = if tokens_left >= (token_count + seperator_token_length)
+                        {
+                            Some(tokens_left - token_count - seperator_token_length)
+                        } else {
+                            Some(0)
+                        };
+                    }
+                } else {
+                    writeln!(prompt, "{snippet_prompt}").unwrap();
+                }
+            }
+        }
+
+        let total_token_count = args.model.count_tokens(&prompt)?;
+        anyhow::Ok((prompt, total_token_count))
+    }
+}

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -1,12 +1,15 @@
 use crate::{
     assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
     codegen::{self, Codegen, CodegenKind},
-    prompts::{generate_content_prompt, PromptCodeSnippet},
+    prompts::generate_content_prompt,
     MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
     SavedMessage,
 };
-use ai::completion::{
-    stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
+use ai::{
+    completion::{
+        stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
+    },
+    templates::repository_context::PromptCodeSnippet,
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
@@ -609,6 +612,18 @@ impl AssistantPanel {
 
         let project = pending_assist.project.clone();
 
+        let project_name = if let Some(project) = project.upgrade(cx) {
+            Some(
+                project
+                    .read(cx)
+                    .worktree_root_names(cx)
+                    .collect::<Vec<&str>>()
+                    .join("/"),
+            )
+        } else {
+            None
+        };
+
         self.inline_prompt_history
             .retain(|prompt| prompt != user_prompt);
         self.inline_prompt_history.push_back(user_prompt.into());
@@ -646,7 +661,19 @@ impl AssistantPanel {
             None
         };
 
-        let codegen_kind = codegen.read(cx).kind().clone();
+        // Higher Temperature increases the randomness of model outputs.
+        // If Markdown or No Language is Known, increase the randomness for more creative output
+        // If Code, decrease temperature to get more deterministic outputs
+        let temperature = if let Some(language) = language_name.clone() {
+            if language.to_string() != "Markdown".to_string() {
+                0.5
+            } else {
+                1.0
+            }
+        } else {
+            1.0
+        };
+
         let user_prompt = user_prompt.to_string();
 
         let snippets = if retrieve_context {
@@ -668,14 +695,7 @@ impl AssistantPanel {
             let snippets = cx.spawn(|_, cx| async move {
                 let mut snippets = Vec::new();
                 for result in search_results.await {
-                    snippets.push(PromptCodeSnippet::new(result, &cx));
-
-                    // snippets.push(result.buffer.read_with(&cx, |buffer, _| {
-                    //     buffer
-                    //         .snapshot()
-                    //         .text_for_range(result.range)
-                    //         .collect::<String>()
-                    // }));
+                    snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx));
                 }
                 snippets
             });
@@ -696,11 +716,11 @@ impl AssistantPanel {
             generate_content_prompt(
                 user_prompt,
                 language_name,
-                &buffer,
+                buffer,
                 range,
-                codegen_kind,
                 snippets,
                 model_name,
+                project_name,
             )
         });
 
@@ -717,18 +737,23 @@ impl AssistantPanel {
         }
 
         cx.spawn(|_, mut cx| async move {
-            let prompt = prompt.await;
+            // I Don't know if we want to return a ? here.
+            let prompt = prompt.await?;
 
             messages.push(RequestMessage {
                 role: Role::User,
                 content: prompt,
             });
+
             let request = OpenAIRequest {
                 model: model.full_name().into(),
                 messages,
                 stream: true,
+                stop: vec!["|END|>".to_string()],
+                temperature,
             };
             codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
+            anyhow::Ok(())
         })
         .detach();
     }
@@ -1718,6 +1743,8 @@ impl Conversation {
                     .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
                     .collect(),
                 stream: true,
+                stop: vec![],
+                temperature: 1.0,
             };
 
             let stream = stream_completion(api_key, cx.background().clone(), request);
@@ -2002,6 +2029,8 @@ impl Conversation {
                     model: self.model.full_name().to_string(),
                     messages: messages.collect(),
                     stream: true,
+                    stop: vec![],
+                    temperature: 1.0,
                 };
 
                 let stream = stream_completion(api_key, cx.background().clone(), request);

crates/assistant/src/prompts.rs πŸ”—

@@ -1,60 +1,13 @@
-use crate::codegen::CodegenKind;
-use gpui::AsyncAppContext;
+use ai::models::{LanguageModel, OpenAILanguageModel};
+use ai::templates::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
+use ai::templates::file_context::FileContext;
+use ai::templates::generate::GenerateInlineContent;
+use ai::templates::preamble::EngineerPreamble;
+use ai::templates::repository_context::{PromptCodeSnippet, RepositoryContext};
 use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
-use semantic_index::SearchResult;
 use std::cmp::{self, Reverse};
-use std::fmt::Write;
 use std::ops::Range;
-use std::path::PathBuf;
-use tiktoken_rs::ChatCompletionRequestMessage;
-
-pub struct PromptCodeSnippet {
-    path: Option<PathBuf>,
-    language_name: Option<String>,
-    content: String,
-}
-
-impl PromptCodeSnippet {
-    pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self {
-        let (content, language_name, file_path) =
-            search_result.buffer.read_with(cx, |buffer, _| {
-                let snapshot = buffer.snapshot();
-                let content = snapshot
-                    .text_for_range(search_result.range.clone())
-                    .collect::<String>();
-
-                let language_name = buffer
-                    .language()
-                    .and_then(|language| Some(language.name().to_string()));
-
-                let file_path = buffer
-                    .file()
-                    .and_then(|file| Some(file.path().to_path_buf()));
-
-                (content, language_name, file_path)
-            });
-
-        PromptCodeSnippet {
-            path: file_path,
-            language_name,
-            content,
-        }
-    }
-}
-
-impl ToString for PromptCodeSnippet {
-    fn to_string(&self) -> String {
-        let path = self
-            .path
-            .as_ref()
-            .and_then(|path| Some(path.to_string_lossy().to_string()))
-            .unwrap_or("".to_string());
-        let language_name = self.language_name.clone().unwrap_or("".to_string());
-        let content = self.content.clone();
-
-        format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
-    }
-}
+use std::sync::Arc;
 
 #[allow(dead_code)]
 fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
@@ -170,138 +123,50 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
 pub fn generate_content_prompt(
     user_prompt: String,
     language_name: Option<&str>,
-    buffer: &BufferSnapshot,
-    range: Range<impl ToOffset>,
-    kind: CodegenKind,
+    buffer: BufferSnapshot,
+    range: Range<usize>,
     search_results: Vec<PromptCodeSnippet>,
     model: &str,
-) -> String {
-    const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
-    const RESERVED_TOKENS_FOR_GENERATION: usize = 1000;
-
-    let mut prompts = Vec::new();
-    let range = range.to_offset(buffer);
-
-    // General Preamble
-    if let Some(language_name) = language_name {
-        prompts.push(format!("You're an expert {language_name} engineer.\n"));
-    } else {
-        prompts.push("You're an expert engineer.\n".to_string());
-    }
-
-    // Snippets
-    let mut snippet_position = prompts.len() - 1;
-
-    let mut content = String::new();
-    content.extend(buffer.text_for_range(0..range.start));
-    if range.start == range.end {
-        content.push_str("<|START|>");
-    } else {
-        content.push_str("<|START|");
-    }
-    content.extend(buffer.text_for_range(range.clone()));
-    if range.start != range.end {
-        content.push_str("|END|>");
-    }
-    content.extend(buffer.text_for_range(range.end..buffer.len()));
-
-    prompts.push("The file you are currently working on has the following content:\n".to_string());
-
-    if let Some(language_name) = language_name {
-        let language_name = language_name.to_lowercase();
-        prompts.push(format!("```{language_name}\n{content}\n```"));
+    project_name: Option<String>,
+) -> anyhow::Result<String> {
+    // Using new Prompt Templates
+    let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
+    let lang_name = if let Some(language_name) = language_name {
+        Some(language_name.to_string())
     } else {
-        prompts.push(format!("```\n{content}\n```"));
-    }
-
-    match kind {
-        CodegenKind::Generate { position: _ } => {
-            prompts.push("In particular, the user's cursor is currently on the '<|START|>' span in the above outline, with no text selected.".to_string());
-            prompts
-                .push("Assume the cursor is located where the `<|START|` marker is.".to_string());
-            prompts.push(
-                "Text can't be replaced, so assume your answer will be inserted at the cursor."
-                    .to_string(),
-            );
-            prompts.push(format!(
-                "Generate text based on the users prompt: {user_prompt}"
-            ));
-        }
-        CodegenKind::Transform { range: _ } => {
-            prompts.push("In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.".to_string());
-            prompts.push(format!(
-                "Modify the users code selected text based upon the users prompt: '{user_prompt}'"
-            ));
-            prompts.push("You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file.".to_string());
-        }
-    }
-
-    if let Some(language_name) = language_name {
-        prompts.push(format!(
-            "Your answer MUST always and only be valid {language_name}"
-        ));
-    }
-    prompts.push("Never make remarks about the output.".to_string());
-    prompts.push("Do not return any text, except the generated code.".to_string());
-    prompts.push("Always wrap your code in a Markdown block".to_string());
-
-    let current_messages = [ChatCompletionRequestMessage {
-        role: "user".to_string(),
-        content: Some(prompts.join("\n")),
-        function_call: None,
-        name: None,
-    }];
-
-    let mut remaining_token_count = if let Ok(current_token_count) =
-        tiktoken_rs::num_tokens_from_messages(model, &current_messages)
-    {
-        let max_token_count = tiktoken_rs::model::get_context_size(model);
-        let intermediate_token_count = if max_token_count > current_token_count {
-            max_token_count - current_token_count
-        } else {
-            0
-        };
-
-        if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION {
-            0
-        } else {
-            intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION
-        }
-    } else {
-        // If tiktoken fails to count token count, assume we have no space remaining.
-        0
+        None
     };
 
-    // TODO:
-    //   - add repository name to snippet
-    //   - add file path
-    //   - add language
-    if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) {
-        let mut template = "You are working inside a large repository, here are a few code snippets that may be useful";
-
-        for search_result in search_results {
-            let mut snippet_prompt = template.to_string();
-            let snippet = search_result.to_string();
-            writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap();
-
-            let token_count = encoding
-                .encode_with_special_tokens(snippet_prompt.as_str())
-                .len();
-            if token_count <= remaining_token_count {
-                if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT {
-                    prompts.insert(snippet_position, snippet_prompt);
-                    snippet_position += 1;
-                    remaining_token_count -= token_count;
-                    // If you have already added the template to the prompt, remove the template.
-                    template = "";
-                }
-            } else {
-                break;
-            }
-        }
-    }
+    let args = PromptArguments {
+        model: openai_model,
+        language_name: lang_name.clone(),
+        project_name,
+        snippets: search_results.clone(),
+        reserved_tokens: 1000,
+        buffer: Some(buffer),
+        selected_range: Some(range),
+        user_prompt: Some(user_prompt.clone()),
+    };
 
-    prompts.join("\n")
+    let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+        (PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
+        (
+            PromptPriority::Ordered { order: 1 },
+            Box::new(RepositoryContext {}),
+        ),
+        (
+            PromptPriority::Ordered { order: 0 },
+            Box::new(FileContext {}),
+        ),
+        (
+            PromptPriority::Mandatory,
+            Box::new(GenerateInlineContent {}),
+        ),
+    ];
+    let chain = PromptChain::new(args, templates);
+    let (prompt, _) = chain.generate(true)?;
+
+    anyhow::Ok(prompt)
 }
 
 #[cfg(test)]

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -15,8 +15,8 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
-    tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
-    Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
+    tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
+    LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
 };
 use live_kit_client::MacOSDisplay;
 use lsp::LanguageServerId;
@@ -4530,6 +4530,7 @@ async fn test_prettier_formatting_buffer(
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
+            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -4537,10 +4538,7 @@ async fn test_prettier_formatting_buffer(
     let test_plugin = "test_plugin";
     let mut fake_language_servers = language
         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            enabled_formatters: vec![BundledFormatter::Prettier {
-                parser_name: Some("test_parser"),
-                plugin_names: vec![test_plugin],
-            }],
+            prettier_plugins: vec![test_plugin],
             ..Default::default()
         }))
         .await;

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -466,7 +466,11 @@ impl CollabTitlebarItem {
     pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
         if self.branch_popover.take().is_none() {
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                let view = cx.add_view(|cx| build_branch_list(workspace, cx));
+                let Some(view) =
+                    cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
+                else {
+                    return;
+                };
                 cx.subscribe(&view, |this, _, event, cx| {
                     match event {
                         PickerEvent::Dismiss => {

crates/collab_ui/src/collab_ui.rs πŸ”—

@@ -6,7 +6,6 @@ mod face_pile;
 pub mod notification_panel;
 pub mod notifications;
 mod panel_settings;
-mod sharing_status_indicator;
 
 use call::{report_call_event_for_room, ActiveCall, Room};
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
@@ -46,7 +45,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     collab_panel::init(cx);
     chat_panel::init(cx);
     notifications::init(&app_state, cx);
-    sharing_status_indicator::init(cx);
 
     cx.add_global_action(toggle_screen_sharing);
     cx.add_global_action(toggle_mute);

crates/collab_ui/src/sharing_status_indicator.rs πŸ”—

@@ -1,62 +0,0 @@
-use crate::toggle_screen_sharing;
-use call::ActiveCall;
-use gpui::{
-    color::Color,
-    elements::{MouseEventHandler, Svg},
-    platform::{Appearance, MouseButton},
-    AnyElement, AppContext, Element, Entity, View, ViewContext,
-};
-use workspace::WorkspaceSettings;
-
-pub fn init(cx: &mut AppContext) {
-    let active_call = ActiveCall::global(cx);
-
-    let mut status_indicator = None;
-    cx.observe(&active_call, move |call, cx| {
-        if let Some(room) = call.read(cx).room() {
-            if room.read(cx).is_screen_sharing() {
-                if status_indicator.is_none()
-                    && settings::get::<WorkspaceSettings>(cx).show_call_status_icon
-                {
-                    status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
-                }
-            } else if let Some(window) = status_indicator.take() {
-                window.update(cx, |cx| cx.remove_window());
-            }
-        } else if let Some(window) = status_indicator.take() {
-            window.update(cx, |cx| cx.remove_window());
-        }
-    })
-    .detach();
-}
-
-pub struct SharingStatusIndicator;
-
-impl Entity for SharingStatusIndicator {
-    type Event = ();
-}
-
-impl View for SharingStatusIndicator {
-    fn ui_name() -> &'static str {
-        "SharingStatusIndicator"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let color = match cx.window_appearance() {
-            Appearance::Light | Appearance::VibrantLight => Color::black(),
-            Appearance::Dark | Appearance::VibrantDark => Color::white(),
-        };
-
-        MouseEventHandler::new::<Self, _>(0, cx, |_, _| {
-            Svg::new("icons/desktop.svg")
-                .with_color(color)
-                .constrained()
-                .with_width(18.)
-                .aligned()
-        })
-        .on_click(MouseButton::Left, |_, _, cx| {
-            toggle_screen_sharing(&Default::default(), cx)
-        })
-        .into_any()
-    }
-}

crates/editor/src/display_map.rs πŸ”—

@@ -5,22 +5,24 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{
-    link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
-    MultiBufferSnapshot, ToOffset, ToPoint,
+    link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
+    EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
     color::Color,
-    fonts::{FontId, HighlightStyle},
+    fonts::{FontId, HighlightStyle, Underline},
+    text_layout::{Line, RunStyle},
     Entity, ModelContext, ModelHandle,
 };
 use inlay_map::InlayMap;
 use language::{
     language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
 };
-use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use lsp::DiagnosticSeverity;
+use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
 use wrap_map::WrapMap;
@@ -316,6 +318,12 @@ pub struct Highlights<'a> {
     pub suggestion_highlight_style: Option<HighlightStyle>,
 }
 
+pub struct HighlightedChunk<'a> {
+    pub chunk: &'a str,
+    pub style: Option<HighlightStyle>,
+    pub is_tab: bool,
+}
+
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
     pub fold_snapshot: fold_map::FoldSnapshot,
@@ -485,7 +493,7 @@ impl DisplaySnapshot {
         language_aware: bool,
         inlay_highlight_style: Option<HighlightStyle>,
         suggestion_highlight_style: Option<HighlightStyle>,
-    ) -> DisplayChunks<'_> {
+    ) -> DisplayChunks<'a> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
@@ -498,6 +506,140 @@ impl DisplaySnapshot {
         )
     }
 
+    pub fn highlighted_chunks<'a>(
+        &'a self,
+        display_rows: Range<u32>,
+        language_aware: bool,
+        style: &'a EditorStyle,
+    ) -> impl Iterator<Item = HighlightedChunk<'a>> {
+        self.chunks(
+            display_rows,
+            language_aware,
+            Some(style.theme.hint),
+            Some(style.theme.suggestion),
+        )
+        .map(|chunk| {
+            let mut highlight_style = chunk
+                .syntax_highlight_id
+                .and_then(|id| id.style(&style.syntax));
+
+            if let Some(chunk_highlight) = chunk.highlight_style {
+                if let Some(highlight_style) = highlight_style.as_mut() {
+                    highlight_style.highlight(chunk_highlight);
+                } else {
+                    highlight_style = Some(chunk_highlight);
+                }
+            }
+
+            let mut diagnostic_highlight = HighlightStyle::default();
+
+            if chunk.is_unnecessary {
+                diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
+            }
+
+            if let Some(severity) = chunk.diagnostic_severity {
+                // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+                if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+                    let diagnostic_style = super::diagnostic_style(severity, true, style);
+                    diagnostic_highlight.underline = Some(Underline {
+                        color: Some(diagnostic_style.message.text.color),
+                        thickness: 1.0.into(),
+                        squiggly: true,
+                    });
+                }
+            }
+
+            if let Some(highlight_style) = highlight_style.as_mut() {
+                highlight_style.highlight(diagnostic_highlight);
+            } else {
+                highlight_style = Some(diagnostic_highlight);
+            }
+
+            HighlightedChunk {
+                chunk: chunk.text,
+                style: highlight_style,
+                is_tab: chunk.is_tab,
+            }
+        })
+    }
+
+    pub fn lay_out_line_for_row(
+        &self,
+        display_row: u32,
+        TextLayoutDetails {
+            font_cache,
+            text_layout_cache,
+            editor_style,
+        }: &TextLayoutDetails,
+    ) -> Line {
+        let mut styles = Vec::new();
+        let mut line = String::new();
+        let mut ended_in_newline = false;
+
+        let range = display_row..display_row + 1;
+        for chunk in self.highlighted_chunks(range, false, editor_style) {
+            line.push_str(chunk.chunk);
+
+            let text_style = if let Some(style) = chunk.style {
+                editor_style
+                    .text
+                    .clone()
+                    .highlight(style, font_cache)
+                    .map(Cow::Owned)
+                    .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+            } else {
+                Cow::Borrowed(&editor_style.text)
+            };
+            ended_in_newline = chunk.chunk.ends_with("\n");
+
+            styles.push((
+                chunk.chunk.len(),
+                RunStyle {
+                    font_id: text_style.font_id,
+                    color: text_style.color,
+                    underline: text_style.underline,
+                },
+            ));
+        }
+
+        // our pixel positioning logic assumes each line ends in \n,
+        // this is almost always true except for the last line which
+        // may have no trailing newline.
+        if !ended_in_newline && display_row == self.max_point().row() {
+            line.push_str("\n");
+
+            styles.push((
+                "\n".len(),
+                RunStyle {
+                    font_id: editor_style.text.font_id,
+                    color: editor_style.text_color,
+                    underline: editor_style.text.underline,
+                },
+            ));
+        }
+
+        text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
+    }
+
+    pub fn x_for_point(
+        &self,
+        display_point: DisplayPoint,
+        text_layout_details: &TextLayoutDetails,
+    ) -> f32 {
+        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
+        layout_line.x_for_index(display_point.column() as usize)
+    }
+
+    pub fn column_for_x(
+        &self,
+        display_row: u32,
+        x_coordinate: f32,
+        text_layout_details: &TextLayoutDetails,
+    ) -> u32 {
+        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
+        layout_line.closest_index_for_x(x_coordinate) as u32
+    }
+
     pub fn chars_at(
         &self,
         mut point: DisplayPoint,
@@ -869,12 +1011,16 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
 #[cfg(test)]
 pub mod tests {
     use super::*;
-    use crate::{movement, test::marked_display_snapshot};
+    use crate::{
+        movement,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+    };
     use gpui::{color::Color, elements::*, test::observe, AppContext};
     use language::{
         language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
         Buffer, Language, LanguageConfig, SelectionGoal,
     };
+    use project::Project;
     use rand::{prelude::*, Rng};
     use settings::SettingsStore;
     use smol::stream::StreamExt;
@@ -1148,95 +1294,120 @@ pub mod tests {
     }
 
     #[gpui::test(retries = 5)]
-    fn test_soft_wraps(cx: &mut AppContext) {
+    async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-        init_test(cx, |_| {});
+        cx.update(|cx| {
+            init_test(cx, |_| {});
+        });
 
-        let font_cache = cx.font_cache();
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
 
-        let family_id = font_cache
-            .load_family(&["Helvetica"], &Default::default())
-            .unwrap();
-        let font_id = font_cache
-            .select_font(family_id, &Default::default())
-            .unwrap();
-        let font_size = 12.0;
-        let wrap_width = Some(64.);
+        cx.update_window(window, |cx| {
+            let text_layout_details =
+                editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
 
-        let text = "one two three four five\nsix seven eight";
-        let buffer = MultiBuffer::build_simple(text, cx);
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
-        });
+            let font_cache = cx.font_cache().clone();
 
-        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            snapshot.text_chunks(0).collect::<String>(),
-            "one two \nthree four \nfive\nsix seven \neight"
-        );
-        assert_eq!(
-            snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
-            DisplayPoint::new(0, 7)
-        );
-        assert_eq!(
-            snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
-            DisplayPoint::new(1, 0)
-        );
-        assert_eq!(
-            movement::right(&snapshot, DisplayPoint::new(0, 7)),
-            DisplayPoint::new(1, 0)
-        );
-        assert_eq!(
-            movement::left(&snapshot, DisplayPoint::new(1, 0)),
-            DisplayPoint::new(0, 7)
-        );
-        assert_eq!(
-            movement::up(
-                &snapshot,
-                DisplayPoint::new(1, 10),
-                SelectionGoal::None,
-                false
-            ),
-            (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
-        );
-        assert_eq!(
-            movement::down(
-                &snapshot,
-                DisplayPoint::new(0, 7),
-                SelectionGoal::Column(10),
-                false
-            ),
-            (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
-        );
-        assert_eq!(
-            movement::down(
-                &snapshot,
-                DisplayPoint::new(1, 10),
-                SelectionGoal::Column(10),
-                false
-            ),
-            (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
-        );
+            let family_id = font_cache
+                .load_family(&["Helvetica"], &Default::default())
+                .unwrap();
+            let font_id = font_cache
+                .select_font(family_id, &Default::default())
+                .unwrap();
+            let font_size = 12.0;
+            let wrap_width = Some(64.);
 
-        let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
-        buffer.update(cx, |buffer, cx| {
-            buffer.edit([(ix..ix, "and ")], None, cx);
-        });
+            let text = "one two three four five\nsix seven eight";
+            let buffer = MultiBuffer::build_simple(text, cx);
+            let map = cx.add_model(|cx| {
+                DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
+            });
 
-        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            snapshot.text_chunks(1).collect::<String>(),
-            "three four \nfive\nsix and \nseven eight"
-        );
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(0).collect::<String>(),
+                "one two \nthree four \nfive\nsix seven \neight"
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+                DisplayPoint::new(0, 7)
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::right(&snapshot, DisplayPoint::new(0, 7)),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::left(&snapshot, DisplayPoint::new(1, 0)),
+                DisplayPoint::new(0, 7)
+            );
 
-        // Re-wrap on font size changes
-        map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+            let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
+            assert_eq!(
+                movement::up(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details,
+                ),
+                (
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 4),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
 
-        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            snapshot.text_chunks(1).collect::<String>(),
-            "three \nfour five\nsix and \nseven \neight"
-        )
+            let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
+            buffer.update(cx, |buffer, cx| {
+                buffer.edit([(ix..ix, "and ")], None, cx);
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three four \nfive\nsix and \nseven eight"
+            );
+
+            // Re-wrap on font size changes
+            map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three \nfour five\nsix and \nseven \neight"
+            )
+        });
     }
 
     #[gpui::test]
@@ -1731,6 +1902,9 @@ pub mod tests {
         cx.foreground().forbid_parking();
         cx.set_global(SettingsStore::test(cx));
         language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+        theme::init((), cx);
         cx.update_global::<SettingsStore, _, _>(|store, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, f);
         });

crates/editor/src/editor.rs πŸ”—

@@ -71,6 +71,7 @@ use link_go_to_definition::{
 };
 use log::error;
 use lsp::LanguageServerId;
+use movement::TextLayoutDetails;
 use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
@@ -3476,6 +3477,14 @@ impl Editor {
             .collect()
     }
 
+    pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails {
+        TextLayoutDetails {
+            font_cache: cx.font_cache().clone(),
+            text_layout_cache: cx.text_layout_cache().clone(),
+            editor_style: self.style(cx),
+        }
+    }
+
     fn splice_inlay_hints(
         &self,
         to_remove: Vec<InlayId>,
@@ -5410,6 +5419,7 @@ impl Editor {
     }
 
     pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
         self.transact(cx, |this, cx| {
             let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 let mut edits: Vec<(Range<usize>, String)> = Default::default();
@@ -5433,7 +5443,10 @@ impl Editor {
 
                     *head.column_mut() += 1;
                     head = display_map.clip_point(head, Bias::Right);
-                    selection.collapse_to(head, SelectionGoal::Column(head.column()));
+                    let goal = SelectionGoal::HorizontalPosition(
+                        display_map.x_for_point(head, &text_layout_details),
+                    );
+                    selection.collapse_to(head, goal);
 
                     let transpose_start = display_map
                         .buffer_snapshot
@@ -5697,13 +5710,21 @@ impl Editor {
             return;
         }
 
+        let text_layout_details = &self.text_layout_details(cx);
+
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             let line_mode = s.line_mode;
             s.move_with(|map, selection| {
                 if !selection.is_empty() && !line_mode {
                     selection.goal = SelectionGoal::None;
                 }
-                let (cursor, goal) = movement::up(map, selection.start, selection.goal, false);
+                let (cursor, goal) = movement::up(
+                    map,
+                    selection.start,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
                 selection.collapse_to(cursor, goal);
             });
         })
@@ -5731,22 +5752,33 @@ impl Editor {
             Autoscroll::fit()
         };
 
+        let text_layout_details = &self.text_layout_details(cx);
+
         self.change_selections(Some(autoscroll), cx, |s| {
             let line_mode = s.line_mode;
             s.move_with(|map, selection| {
                 if !selection.is_empty() && !line_mode {
                     selection.goal = SelectionGoal::None;
                 }
-                let (cursor, goal) =
-                    movement::up_by_rows(map, selection.end, row_count, selection.goal, false);
+                let (cursor, goal) = movement::up_by_rows(
+                    map,
+                    selection.end,
+                    row_count,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
                 selection.collapse_to(cursor, goal);
             });
         });
     }
 
     pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-            s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
+            s.move_heads_with(|map, head, goal| {
+                movement::up(map, head, goal, false, &text_layout_details)
+            })
         })
     }
 
@@ -5758,13 +5790,20 @@ impl Editor {
             return;
         }
 
+        let text_layout_details = &self.text_layout_details(cx);
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             let line_mode = s.line_mode;
             s.move_with(|map, selection| {
                 if !selection.is_empty() && !line_mode {
                     selection.goal = SelectionGoal::None;
                 }
-                let (cursor, goal) = movement::down(map, selection.end, selection.goal, false);
+                let (cursor, goal) = movement::down(
+                    map,
+                    selection.end,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
                 selection.collapse_to(cursor, goal);
             });
         });
@@ -5802,22 +5841,32 @@ impl Editor {
             Autoscroll::fit()
         };
 
+        let text_layout_details = &self.text_layout_details(cx);
         self.change_selections(Some(autoscroll), cx, |s| {
             let line_mode = s.line_mode;
             s.move_with(|map, selection| {
                 if !selection.is_empty() && !line_mode {
                     selection.goal = SelectionGoal::None;
                 }
-                let (cursor, goal) =
-                    movement::down_by_rows(map, selection.end, row_count, selection.goal, false);
+                let (cursor, goal) = movement::down_by_rows(
+                    map,
+                    selection.end,
+                    row_count,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
                 selection.collapse_to(cursor, goal);
             });
         });
     }
 
     pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-            s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
+            s.move_heads_with(|map, head, goal| {
+                movement::down(map, head, goal, false, &text_layout_details)
+            })
         });
     }
 
@@ -6336,11 +6385,14 @@ impl Editor {
     fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections.all::<Point>(cx);
+        let text_layout_details = self.text_layout_details(cx);
         let mut state = self.add_selections_state.take().unwrap_or_else(|| {
             let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
             let range = oldest_selection.display_range(&display_map).sorted();
-            let columns = cmp::min(range.start.column(), range.end.column())
-                ..cmp::max(range.start.column(), range.end.column());
+
+            let start_x = display_map.x_for_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let positions = start_x.min(end_x)..start_x.max(end_x);
 
             selections.clear();
             let mut stack = Vec::new();
@@ -6348,8 +6400,9 @@ impl Editor {
                 if let Some(selection) = self.selections.build_columnar_selection(
                     &display_map,
                     row,
-                    &columns,
+                    &positions,
                     oldest_selection.reversed,
+                    &text_layout_details,
                 ) {
                     stack.push(selection.id);
                     selections.push(selection);
@@ -6377,12 +6430,15 @@ impl Editor {
                     let range = selection.display_range(&display_map).sorted();
                     debug_assert_eq!(range.start.row(), range.end.row());
                     let mut row = range.start.row();
-                    let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal
+                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
+                        selection.goal
                     {
                         start..end
                     } else {
-                        cmp::min(range.start.column(), range.end.column())
-                            ..cmp::max(range.start.column(), range.end.column())
+                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
+                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
+
+                        start_x.min(end_x)..start_x.max(end_x)
                     };
 
                     while row != end_row {
@@ -6395,8 +6451,9 @@ impl Editor {
                         if let Some(new_selection) = self.selections.build_columnar_selection(
                             &display_map,
                             row,
-                            &columns,
+                            &positions,
                             selection.reversed,
+                            &text_layout_details,
                         ) {
                             state.stack.push(new_selection.id);
                             if above {
@@ -6690,6 +6747,7 @@ impl Editor {
     }
 
     pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
         self.transact(cx, |this, cx| {
             let mut selections = this.selections.all::<Point>(cx);
             let mut edits = Vec::new();
@@ -6932,7 +6990,10 @@ impl Editor {
                         point.row += 1;
                         point = snapshot.clip_point(point, Bias::Left);
                         let display_point = point.to_display_point(display_snapshot);
-                        (display_point, SelectionGoal::Column(display_point.column()))
+                        let goal = SelectionGoal::HorizontalPosition(
+                            display_snapshot.x_for_point(display_point, &text_layout_details),
+                        );
+                        (display_point, goal)
                     })
                 });
             }

crates/editor/src/editor_tests.rs πŸ”—

@@ -19,8 +19,8 @@ use gpui::{
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
-    BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
-    LanguageRegistry, Override, Point,
+    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+    Override, Point,
 };
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -851,7 +851,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 
     let view = cx
         .add_window(|cx| {
-            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδΡ\n", cx);
+            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδΡ", cx);
             build_editor(buffer.clone(), cx)
         })
         .root(cx);
@@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
             true,
             cx,
         );
-        assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nabβ‹―e\nΞ±Ξ²β‹―Ξ΅\n");
+        assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nabβ‹―e\nΞ±Ξ²β‹―Ξ΅");
 
         view.move_right(&MoveRight, cx);
         assert_eq!(
@@ -888,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
         );
 
         view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "abβ‹―e".len())]
+        );
+        view.move_left(&MoveLeft, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
             &[empty_range(1, "abβ‹―".len())]
@@ -929,17 +934,18 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
             view.selections.display_ranges(cx),
             &[empty_range(1, "abβ‹―e".len())]
         );
-        view.move_up(&MoveUp, cx);
+        view.move_down(&MoveDown, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ⋯ⓔ".len())]
+            &[empty_range(2, "Ξ±Ξ²β‹―Ξ΅".len())]
         );
-        view.move_left(&MoveLeft, cx);
+        view.move_up(&MoveUp, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ⋯".len())]
+            &[empty_range(1, "abβ‹―e".len())]
         );
-        view.move_left(&MoveLeft, cx);
+
+        view.move_up(&MoveUp, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
             &[empty_range(0, "ⓐⓑ".len())]
@@ -949,6 +955,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
             view.selections.display_ranges(cx),
             &[empty_range(0, "ⓐ".len())]
         );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "".len())]
+        );
     });
 }
 
@@ -5084,6 +5095,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
+            // Enable Prettier formatting for the same buffer, and ensure
+            // LSP is called instead of Prettier.
+            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -5094,12 +5108,6 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
                 document_formatting_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
-            // Enable Prettier formatting for the same buffer, and ensure
-            // LSP is called instead of Prettier.
-            enabled_formatters: vec![BundledFormatter::Prettier {
-                parser_name: Some("test_parser"),
-                plugin_names: Vec::new(),
-            }],
             ..Default::default()
         }))
         .await;
@@ -7838,6 +7846,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
+            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -7846,10 +7855,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     let test_plugin = "test_plugin";
     let _ = language
         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            enabled_formatters: vec![BundledFormatter::Prettier {
-                parser_name: Some("test_parser"),
-                plugin_names: vec![test_plugin],
-            }],
+            prettier_plugins: vec![test_plugin],
             ..Default::default()
         }))
         .await;

crates/editor/src/element.rs πŸ”—

@@ -4,7 +4,7 @@ use super::{
     MAX_LINE_LEN,
 };
 use crate::{
-    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
+    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
     editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
@@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
 use gpui::{
     color::Color,
     elements::*,
-    fonts::{HighlightStyle, TextStyle, Underline},
+    fonts::TextStyle,
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -37,8 +37,7 @@ use gpui::{
 use itertools::Itertools;
 use json::json;
 use language::{
-    language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
-    Selection,
+    language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
 };
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
@@ -1584,56 +1583,7 @@ impl EditorElement {
                 .collect()
         } else {
             let style = &self.style;
-            let chunks = snapshot
-                .chunks(
-                    rows.clone(),
-                    true,
-                    Some(style.theme.hint),
-                    Some(style.theme.suggestion),
-                )
-                .map(|chunk| {
-                    let mut highlight_style = chunk
-                        .syntax_highlight_id
-                        .and_then(|id| id.style(&style.syntax));
-
-                    if let Some(chunk_highlight) = chunk.highlight_style {
-                        if let Some(highlight_style) = highlight_style.as_mut() {
-                            highlight_style.highlight(chunk_highlight);
-                        } else {
-                            highlight_style = Some(chunk_highlight);
-                        }
-                    }
-
-                    let mut diagnostic_highlight = HighlightStyle::default();
-
-                    if chunk.is_unnecessary {
-                        diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
-                    }
-
-                    if let Some(severity) = chunk.diagnostic_severity {
-                        // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
-                        if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
-                            let diagnostic_style = super::diagnostic_style(severity, true, style);
-                            diagnostic_highlight.underline = Some(Underline {
-                                color: Some(diagnostic_style.message.text.color),
-                                thickness: 1.0.into(),
-                                squiggly: true,
-                            });
-                        }
-                    }
-
-                    if let Some(highlight_style) = highlight_style.as_mut() {
-                        highlight_style.highlight(diagnostic_highlight);
-                    } else {
-                        highlight_style = Some(diagnostic_highlight);
-                    }
-
-                    HighlightedChunk {
-                        chunk: chunk.text,
-                        style: highlight_style,
-                        is_tab: chunk.is_tab,
-                    }
-                });
+            let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
 
             LineWithInvisibles::from_chunks(
                 chunks,
@@ -1870,12 +1820,6 @@ impl EditorElement {
     }
 }
 
-struct HighlightedChunk<'a> {
-    chunk: &'a str,
-    style: Option<HighlightStyle>,
-    is_tab: bool,
-}
-
 #[derive(Debug)]
 pub struct LineWithInvisibles {
     pub line: Line,

crates/editor/src/inlay_hint_cache.rs πŸ”—

@@ -2138,7 +2138,7 @@ pub mod tests {
         });
     }
 
-    #[gpui::test]
+    #[gpui::test(iterations = 10)]
     async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
@@ -2400,11 +2400,13 @@ pub mod tests {
         ));
         cx.foreground().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|r| r.start);
+
             assert_eq!(ranges.len(), 3,
                 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
-            let visible_query_range = &ranges[0];
-            let above_query_range = &ranges[1];
+            let above_query_range = &ranges[0];
+            let visible_query_range = &ranges[1];
             let below_query_range = &ranges[2];
             assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
                 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");

crates/editor/src/movement.rs πŸ”—

@@ -1,7 +1,8 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, ToOffset, ToPoint};
+use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
+use gpui::{FontCache, TextLayoutCache};
 use language::Point;
-use std::ops::Range;
+use std::{ops::Range, sync::Arc};
 
 #[derive(Debug, PartialEq)]
 pub enum FindRange {
@@ -9,6 +10,14 @@ pub enum FindRange {
     MultiLine,
 }
 
+/// TextLayoutDetails encompasses everything we need to move vertically
+/// taking into account variable width characters.
+pub struct TextLayoutDetails {
+    pub font_cache: Arc<FontCache>,
+    pub text_layout_cache: Arc<TextLayoutCache>,
+    pub editor_style: EditorStyle,
+}
+
 pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     if point.column() > 0 {
         *point.column_mut() -= 1;
@@ -47,8 +56,16 @@ pub fn up(
     start: DisplayPoint,
     goal: SelectionGoal,
     preserve_column_at_start: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    up_by_rows(map, start, 1, goal, preserve_column_at_start)
+    up_by_rows(
+        map,
+        start,
+        1,
+        goal,
+        preserve_column_at_start,
+        text_layout_details,
+    )
 }
 
 pub fn down(
@@ -56,8 +73,16 @@ pub fn down(
     start: DisplayPoint,
     goal: SelectionGoal,
     preserve_column_at_end: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    down_by_rows(map, start, 1, goal, preserve_column_at_end)
+    down_by_rows(
+        map,
+        start,
+        1,
+        goal,
+        preserve_column_at_end,
+        text_layout_details,
+    )
 }
 
 pub fn up_by_rows(
@@ -66,11 +91,13 @@ pub fn up_by_rows(
     row_count: u32,
     goal: SelectionGoal,
     preserve_column_at_start: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    let mut goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => map.column_to_chars(start.row(), start.column()),
+    let mut goal_x = match goal {
+        SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+        SelectionGoal::HorizontalRange { end, .. } => end,
+        _ => map.x_for_point(start, text_layout_details),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -79,19 +106,19 @@ pub fn up_by_rows(
         Bias::Left,
     );
     if point.row() < start.row() {
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_start {
         return (start, goal);
     } else {
         point = DisplayPoint::new(0, 0);
-        goal_column = 0;
+        goal_x = 0.0;
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Left);
     if clipped_point.row() < point.row() {
         clipped_point = map.clip_point(point, Bias::Right);
     }
-    (clipped_point, SelectionGoal::Column(goal_column))
+    (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
 }
 
 pub fn down_by_rows(
@@ -100,29 +127,31 @@ pub fn down_by_rows(
     row_count: u32,
     goal: SelectionGoal,
     preserve_column_at_end: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    let mut goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => map.column_to_chars(start.row(), start.column()),
+    let mut goal_x = match goal {
+        SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+        SelectionGoal::HorizontalRange { end, .. } => end,
+        _ => map.x_for_point(start, text_layout_details),
     };
 
     let new_row = start.row() + row_count;
     let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
     if point.row() > start.row() {
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_end {
         return (start, goal);
     } else {
         point = map.max_point();
-        goal_column = map.column_to_chars(point.row(), point.column())
+        goal_x = map.x_for_point(point, text_layout_details)
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Right);
     if clipped_point.row() > point.row() {
         clipped_point = map.clip_point(point, Bias::Left);
     }
-    (clipped_point, SelectionGoal::Column(goal_column))
+    (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
 }
 
 pub fn line_beginning(
@@ -396,9 +425,11 @@ pub fn split_display_range_by_lines(
 mod tests {
     use super::*;
     use crate::{
-        display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
-        InlayId, MultiBuffer,
+        display_map::Inlay,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+        Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
     };
+    use project::Project;
     use settings::SettingsStore;
     use util::post_inc;
 
@@ -691,123 +722,173 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
-        init_test(cx);
-
-        let family_id = cx
-            .font_cache()
-            .load_family(&["Helvetica"], &Default::default())
-            .unwrap();
-        let font_id = cx
-            .font_cache()
-            .select_font(family_id, &Default::default())
-            .unwrap();
+    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| {
+            init_test(cx);
+        });
 
-        let buffer =
-            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
-        let multibuffer = cx.add_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
-            multibuffer.push_excerpts(
-                buffer.clone(),
-                [
-                    ExcerptRange {
-                        context: Point::new(0, 0)..Point::new(1, 4),
-                        primary: None,
-                    },
-                    ExcerptRange {
-                        context: Point::new(2, 0)..Point::new(3, 2),
-                        primary: None,
-                    },
-                ],
-                cx,
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
+        cx.update_window(window, |cx| {
+            let text_layout_details =
+                editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let family_id = cx
+                .font_cache()
+                .load_family(&["Helvetica"], &Default::default())
+                .unwrap();
+            let font_id = cx
+                .font_cache()
+                .select_font(family_id, &Default::default())
+                .unwrap();
+
+            let buffer =
+                cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
+            let multibuffer = cx.add_model(|cx| {
+                let mut multibuffer = MultiBuffer::new(0);
+                multibuffer.push_excerpts(
+                    buffer.clone(),
+                    [
+                        ExcerptRange {
+                            context: Point::new(0, 0)..Point::new(1, 4),
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: Point::new(2, 0)..Point::new(3, 2),
+                            primary: None,
+                        },
+                    ],
+                    cx,
+                );
+                multibuffer
+            });
+            let display_map =
+                cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
+            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+            let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+            // Can't move up into the first excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 2),
+                    SelectionGoal::HorizontalPosition(col_2_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
+            );
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
             );
-            multibuffer
-        });
-        let display_map =
-            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
-        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 
-        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+            let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
 
-        // Can't move up into the first excerpt's header
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(2, 2),
-                SelectionGoal::Column(2),
-                false
-            ),
-            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
-        );
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(2, 0),
-                SelectionGoal::None,
-                false
-            ),
-            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
-        );
+            // Move up and down within first excerpt
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x)
+                ),
+            );
 
-        // Move up and down within first excerpt
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(3, 4),
-                SelectionGoal::Column(4),
-                false
-            ),
-            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
-        );
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(2, 3),
-                SelectionGoal::Column(4),
-                false
-            ),
-            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
-        );
+            let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
 
-        // Move up and down across second excerpt's header
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(6, 5),
-                SelectionGoal::Column(5),
-                false
-            ),
-            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
-        );
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(3, 4),
-                SelectionGoal::Column(5),
-                false
-            ),
-            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
-        );
+            // Move up and down across second excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x)
+                ),
+            );
 
-        // Can't move down off the end
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(7, 0),
-                SelectionGoal::Column(0),
-                false
-            ),
-            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
-        );
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(7, 2),
-                SelectionGoal::Column(2),
-                false
-            ),
-            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
-        );
+            let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+            // Can't move down off the end
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 0),
+                    SelectionGoal::HorizontalPosition(0.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x)
+                ),
+            );
+        });
     }
 
     fn init_test(cx: &mut gpui::AppContext) {
@@ -815,5 +896,6 @@ mod tests {
         theme::init((), cx);
         language::init(cx);
         crate::init(cx);
+        Project::init_settings(cx);
     }
 }

crates/editor/src/selections_collection.rs πŸ”—

@@ -1,6 +1,6 @@
 use std::{
     cell::Ref,
-    cmp, iter, mem,
+    iter, mem,
     ops::{Deref, DerefMut, Range, Sub},
     sync::Arc,
 };
@@ -13,6 +13,7 @@ use util::post_inc;
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    movement::TextLayoutDetails,
     Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
 };
 
@@ -305,23 +306,29 @@ impl SelectionsCollection {
         &mut self,
         display_map: &DisplaySnapshot,
         row: u32,
-        columns: &Range<u32>,
+        positions: &Range<f32>,
         reversed: bool,
+        text_layout_details: &TextLayoutDetails,
     ) -> Option<Selection<Point>> {
-        let is_empty = columns.start == columns.end;
+        let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
-        if columns.start < line_len || (is_empty && columns.start == line_len) {
-            let start = DisplayPoint::new(row, columns.start);
-            let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
+
+        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+
+        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
+            let start = DisplayPoint::new(row, start_col);
+            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end = DisplayPoint::new(row, end_col);
 
             Some(Selection {
                 id: post_inc(&mut self.next_selection_id),
                 start: start.to_point(display_map),
                 end: end.to_point(display_map),
                 reversed,
-                goal: SelectionGoal::ColumnRange {
-                    start: columns.start,
-                    end: columns.end,
+                goal: SelectionGoal::HorizontalRange {
+                    start: positions.start,
+                    end: positions.end,
                 },
             })
         } else {

crates/gpui/src/text_layout.rs πŸ”—

@@ -266,6 +266,8 @@ impl Line {
         self.layout.len == 0
     }
 
+    /// index_for_x returns the character containing the given x coordinate.
+    /// (e.g. to handle a mouse-click)
     pub fn index_for_x(&self, x: f32) -> Option<usize> {
         if x >= self.layout.width {
             None
@@ -281,6 +283,28 @@ impl Line {
         }
     }
 
+    /// closest_index_for_x returns the character boundary closest to the given x coordinate
+    /// (e.g. to handle aligning up/down arrow keys)
+    pub fn closest_index_for_x(&self, x: f32) -> usize {
+        let mut prev_index = 0;
+        let mut prev_x = 0.0;
+
+        for run in self.layout.runs.iter() {
+            for glyph in run.glyphs.iter() {
+                if glyph.position.x() >= x {
+                    if glyph.position.x() - x < x - prev_x {
+                        return glyph.index;
+                    } else {
+                        return prev_index;
+                    }
+                }
+                prev_index = glyph.index;
+                prev_x = glyph.position.x();
+            }
+        }
+        prev_index
+    }
+
     pub fn paint(
         &self,
         origin: Vector2F,

crates/language/src/buffer.rs πŸ”—

@@ -201,7 +201,7 @@ pub struct CodeAction {
     pub lsp_action: lsp::CodeAction,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Operation {
     Buffer(text::Operation),
 
@@ -224,7 +224,7 @@ pub enum Operation {
     },
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     Operation(Operation),
     Edited,

crates/language/src/language.rs πŸ”—

@@ -226,8 +226,8 @@ impl CachedLspAdapter {
         self.adapter.label_for_symbol(name, kind, language).await
     }
 
-    pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        self.adapter.enabled_formatters()
+    pub fn prettier_plugins(&self) -> &[&'static str] {
+        self.adapter.prettier_plugins()
     }
 }
 
@@ -336,31 +336,8 @@ pub trait LspAdapter: 'static + Send + Sync {
         Default::default()
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        Vec::new()
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum BundledFormatter {
-    Prettier {
-        // See https://prettier.io/docs/en/options.html#parser for a list of valid values.
-        // Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
-        // There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
-        //
-        // But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
-        // For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
-        parser_name: Option<&'static str>,
-        plugin_names: Vec<&'static str>,
-    },
-}
-
-impl BundledFormatter {
-    pub fn prettier(parser_name: &'static str) -> Self {
-        Self::Prettier {
-            parser_name: Some(parser_name),
-            plugin_names: Vec::new(),
-        }
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &[]
     }
 }
 
@@ -398,6 +375,8 @@ pub struct LanguageConfig {
     pub overrides: HashMap<String, LanguageConfigOverride>,
     #[serde(default)]
     pub word_characters: HashSet<char>,
+    #[serde(default)]
+    pub prettier_parser_name: Option<String>,
 }
 
 #[derive(Debug, Default)]
@@ -471,6 +450,7 @@ impl Default for LanguageConfig {
             overrides: Default::default(),
             collapsed_placeholder: Default::default(),
             word_characters: Default::default(),
+            prettier_parser_name: None,
         }
     }
 }
@@ -496,7 +476,7 @@ pub struct FakeLspAdapter {
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub disk_based_diagnostics_sources: Vec<String>,
-    pub enabled_formatters: Vec<BundledFormatter>,
+    pub prettier_plugins: Vec<&'static str>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -1597,6 +1577,10 @@ impl Language {
             override_id: None,
         }
     }
+
+    pub fn prettier_parser_name(&self) -> Option<&str> {
+        self.config.prettier_parser_name.as_deref()
+    }
 }
 
 impl LanguageScope {
@@ -1759,7 +1743,7 @@ impl Default for FakeLspAdapter {
             disk_based_diagnostics_progress_token: None,
             initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
-            enabled_formatters: Vec::new(),
+            prettier_plugins: Vec::new(),
         }
     }
 }
@@ -1817,8 +1801,8 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         self.initialization_options.clone()
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        self.enabled_formatters.clone()
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &self.prettier_plugins
     }
 }
 

crates/prettier/src/prettier.rs πŸ”—

@@ -3,11 +3,11 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::Context;
-use collections::{HashMap, HashSet};
+use collections::HashMap;
 use fs::Fs;
 use gpui::{AsyncAppContext, ModelHandle};
 use language::language_settings::language_settings;
-use language::{Buffer, BundledFormatter, Diff};
+use language::{Buffer, Diff};
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
@@ -242,40 +242,16 @@ impl Prettier {
             Self::Real(local) => {
                 let params = buffer.read_with(cx, |buffer, cx| {
                     let buffer_language = buffer.language();
-                    let parsers_with_plugins = buffer_language
-                        .into_iter()
-                        .flat_map(|language| {
-                            language
-                                .lsp_adapters()
-                                .iter()
-                                .flat_map(|adapter| adapter.enabled_formatters())
-                                .filter_map(|formatter| match formatter {
-                                    BundledFormatter::Prettier {
-                                        parser_name,
-                                        plugin_names,
-                                    } => Some((parser_name, plugin_names)),
-                                })
-                        })
-                        .fold(
-                            HashMap::default(),
-                            |mut parsers_with_plugins, (parser_name, plugins)| {
-                                match parser_name {
-                                    Some(parser_name) => parsers_with_plugins
-                                        .entry(parser_name)
-                                        .or_insert_with(HashSet::default)
-                                        .extend(plugins),
-                                    None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
-                                        existing_plugins.extend(plugins.iter());
-                                    }),
-                                }
-                                parsers_with_plugins
-                            },
-                        );
-
-                    let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
-                    if parsers_with_plugins.len() > 1 {
-                        log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
-                    }
+                    let parser_with_plugins = buffer_language.and_then(|l| {
+                        let prettier_parser = l.prettier_parser_name()?;
+                        let mut prettier_plugins = l
+                            .lsp_adapters()
+                            .iter()
+                            .flat_map(|adapter| adapter.prettier_plugins())
+                            .collect::<Vec<_>>();
+                        prettier_plugins.dedup();
+                        Some((prettier_parser, prettier_plugins))
+                    });
 
                     let prettier_node_modules = self.prettier_dir().join("node_modules");
                     anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
@@ -296,7 +272,7 @@ impl Prettier {
                         }
                         None
                     };
-                    let (parser, located_plugins) = match selected_parser_with_plugins {
+                    let (parser, located_plugins) = match parser_with_plugins {
                         Some((parser, plugins)) => {
                             // Tailwind plugin requires being added last
                             // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins

crates/project/src/project.rs πŸ”—

@@ -39,11 +39,11 @@ use language::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version, split_operations,
     },
-    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
-    CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
-    Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
-    LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
-    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
+    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
+    File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
+    OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
+    ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
@@ -8352,12 +8352,7 @@ impl Project {
         let Some(buffer_language) = buffer.language() else {
             return Task::ready(None);
         };
-        if !buffer_language
-            .lsp_adapters()
-            .iter()
-            .flat_map(|adapter| adapter.enabled_formatters())
-            .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
-        {
+        if buffer_language.prettier_parser_name().is_none() {
             return Task::ready(None);
         }
 
@@ -8510,16 +8505,15 @@ impl Project {
         };
 
         let mut prettier_plugins = None;
-        for formatter in new_language
-            .lsp_adapters()
-            .into_iter()
-            .flat_map(|adapter| adapter.enabled_formatters())
-        {
-            match formatter {
-                BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
-                    .get_or_insert_with(|| HashSet::default())
-                    .extend(plugin_names),
-            }
+        if new_language.prettier_parser_name().is_some() {
+            prettier_plugins
+                .get_or_insert_with(|| HashSet::default())
+                .extend(
+                    new_language
+                        .lsp_adapters()
+                        .iter()
+                        .flat_map(|adapter| adapter.prettier_plugins()),
+                )
         }
         let Some(prettier_plugins) = prettier_plugins else {
             return Task::ready(Ok(()));

crates/search/src/project_search.rs πŸ”—

@@ -351,33 +351,32 @@ impl View for ProjectSearchView {
                     SemanticIndexStatus::NotAuthenticated => {
                         major_text = Cow::Borrowed("Not Authenticated");
                         show_minor_text = false;
-                        Some(
-                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
-                                .to_string(),
-                        )
+                        Some(vec![
+                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
+                                .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
                     }
-                    SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
+                    SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
                     SemanticIndexStatus::Indexing {
                         remaining_files,
                         rate_limit_expiry,
                     } => {
                         if remaining_files == 0 {
-                            Some(format!("Indexing..."))
+                            Some(vec![format!("Indexing...")])
                         } else {
                             if let Some(rate_limit_expiry) = rate_limit_expiry {
                                 let remaining_seconds =
                                     rate_limit_expiry.duration_since(Instant::now());
                                 if remaining_seconds > Duration::from_secs(0) {
-                                    Some(format!(
+                                    Some(vec![format!(
                                         "Remaining files to index (rate limit resets in {}s): {}",
                                         remaining_seconds.as_secs(),
                                         remaining_files
-                                    ))
+                                    )])
                                 } else {
-                                    Some(format!("Remaining files to index: {}", remaining_files))
+                                    Some(vec![format!("Remaining files to index: {}", remaining_files)])
                                 }
                             } else {
-                                Some(format!("Remaining files to index: {}", remaining_files))
+                                Some(vec![format!("Remaining files to index: {}", remaining_files)])
                             }
                         }
                     }
@@ -394,9 +393,11 @@ impl View for ProjectSearchView {
             } else {
                 match current_mode {
                     SearchMode::Semantic => {
-                        let mut minor_text = Vec::new();
+                        let mut minor_text: Vec<String> = Vec::new();
                         minor_text.push("".into());
-                        minor_text.extend(semantic_status);
+                        if let Some(semantic_status) = semantic_status {
+                            minor_text.extend(semantic_status);
+                        }
                         if show_minor_text {
                             minor_text
                                 .push("Simply explain the code you are looking to find.".into());

crates/semantic_index/src/semantic_index.rs πŸ”—

@@ -7,7 +7,10 @@ pub mod semantic_index_settings;
 mod semantic_index_tests;
 
 use crate::semantic_index_settings::SemanticIndexSettings;
-use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
+use ai::{
+    completion::OPENAI_API_URL,
+    embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings},
+};
 use anyhow::{anyhow, Result};
 use collections::{BTreeMap, HashMap, HashSet};
 use db::VectorDatabase;
@@ -55,6 +58,19 @@ pub fn init(
         .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
         .join("embeddings_db");
 
+    let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+        Some(api_key)
+    } else if let Some((_, api_key)) = cx
+        .platform()
+        .read_credentials(OPENAI_API_URL)
+        .log_err()
+        .flatten()
+    {
+        String::from_utf8(api_key).log_err()
+    } else {
+        None
+    };
+
     cx.subscribe_global::<WorkspaceCreated, _>({
         move |event, cx| {
             let Some(semantic_index) = SemanticIndex::global(cx) else {
@@ -88,7 +104,7 @@ pub fn init(
         let semantic_index = SemanticIndex::new(
             fs,
             db_file_path,
-            Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
+            Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())),
             language_registry,
             cx.clone(),
         )

crates/text/src/selection.rs πŸ”—

@@ -2,14 +2,15 @@ use crate::{Anchor, BufferSnapshot, TextDimension};
 use std::cmp::Ordering;
 use std::ops::Range;
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq)]
 pub enum SelectionGoal {
     None,
-    Column(u32),
-    ColumnRange { start: u32, end: u32 },
+    HorizontalPosition(f32),
+    HorizontalRange { start: f32, end: f32 },
+    WrappedHorizontalPosition((u32, f32)),
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct Selection<T> {
     pub id: usize,
     pub start: T,

crates/vcs_menu/Cargo.toml πŸ”—

@@ -7,6 +7,7 @@ publish = false
 
 [dependencies]
 fuzzy = {path = "../fuzzy"}
+fs = {path = "../fs"}
 gpui = {path = "../gpui"}
 picker = {path = "../picker"}
 util = {path = "../util"}

crates/vcs_menu/src/lib.rs πŸ”—

@@ -1,4 +1,5 @@
 use anyhow::{anyhow, bail, Result};
+use fs::repository::Branch;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
@@ -22,18 +23,9 @@ pub type BranchList = Picker<BranchListDelegate>;
 pub fn build_branch_list(
     workspace: ViewHandle<Workspace>,
     cx: &mut ViewContext<BranchList>,
-) -> BranchList {
-    Picker::new(
-        BranchListDelegate {
-            matches: vec![],
-            workspace,
-            selected_index: 0,
-            last_query: String::default(),
-            branch_name_trailoff_after: 29,
-        },
-        cx,
-    )
-    .with_theme(|theme| theme.picker.clone())
+) -> Result<BranchList> {
+    Ok(Picker::new(BranchListDelegate::new(workspace, 29, cx)?, cx)
+        .with_theme(|theme| theme.picker.clone()))
 }
 
 fn toggle(
@@ -43,31 +35,24 @@ fn toggle(
 ) -> Option<Task<Result<()>>> {
     Some(cx.spawn(|workspace, mut cx| async move {
         workspace.update(&mut cx, |workspace, cx| {
+            // Modal branch picker has a longer trailoff than a popover one.
+            let delegate = BranchListDelegate::new(cx.handle(), 70, cx)?;
             workspace.toggle_modal(cx, |_, cx| {
-                let workspace = cx.handle();
                 cx.add_view(|cx| {
-                    Picker::new(
-                        BranchListDelegate {
-                            matches: vec![],
-                            workspace,
-                            selected_index: 0,
-                            last_query: String::default(),
-                            /// Modal branch picker has a longer trailoff than a popover one.
-                            branch_name_trailoff_after: 70,
-                        },
-                        cx,
-                    )
-                    .with_theme(|theme| theme.picker.clone())
-                    .with_max_size(800., 1200.)
+                    Picker::new(delegate, cx)
+                        .with_theme(|theme| theme.picker.clone())
+                        .with_max_size(800., 1200.)
                 })
             });
-        })?;
+            Ok::<_, anyhow::Error>(())
+        })??;
         Ok(())
     }))
 }
 
 pub struct BranchListDelegate {
     matches: Vec<StringMatch>,
+    all_branches: Vec<Branch>,
     workspace: ViewHandle<Workspace>,
     selected_index: usize,
     last_query: String,
@@ -76,6 +61,31 @@ pub struct BranchListDelegate {
 }
 
 impl BranchListDelegate {
+    fn new(
+        workspace: ViewHandle<Workspace>,
+        branch_name_trailoff_after: usize,
+        cx: &AppContext,
+    ) -> Result<Self> {
+        let project = workspace.read(cx).project().read(&cx);
+
+        let Some(worktree) = project.visible_worktrees(cx).next() else {
+            bail!("Cannot update branch list as there are no visible worktrees")
+        };
+        let mut cwd = worktree.read(cx).abs_path().to_path_buf();
+        cwd.push(".git");
+        let Some(repo) = project.fs().open_repo(&cwd) else {
+            bail!("Project does not have associated git repository.")
+        };
+        let all_branches = repo.lock().branches()?;
+        Ok(Self {
+            matches: vec![],
+            workspace,
+            all_branches,
+            selected_index: 0,
+            last_query: Default::default(),
+            branch_name_trailoff_after,
+        })
+    }
     fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
         const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
         self.workspace.update(cx, |model, ctx| {
@@ -83,6 +93,7 @@ impl BranchListDelegate {
         });
     }
 }
+
 impl PickerDelegate for BranchListDelegate {
     fn placeholder_text(&self) -> Arc<str> {
         "Select branch...".into()
@@ -102,45 +113,28 @@ impl PickerDelegate for BranchListDelegate {
 
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         cx.spawn(move |picker, mut cx| async move {
-            let Some(candidates) = picker
-                .read_with(&mut cx, |view, cx| {
-                    let delegate = view.delegate();
-                    let project = delegate.workspace.read(cx).project().read(&cx);
-
-                    let Some(worktree) = project.visible_worktrees(cx).next() else {
-                        bail!("Cannot update branch list as there are no visible worktrees")
-                    };
-                    let mut cwd = worktree.read(cx).abs_path().to_path_buf();
-                    cwd.push(".git");
-                    let Some(repo) = project.fs().open_repo(&cwd) else {
-                        bail!("Project does not have associated git repository.")
-                    };
-                    let mut branches = repo.lock().branches()?;
-                    const RECENT_BRANCHES_COUNT: usize = 10;
-                    if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
-                        // Truncate list of recent branches
-                        // Do a partial sort to show recent-ish branches first.
-                        branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
-                            rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
-                        });
-                        branches.truncate(RECENT_BRANCHES_COUNT);
-                        branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
-                    }
-                    Ok(branches
-                        .iter()
-                        .cloned()
-                        .enumerate()
-                        .map(|(ix, command)| StringMatchCandidate {
-                            id: ix,
-                            char_bag: command.name.chars().collect(),
-                            string: command.name.into(),
-                        })
-                        .collect::<Vec<_>>())
-                })
-                .log_err()
-            else {
-                return;
-            };
+            let candidates = picker.read_with(&mut cx, |view, _| {
+                const RECENT_BRANCHES_COUNT: usize = 10;
+                let mut branches = view.delegate().all_branches.clone();
+                if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+                    // Truncate list of recent branches
+                    // Do a partial sort to show recent-ish branches first.
+                    branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+                        rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+                    });
+                    branches.truncate(RECENT_BRANCHES_COUNT);
+                    branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+                }
+                branches
+                    .into_iter()
+                    .enumerate()
+                    .map(|(ix, command)| StringMatchCandidate {
+                        id: ix,
+                        char_bag: command.name.chars().collect(),
+                        string: command.name.into(),
+                    })
+                    .collect::<Vec<StringMatchCandidate>>()
+            });
             let Some(candidates) = candidates.log_err() else {
                 return;
             };

crates/vim/src/motion.rs πŸ”—

@@ -1,9 +1,7 @@
-use std::cmp;
-
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
-    movement::{self, find_boundary, find_preceding_boundary, FindRange},
+    movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
     Bias, CharKind, DisplayPoint, ToOffset,
 };
 use gpui::{actions, impl_actions, AppContext, WindowContext};
@@ -361,6 +359,7 @@ impl Motion {
         point: DisplayPoint,
         goal: SelectionGoal,
         maybe_times: Option<usize>,
+        text_layout_details: &TextLayoutDetails,
     ) -> Option<(DisplayPoint, SelectionGoal)> {
         let times = maybe_times.unwrap_or(1);
         use Motion::*;
@@ -370,16 +369,16 @@ impl Motion {
             Backspace => (backspace(map, point, times), SelectionGoal::None),
             Down {
                 display_lines: false,
-            } => down(map, point, goal, times),
+            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
             Down {
                 display_lines: true,
-            } => down_display(map, point, goal, times),
+            } => down_display(map, point, goal, times, &text_layout_details),
             Up {
                 display_lines: false,
-            } => up(map, point, goal, times),
+            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
             Up {
                 display_lines: true,
-            } => up_display(map, point, goal, times),
+            } => up_display(map, point, goal, times, &text_layout_details),
             Right => (right(map, point, times), SelectionGoal::None),
             NextWordStart { ignore_punctuation } => (
                 next_word_start(map, point, *ignore_punctuation, times),
@@ -442,10 +441,15 @@ impl Motion {
         selection: &mut Selection<DisplayPoint>,
         times: Option<usize>,
         expand_to_surrounding_newline: bool,
+        text_layout_details: &TextLayoutDetails,
     ) -> bool {
-        if let Some((new_head, goal)) =
-            self.move_point(map, selection.head(), selection.goal, times)
-        {
+        if let Some((new_head, goal)) = self.move_point(
+            map,
+            selection.head(),
+            selection.goal,
+            times,
+            &text_layout_details,
+        ) {
             selection.set_head(new_head, goal);
 
             if self.linewise() {
@@ -530,35 +534,85 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
     point
 }
 
-fn down(
+pub(crate) fn start_of_relative_buffer_row(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    times: isize,
+) -> DisplayPoint {
+    let start = map.display_point_to_fold_point(point, Bias::Left);
+    let target = start.row() as isize + times;
+    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+    map.clip_point(
+        map.fold_point_to_display_point(
+            map.fold_snapshot
+                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
+        ),
+        Bias::Right,
+    )
+}
+
+fn up_down_buffer_rows(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     mut goal: SelectionGoal,
-    times: usize,
+    times: isize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     let start = map.display_point_to_fold_point(point, Bias::Left);
+    let begin_folded_line = map.fold_point_to_display_point(
+        map.fold_snapshot
+            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
+    );
+    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 
-    let goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
+    let (goal_wrap, goal_x) = match goal {
+        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
+        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
+        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
         _ => {
-            goal = SelectionGoal::Column(start.column());
-            start.column()
+            let x = map.x_for_point(point, text_layout_details);
+            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
+            (select_nth_wrapped_row, x)
         }
     };
 
-    let new_row = cmp::min(
-        start.row() + times as u32,
-        map.fold_snapshot.max_point().row(),
-    );
-    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(
+    let target = start.row() as isize + times;
+    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+    let mut begin_folded_line = map.fold_point_to_display_point(
         map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
+            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
     );
 
-    // clip twice to "clip at end of line"
-    (map.clip_point(point, Bias::Left), goal)
+    let mut i = 0;
+    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
+        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
+        if map
+            .display_point_to_fold_point(next_folded_line, Bias::Right)
+            .row()
+            == new_row
+        {
+            i += 1;
+            begin_folded_line = next_folded_line;
+        } else {
+            break;
+        }
+    }
+
+    let new_col = if i == goal_wrap {
+        map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
+    } else {
+        map.line_len(begin_folded_line.row())
+    };
+
+    (
+        map.clip_point(
+            DisplayPoint::new(begin_folded_line.row(), new_col),
+            Bias::Left,
+        ),
+        goal,
+    )
 }
 
 fn down_display(
@@ -566,49 +620,24 @@ fn down_display(
     mut point: DisplayPoint,
     mut goal: SelectionGoal,
     times: usize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     for _ in 0..times {
-        (point, goal) = movement::down(map, point, goal, true);
+        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
     }
 
     (point, goal)
 }
 
-pub(crate) fn up(
-    map: &DisplaySnapshot,
-    point: DisplayPoint,
-    mut goal: SelectionGoal,
-    times: usize,
-) -> (DisplayPoint, SelectionGoal) {
-    let start = map.display_point_to_fold_point(point, Bias::Left);
-
-    let goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => {
-            goal = SelectionGoal::Column(start.column());
-            start.column()
-        }
-    };
-
-    let new_row = start.row().saturating_sub(times as u32);
-    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(
-        map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
-    );
-
-    (map.clip_point(point, Bias::Left), goal)
-}
-
 fn up_display(
     map: &DisplaySnapshot,
     mut point: DisplayPoint,
     mut goal: SelectionGoal,
     times: usize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     for _ in 0..times {
-        (point, goal) = movement::up(map, point, goal, true);
+        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
     }
 
     (point, goal)
@@ -707,7 +736,7 @@ fn previous_word_start(
     point
 }
 
-fn first_non_whitespace(
+pub(crate) fn first_non_whitespace(
     map: &DisplaySnapshot,
     display_lines: bool,
     from: DisplayPoint,
@@ -886,13 +915,17 @@ fn find_backward(
 }
 
 fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
-    let correct_line = down(map, point, SelectionGoal::None, times).0;
+    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
     first_non_whitespace(map, false, correct_line)
 }
 
-fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+pub(crate) fn next_line_end(
+    map: &DisplaySnapshot,
+    mut point: DisplayPoint,
+    times: usize,
+) -> DisplayPoint {
     if times > 1 {
-        point = down(map, point, SelectionGoal::None, times - 1).0;
+        point = start_of_relative_buffer_row(map, point, times as isize - 1);
     }
     end_of_line(map, false, point)
 }

crates/vim/src/normal.rs πŸ”—

@@ -12,7 +12,7 @@ mod yank;
 use std::sync::Arc;
 
 use crate::{
-    motion::{self, Motion},
+    motion::{self, first_non_whitespace, next_line_end, right, Motion},
     object::Object,
     state::{Mode, Operator},
     Vim,
@@ -179,10 +179,11 @@ pub(crate) fn move_cursor(
     cx: &mut WindowContext,
 ) {
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_cursors_with(|map, cursor, goal| {
                 motion
-                    .move_point(map, cursor, goal, times)
+                    .move_point(map, cursor, goal, times, &text_layout_details)
                     .unwrap_or((cursor, goal))
             })
         })
@@ -195,9 +196,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::Right.move_point(map, cursor, goal, None)
-                });
+                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
             });
         });
     });
@@ -220,11 +219,11 @@ fn insert_first_non_whitespace(
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::FirstNonWhitespace {
-                        display_lines: false,
-                    }
-                    .move_point(map, cursor, goal, None)
+                s.move_cursors_with(|map, cursor, _| {
+                    (
+                        first_non_whitespace(map, false, cursor),
+                        SelectionGoal::None,
+                    )
                 });
             });
         });
@@ -237,8 +236,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::CurrentLine.move_point(map, cursor, goal, None)
+                s.move_cursors_with(|map, cursor, _| {
+                    (next_line_end(map, cursor, 1), SelectionGoal::None)
                 });
             });
         });
@@ -268,7 +267,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
                 editor.edit_with_autoindent(edits, cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_cursors_with(|map, cursor, _| {
-                        let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
+                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
                         let insert_point = motion::end_of_line(map, false, previous_line);
                         (insert_point, SelectionGoal::None)
                     });
@@ -283,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
                 let (map, old_selections) = editor.selections.all_display(cx);
 
@@ -301,7 +301,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
                 });
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.maybe_move_cursors_with(|map, cursor, goal| {
-                        Motion::CurrentLine.move_point(map, cursor, goal, None)
+                        Motion::CurrentLine.move_point(
+                            map,
+                            cursor,
+                            goal,
+                            None,
+                            &text_layout_details,
+                        )
                     });
                 });
                 editor.edit_with_autoindent(edits, cx);
@@ -399,12 +405,26 @@ mod test {
 
     #[gpui::test]
     async fn test_j(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
-        cx.assert_all(indoc! {"
-            ˇThe qˇuick broˇwn
-            Λ‡fox jumps"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+                    aaˇaa
+                    πŸ˜ƒπŸ˜ƒ"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j"]).await;
+        cx.assert_shared_state(indoc! {"
+                    aaaa
+                    πŸ˜ƒΛ‡πŸ˜ƒ"
         })
         .await;
+
+        for marked_position in cx.each_marked_position(indoc! {"
+                    ˇThe qˇuick broˇwn
+                    Λ‡fox jumps"
+        }) {
+            cx.assert_neovim_compatible(&marked_position, ["j"]).await;
+        }
     }
 
     #[gpui::test]

crates/vim/src/normal/change.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_
 use editor::{
     char_kind,
     display_map::DisplaySnapshot,
-    movement::{self, FindRange},
+    movement::{self, FindRange, TextLayoutDetails},
     scroll::autoscroll::Autoscroll,
     CharKind, DisplayPoint,
 };
@@ -20,6 +20,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
             | Motion::StartOfLine { .. }
     );
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             // We are swapping to insert mode anyway. Just set the line end clipping behavior now
             editor.set_clip_at_line_ends(false, cx);
@@ -27,9 +28,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                 s.move_with(|map, selection| {
                     motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
                     {
-                        expand_changed_word_selection(map, selection, times, ignore_punctuation)
+                        expand_changed_word_selection(
+                            map,
+                            selection,
+                            times,
+                            ignore_punctuation,
+                            &text_layout_details,
+                        )
                     } else {
-                        motion.expand_selection(map, selection, times, false)
+                        motion.expand_selection(map, selection, times, false, &text_layout_details)
                     };
                 });
             });
@@ -81,6 +88,7 @@ fn expand_changed_word_selection(
     selection: &mut Selection<DisplayPoint>,
     times: Option<usize>,
     ignore_punctuation: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> bool {
     if times.is_none() || times.unwrap() == 1 {
         let scope = map
@@ -103,11 +111,22 @@ fn expand_changed_word_selection(
                 });
             true
         } else {
-            Motion::NextWordStart { ignore_punctuation }
-                .expand_selection(map, selection, None, false)
+            Motion::NextWordStart { ignore_punctuation }.expand_selection(
+                map,
+                selection,
+                None,
+                false,
+                &text_layout_details,
+            )
         }
     } else {
-        Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
+        Motion::NextWordStart { ignore_punctuation }.expand_selection(
+            map,
+            selection,
+            times,
+            false,
+            &text_layout_details,
+        )
     }
 }
 

crates/vim/src/normal/delete.rs πŸ”—

@@ -7,6 +7,7 @@ use language::Point;
 pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.stop_recording();
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             let mut original_columns: HashMap<_, _> = Default::default();
@@ -14,7 +15,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                 s.move_with(|map, selection| {
                     let original_head = selection.head();
                     original_columns.insert(selection.id, original_head.column());
-                    motion.expand_selection(map, selection, times, true);
+                    motion.expand_selection(map, selection, times, true, &text_layout_details);
 
                     // Motion::NextWordStart on an empty line should delete it.
                     if let Motion::NextWordStart {

crates/vim/src/normal/increment.rs πŸ”—

@@ -255,8 +255,18 @@ mod test {
             4
             5"})
             .await;
-        cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g", "g", "ctrl-x"])
+
+        cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            Β«1Λ‡Β»
+            Β«2Λ‡Β»
+            Β«3Λ‡Β»  2
+            Β«4Λ‡Β»
+            Β«5Λ‡Β»"})
             .await;
+
+        cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await;
         cx.assert_shared_state(indoc! {"
             Λ‡0
             0

crates/vim/src/normal/paste.rs πŸ”—

@@ -30,6 +30,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
         vim.update_active_editor(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 
@@ -168,8 +169,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
                             let mut cursor = anchor.to_display_point(map);
                             if *line_mode {
                                 if !before {
-                                    cursor =
-                                        movement::down(map, cursor, SelectionGoal::None, false).0;
+                                    cursor = movement::down(
+                                        map,
+                                        cursor,
+                                        SelectionGoal::None,
+                                        false,
+                                        &text_layout_details,
+                                    )
+                                    .0;
                                 }
                                 cursor = movement::indented_line_beginning(map, cursor, true);
                             } else if !is_multiline {

crates/vim/src/normal/substitute.rs πŸ”—

@@ -32,10 +32,17 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
     vim.update_active_editor(cx, |editor, cx| {
         editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
                     if selection.start == selection.end {
-                        Motion::Right.expand_selection(map, selection, count, true);
+                        Motion::Right.expand_selection(
+                            map,
+                            selection,
+                            count,
+                            true,
+                            &text_layout_details,
+                        );
                     }
                     if line_mode {
                         // in Visual mode when the selection contains the newline at the end
@@ -43,7 +50,13 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
                         if !selection.is_empty() && selection.end.column() == 0 {
                             selection.end = movement::left(map, selection.end);
                         }
-                        Motion::CurrentLine.expand_selection(map, selection, None, false);
+                        Motion::CurrentLine.expand_selection(
+                            map,
+                            selection,
+                            None,
+                            false,
+                            &text_layout_details,
+                        );
                         if let Some((point, _)) = (Motion::FirstNonWhitespace {
                             display_lines: false,
                         })
@@ -52,6 +65,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
                             selection.start,
                             selection.goal,
                             None,
+                            &text_layout_details,
                         ) {
                             selection.start = point;
                         }

crates/vim/src/normal/yank.rs πŸ”—

@@ -4,6 +4,7 @@ use gpui::WindowContext;
 
 pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             let mut original_positions: HashMap<_, _> = Default::default();
@@ -11,7 +12,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
                 s.move_with(|map, selection| {
                     let original_position = (selection.head(), selection.goal);
                     original_positions.insert(selection.id, original_position);
-                    motion.expand_selection(map, selection, times, true);
+                    motion.expand_selection(map, selection, times, true, &text_layout_details);
                 });
             });
             copy_selections_content(editor, motion.linewise(), cx);

crates/vim/src/test.rs πŸ”—

@@ -653,6 +653,63 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
         .await;
 }
 
+#[gpui::test]
+async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_wrap(12).await;
+
+    cx.set_shared_state(indoc! {"
+                aaˇaa
+                πŸ˜ƒπŸ˜ƒ"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+                aaaa
+                πŸ˜ƒΛ‡πŸ˜ƒ"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+                123456789012aaˇaa
+                123456789012πŸ˜ƒπŸ˜ƒ"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaa
+        123456789012πŸ˜ƒΛ‡πŸ˜ƒ"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+                123456789012aaˇaa
+                123456789012πŸ˜ƒπŸ˜ƒ"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaa
+        123456789012πŸ˜ƒΛ‡πŸ˜ƒ"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+        123456789012aaaaˇaaaaaaaa123456789012
+        wow
+        123456789012πŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒ123456789012"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j", "j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaaaaaaaaaa123456789012
+        wow
+        123456789012πŸ˜ƒπŸ˜ƒΛ‡πŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒ123456789012"
+    })
+    .await;
+}
+
 #[gpui::test]
 async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
     let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/vim.rs πŸ”—

@@ -25,7 +25,7 @@ pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;
-use settings::{Setting, SettingsStore};
+use settings::{update_settings_file, Setting, SettingsStore};
 use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
 use std::{ops::Range, sync::Arc};
 use visual::{visual_block_motion, visual_replace};
@@ -48,6 +48,7 @@ actions!(
     vim,
     [Tab, Enter, Object, InnerObject, FindForward, FindBackward]
 );
+actions!(workspace, [ToggleVimMode]);
 impl_actions!(vim, [Number, SwitchMode, PushOperator]);
 
 #[derive(Copy, Clone, Debug)]
@@ -88,6 +89,14 @@ pub fn init(cx: &mut AppContext) {
         Vim::active_editor_input_ignored("\n".into(), cx)
     });
 
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
+        let fs = workspace.app_state().fs.clone();
+        let currently_enabled = settings::get::<VimModeSetting>(cx).0;
+        update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
+            *setting = Some(!currently_enabled)
+        })
+    });
+
     // Any time settings change, update vim mode to match. The Vim struct
     // will be initialized as disabled by default, so we filter its commands
     // out when starting up.
@@ -581,7 +590,7 @@ impl Setting for VimModeSetting {
 fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
-            if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
+            if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
                 vim.switch_mode(Mode::VisualBlock, false, cx);
             } else {
                 vim.switch_mode(Mode::Visual, false, cx)

crates/vim/src/visual.rs πŸ”—

@@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) {
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             if vim.state().mode == Mode::VisualBlock
                 && !matches!(
                     motion,
@@ -67,7 +68,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
             {
                 let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
                 visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
-                    motion.move_point(map, point, goal, times)
+                    motion.move_point(map, point, goal, times, &text_layout_details)
                 })
             } else {
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -89,9 +90,13 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
                             current_head = movement::left(map, selection.end)
                         }
 
-                        let Some((new_head, goal)) =
-                            motion.move_point(map, current_head, selection.goal, times)
-                        else {
+                        let Some((new_head, goal)) = motion.move_point(
+                            map,
+                            current_head,
+                            selection.goal,
+                            times,
+                            &text_layout_details,
+                        ) else {
                             return;
                         };
 
@@ -135,19 +140,23 @@ pub fn visual_block_motion(
         SelectionGoal,
     ) -> Option<(DisplayPoint, SelectionGoal)>,
 ) {
+    let text_layout_details = editor.text_layout_details(cx);
     editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
         let map = &s.display_map();
         let mut head = s.newest_anchor().head().to_display_point(map);
         let mut tail = s.oldest_anchor().tail().to_display_point(map);
 
+        let mut head_x = map.x_for_point(head, &text_layout_details);
+        let mut tail_x = map.x_for_point(tail, &text_layout_details);
+
         let (start, end) = match s.newest_anchor().goal {
-            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
-            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
-            _ => (tail.column(), head.column()),
+            SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
+            SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
+            _ => (tail_x, head_x),
         };
-        let goal = SelectionGoal::ColumnRange { start, end };
+        let mut goal = SelectionGoal::HorizontalRange { start, end };
 
-        let was_reversed = tail.column() > head.column();
+        let was_reversed = tail_x > head_x;
         if !was_reversed && !preserve_goal {
             head = movement::saturating_left(map, head);
         }
@@ -156,32 +165,56 @@ pub fn visual_block_motion(
             return;
         };
         head = new_head;
+        head_x = map.x_for_point(head, &text_layout_details);
 
-        let is_reversed = tail.column() > head.column();
+        let is_reversed = tail_x > head_x;
         if was_reversed && !is_reversed {
-            tail = movement::left(map, tail)
+            tail = movement::saturating_left(map, tail);
+            tail_x = map.x_for_point(tail, &text_layout_details);
         } else if !was_reversed && is_reversed {
-            tail = movement::right(map, tail)
+            tail = movement::saturating_right(map, tail);
+            tail_x = map.x_for_point(tail, &text_layout_details);
         }
         if !is_reversed && !preserve_goal {
-            head = movement::saturating_right(map, head)
+            head = movement::saturating_right(map, head);
+            head_x = map.x_for_point(head, &text_layout_details);
         }
 
-        let columns = if is_reversed {
-            head.column()..tail.column()
-        } else if head.column() == tail.column() {
-            head.column()..(head.column() + 1)
+        let positions = if is_reversed {
+            head_x..tail_x
         } else {
-            tail.column()..head.column()
+            tail_x..head_x
         };
 
+        if !preserve_goal {
+            goal = SelectionGoal::HorizontalRange {
+                start: positions.start,
+                end: positions.end,
+            };
+        }
+
         let mut selections = Vec::new();
         let mut row = tail.row();
 
         loop {
-            let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
-            let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
-            if columns.start <= map.line_len(row) {
+            let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
+            let start = DisplayPoint::new(
+                row,
+                layed_out_line.closest_index_for_x(positions.start) as u32,
+            );
+            let mut end = DisplayPoint::new(
+                row,
+                layed_out_line.closest_index_for_x(positions.end) as u32,
+            );
+            if end <= start {
+                if start.column() == map.line_len(start.row()) {
+                    end = start;
+                } else {
+                    end = movement::saturating_right(map, start);
+                }
+            }
+
+            if positions.start <= layed_out_line.width() {
                 let selection = Selection {
                     id: s.new_selection_id(),
                     start: start.to_point(map),
@@ -888,6 +921,28 @@ mod test {
         .await;
     }
 
+    #[gpui::test]
+    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "The Λ‡quick brown
+            fox jumps over
+            the lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "The «quˇ»ick brown
+            fox «juˇ»mps over
+            the lazy dog
+            "
+        })
+        .await;
+    }
+
     #[gpui::test]
     async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/test_data/test_increment_steps.json πŸ”—

@@ -9,6 +9,7 @@
 {"Key":"ctrl-v"}
 {"Key":"g"}
 {"Key":"g"}
+{"Get":{"state":"Β«1Λ‡Β»\nΒ«2Λ‡Β»\nΒ«3Λ‡Β»  2\nΒ«4Λ‡Β»\nΒ«5Λ‡Β»","mode":"VisualBlock"}}
 {"Key":"g"}
 {"Key":"ctrl-x"}
 {"Get":{"state":"Λ‡0\n0\n0  2\n0\n0","mode":"Normal"}}

crates/vim/test_data/test_j.json πŸ”—

@@ -1,3 +1,6 @@
+{"Put":{"state":"aaΛ‡aa\nπŸ˜ƒπŸ˜ƒ"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\nπŸ˜ƒΛ‡πŸ˜ƒ","mode":"Normal"}}
 {"Put":{"state":"Λ‡The quick brown\nfox jumps"}}
 {"Key":"j"}
 {"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}

crates/vim/test_data/test_visual_block_issue_2123.json πŸ”—

@@ -0,0 +1,5 @@
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"right"}
+{"Key":"down"}
+{"Get":{"state":"The «quˇ»ick brown\nfox «juˇ»mps over\nthe lazy dog\n","mode":"VisualBlock"}}

crates/vim/test_data/test_wrapped_motions.json πŸ”—

@@ -0,0 +1,15 @@
+{"SetOption":{"value":"wrap"}}
+{"SetOption":{"value":"columns=12"}}
+{"Put":{"state":"aaΛ‡aa\nπŸ˜ƒπŸ˜ƒ"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\nπŸ˜ƒΛ‡πŸ˜ƒ","mode":"Normal"}}
+{"Put":{"state":"123456789012aaΛ‡aa\n123456789012πŸ˜ƒπŸ˜ƒ"}}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaa\n123456789012πŸ˜ƒΛ‡πŸ˜ƒ","mode":"Normal"}}
+{"Put":{"state":"123456789012aaΛ‡aa\n123456789012πŸ˜ƒπŸ˜ƒ"}}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaa\n123456789012πŸ˜ƒΛ‡πŸ˜ƒ","mode":"Normal"}}
+{"Put":{"state":"123456789012aaaaΛ‡aaaaaaaa123456789012\nwow\n123456789012πŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒ123456789012"}}
+{"Key":"j"}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaaaaaaaaaa123456789012\nwow\n123456789012πŸ˜ƒπŸ˜ƒΛ‡πŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒ123456789012","mode":"Normal"}}

crates/zed/examples/semantic_index_eval.rs πŸ”—

@@ -1,3 +1,4 @@
+use ai::completion::OPENAI_API_URL;
 use ai::embedding::OpenAIEmbeddings;
 use anyhow::{anyhow, Result};
 use client::{self, UserStore};
@@ -17,6 +18,7 @@ use std::{cmp, env, fs};
 use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
 use util::http::{self};
 use util::paths::EMBEDDINGS_DIR;
+use util::ResultExt;
 use zed::languages;
 
 #[derive(Deserialize, Clone, Serialize)]
@@ -469,12 +471,26 @@ fn main() {
             .join("embeddings_db");
 
         let languages = languages.clone();
+
+        let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+            Some(api_key)
+        } else if let Some((_, api_key)) = cx
+            .platform()
+            .read_credentials(OPENAI_API_URL)
+            .log_err()
+            .flatten()
+        {
+            String::from_utf8(api_key).log_err()
+        } else {
+            None
+        };
+
         let fs = fs.clone();
         cx.spawn(|mut cx| async move {
             let semantic_index = SemanticIndex::new(
                 fs.clone(),
                 db_file_path,
-                Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
+                Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())),
                 languages.clone(),
                 cx.clone(),
             )

crates/zed/src/languages.rs πŸ”—

@@ -76,7 +76,10 @@ pub fn init(
         elixir::ElixirLspSetting::ElixirLs => language(
             "elixir",
             tree_sitter_elixir::language(),
-            vec![Arc::new(elixir::ElixirLspAdapter)],
+            vec![
+                Arc::new(elixir::ElixirLspAdapter),
+                Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+            ],
         ),
         elixir::ElixirLspSetting::NextLs => language(
             "elixir",
@@ -101,7 +104,10 @@ pub fn init(
     language(
         "heex",
         tree_sitter_heex::language(),
-        vec![Arc::new(elixir::ElixirLspAdapter)],
+        vec![
+            Arc::new(elixir::ElixirLspAdapter),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
     language(
         "json",
@@ -167,7 +173,10 @@ pub fn init(
     language(
         "erb",
         tree_sitter_embedded_template::language(),
-        vec![Arc::new(ruby::RubyLanguageServer)],
+        vec![
+            Arc::new(ruby::RubyLanguageServer),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
     language("scheme", tree_sitter_scheme::language(), vec![]);
     language("racket", tree_sitter_racket::language(), vec![]);
@@ -184,16 +193,18 @@ pub fn init(
     language(
         "svelte",
         tree_sitter_svelte::language(),
-        vec![Arc::new(svelte::SvelteLspAdapter::new(
-            node_runtime.clone(),
-        ))],
+        vec![
+            Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
     language(
         "php",
         tree_sitter_php::language(),
-        vec![Arc::new(php::IntelephenseLspAdapter::new(
-            node_runtime.clone(),
-        ))],
+        vec![
+            Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
 
     language("elm", tree_sitter_elm::language(), vec![]);

crates/zed/src/languages/css.rs πŸ”—

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -96,10 +96,6 @@ impl LspAdapter for CssLspAdapter {
             "provideFormatter": true
         }))
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("css")]
-    }
 }
 
 async fn get_cached_server_binary(

crates/zed/src/languages/elixir/config.toml πŸ”—

@@ -9,3 +9,8 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
     { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed/src/languages/heex/config.toml πŸ”—

@@ -5,3 +5,8 @@ brackets = [
     { start = "<", end = ">", close = true, newline = true },
 ]
 block_comment = ["<%!-- ", " --%>"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed/src/languages/html.rs πŸ”—

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -96,10 +96,6 @@ impl LspAdapter for HtmlLspAdapter {
             "provideFormatter": true
         }))
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("html")]
-    }
 }
 
 async fn get_cached_server_binary(

crates/zed/src/languages/json.rs πŸ”—

@@ -4,9 +4,7 @@ use collections::HashMap;
 use feature_flags::FeatureFlagAppExt;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
-use language::{
-    BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate,
-};
+use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -146,10 +144,6 @@ impl LspAdapter for JsonLspAdapter {
     async fn language_ids(&self) -> HashMap<String, String> {
         [("JSON".into(), "jsonc".into())].into_iter().collect()
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("json")]
-    }
 }
 
 async fn get_cached_server_binary(

crates/zed/src/languages/json/config.toml πŸ”—

@@ -7,3 +7,4 @@ brackets = [
     { start = "[", end = "]", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
+prettier_parser_name = "json"

crates/zed/src/languages/svelte.rs πŸ”—

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -96,11 +96,8 @@ impl LspAdapter for SvelteLspAdapter {
         }))
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::Prettier {
-            parser_name: Some("svelte"),
-            plugin_names: vec!["prettier-plugin-svelte"],
-        }]
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-svelte"]
     }
 }
 

crates/zed/src/languages/svelte/config.toml πŸ”—

@@ -12,7 +12,9 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "svelte"
 
-[overrides.element]
-line_comment = { remove = true }
-block_comment = ["{/* ", " */}"]
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed/src/languages/tailwind.rs πŸ”—

@@ -6,7 +6,7 @@ use futures::{
     FutureExt, StreamExt,
 };
 use gpui::AppContext;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
@@ -117,22 +117,21 @@ impl LspAdapter for TailwindLspAdapter {
     }
 
     async fn language_ids(&self) -> HashMap<String, String> {
-        HashMap::from_iter(
-            [
-                ("HTML".to_string(), "html".to_string()),
-                ("CSS".to_string(), "css".to_string()),
-                ("JavaScript".to_string(), "javascript".to_string()),
-                ("TSX".to_string(), "typescriptreact".to_string()),
-            ]
-            .into_iter(),
-        )
+        HashMap::from_iter([
+            ("HTML".to_string(), "html".to_string()),
+            ("CSS".to_string(), "css".to_string()),
+            ("JavaScript".to_string(), "javascript".to_string()),
+            ("TSX".to_string(), "typescriptreact".to_string()),
+            ("Svelte".to_string(), "svelte".to_string()),
+            ("Elixir".to_string(), "phoenix-heex".to_string()),
+            ("HEEX".to_string(), "phoenix-heex".to_string()),
+            ("ERB".to_string(), "erb".to_string()),
+            ("PHP".to_string(), "php".to_string()),
+        ])
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::Prettier {
-            parser_name: None,
-            plugin_names: vec!["prettier-plugin-tailwindcss"],
-        }]
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-tailwindcss"]
     }
 }
 

crates/zed/src/languages/tsx/config.toml πŸ”—

@@ -14,6 +14,7 @@ brackets = [
 ]
 word_characters = ["#", "$"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "typescript"
 
 [overrides.element]
 line_comment = { remove = true }

crates/zed/src/languages/typescript.rs πŸ”—

@@ -4,7 +4,7 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt};
 use gpui::AppContext;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::{CodeActionKind, LanguageServerBinary};
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
@@ -161,10 +161,6 @@ impl LspAdapter for TypeScriptLspAdapter {
             "provideFormatter": true
         }))
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("typescript")]
-    }
 }
 
 async fn get_cached_ts_server_binary(
@@ -313,10 +309,6 @@ impl LspAdapter for EsLintLspAdapter {
     async fn initialization_options(&self) -> Option<serde_json::Value> {
         None
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("babel")]
-    }
 }
 
 async fn get_cached_eslint_server_binary(

crates/zed/src/languages/yaml.rs πŸ”—

@@ -3,8 +3,7 @@ use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
 use language::{
-    language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter,
-    LspAdapterDelegate,
+    language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
 };
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
@@ -109,10 +108,6 @@ impl LspAdapter for YamlLspAdapter {
         }))
         .boxed()
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("yaml")]
-    }
 }
 
 async fn get_cached_server_binary(