Merge branch 'main' into notifications

This commit is contained in:
Max Brunsfeld 2023-10-22 12:25:30 +02:00
commit 234ccbe51f
73 changed files with 2196 additions and 961 deletions

2
Cargo.lock generated
View file

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

View file

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

View file

@ -1,2 +1,4 @@
pub mod completion; pub mod completion;
pub mod embedding; pub mod embedding;
pub mod models;
pub mod templates;

View file

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

View file

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

66
crates/ai/src/models.rs Normal file
View file

@ -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))
}
}

View file

@ -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);
}
}

View file

@ -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"))
}
}
}

View file

@ -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))
}
}

View file

@ -0,0 +1,5 @@
pub mod base;
pub mod file_context;
pub mod generate;
pub mod preamble;
pub mod repository_context;

View file

@ -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))
}
}
}

View file

@ -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))
}
}

View file

@ -1,12 +1,15 @@
use crate::{ use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
codegen::{self, Codegen, CodegenKind}, codegen::{self, Codegen, CodegenKind},
prompts::{generate_content_prompt, PromptCodeSnippet}, prompts::generate_content_prompt,
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
SavedMessage, SavedMessage,
}; };
use ai::completion::{ use ai::{
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL, completion::{
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
},
templates::repository_context::PromptCodeSnippet,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
@ -609,6 +612,18 @@ impl AssistantPanel {
let project = pending_assist.project.clone(); 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 self.inline_prompt_history
.retain(|prompt| prompt != user_prompt); .retain(|prompt| prompt != user_prompt);
self.inline_prompt_history.push_back(user_prompt.into()); self.inline_prompt_history.push_back(user_prompt.into());
@ -646,7 +661,19 @@ impl AssistantPanel {
None 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 user_prompt = user_prompt.to_string();
let snippets = if retrieve_context { let snippets = if retrieve_context {
@ -668,14 +695,7 @@ impl AssistantPanel {
let snippets = cx.spawn(|_, cx| async move { let snippets = cx.spawn(|_, cx| async move {
let mut snippets = Vec::new(); let mut snippets = Vec::new();
for result in search_results.await { for result in search_results.await {
snippets.push(PromptCodeSnippet::new(result, &cx)); snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx));
// snippets.push(result.buffer.read_with(&cx, |buffer, _| {
// buffer
// .snapshot()
// .text_for_range(result.range)
// .collect::<String>()
// }));
} }
snippets snippets
}); });
@ -696,11 +716,11 @@ impl AssistantPanel {
generate_content_prompt( generate_content_prompt(
user_prompt, user_prompt,
language_name, language_name,
&buffer, buffer,
range, range,
codegen_kind,
snippets, snippets,
model_name, model_name,
project_name,
) )
}); });
@ -717,18 +737,23 @@ impl AssistantPanel {
} }
cx.spawn(|_, mut cx| async move { 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 { messages.push(RequestMessage {
role: Role::User, role: Role::User,
content: prompt, content: prompt,
}); });
let request = OpenAIRequest { let request = OpenAIRequest {
model: model.full_name().into(), model: model.full_name().into(),
messages, messages,
stream: true, stream: true,
stop: vec!["|END|>".to_string()],
temperature,
}; };
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
anyhow::Ok(())
}) })
.detach(); .detach();
} }
@ -1718,6 +1743,8 @@ impl Conversation {
.map(|message| message.to_open_ai_message(self.buffer.read(cx))) .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
.collect(), .collect(),
stream: true, stream: true,
stop: vec![],
temperature: 1.0,
}; };
let stream = stream_completion(api_key, cx.background().clone(), request); let stream = stream_completion(api_key, cx.background().clone(), request);
@ -2002,6 +2029,8 @@ impl Conversation {
model: self.model.full_name().to_string(), model: self.model.full_name().to_string(),
messages: messages.collect(), messages: messages.collect(),
stream: true, stream: true,
stop: vec![],
temperature: 1.0,
}; };
let stream = stream_completion(api_key, cx.background().clone(), request); let stream = stream_completion(api_key, cx.background().clone(), request);

View file

@ -1,60 +1,13 @@
use crate::codegen::CodegenKind; use ai::models::{LanguageModel, OpenAILanguageModel};
use gpui::AsyncAppContext; 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 language::{BufferSnapshot, OffsetRangeExt, ToOffset};
use semantic_index::SearchResult;
use std::cmp::{self, Reverse}; use std::cmp::{self, Reverse};
use std::fmt::Write;
use std::ops::Range; use std::ops::Range;
use std::path::PathBuf; use std::sync::Arc;
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```")
}
}
#[allow(dead_code)] #[allow(dead_code)]
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String { 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( pub fn generate_content_prompt(
user_prompt: String, user_prompt: String,
language_name: Option<&str>, language_name: Option<&str>,
buffer: &BufferSnapshot, buffer: BufferSnapshot,
range: Range<impl ToOffset>, range: Range<usize>,
kind: CodegenKind,
search_results: Vec<PromptCodeSnippet>, search_results: Vec<PromptCodeSnippet>,
model: &str, model: &str,
) -> String { project_name: Option<String>,
const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; ) -> anyhow::Result<String> {
const RESERVED_TOKENS_FOR_GENERATION: usize = 1000; // Using new Prompt Templates
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
let mut prompts = Vec::new(); let lang_name = if let Some(language_name) = language_name {
let range = range.to_offset(buffer); Some(language_name.to_string())
// General Preamble
if let Some(language_name) = language_name {
prompts.push(format!("You're an expert {language_name} engineer.\n"));
} else { } else {
prompts.push("You're an expert engineer.\n".to_string()); None
}
// 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```"));
} 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
}; };
// TODO: let args = PromptArguments {
// - add repository name to snippet model: openai_model,
// - add file path language_name: lang_name.clone(),
// - add language project_name,
if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) { snippets: search_results.clone(),
let mut template = "You are working inside a large repository, here are a few code snippets that may be useful"; reserved_tokens: 1000,
buffer: Some(buffer),
selected_range: Some(range),
user_prompt: Some(user_prompt.clone()),
};
for search_result in search_results { let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
let mut snippet_prompt = template.to_string(); (PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
let snippet = search_result.to_string(); (
writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap(); 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)?;
let token_count = encoding anyhow::Ok(prompt)
.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;
}
}
}
prompts.join("\n")
} }
#[cfg(test)] #[cfg(test)]

View file

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

View file

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

View file

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

View file

@ -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()
}
}

View file

@ -5,22 +5,24 @@ mod tab_map;
mod wrap_map; mod wrap_map;
use crate::{ use crate::{
link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer, link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
MultiBufferSnapshot, ToOffset, ToPoint, EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
}; };
pub use block_map::{BlockMap, BlockPoint}; pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use fold_map::FoldMap; use fold_map::FoldMap;
use gpui::{ use gpui::{
color::Color, color::Color,
fonts::{FontId, HighlightStyle}, fonts::{FontId, HighlightStyle, Underline},
text_layout::{Line, RunStyle},
Entity, ModelContext, ModelHandle, Entity, ModelContext, ModelHandle,
}; };
use inlay_map::InlayMap; use inlay_map::InlayMap;
use language::{ use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, 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 sum_tree::{Bias, TreeMap};
use tab_map::TabMap; use tab_map::TabMap;
use wrap_map::WrapMap; use wrap_map::WrapMap;
@ -316,6 +318,12 @@ pub struct Highlights<'a> {
pub suggestion_highlight_style: Option<HighlightStyle>, 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 struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot, pub buffer_snapshot: MultiBufferSnapshot,
pub fold_snapshot: fold_map::FoldSnapshot, pub fold_snapshot: fold_map::FoldSnapshot,
@ -485,7 +493,7 @@ impl DisplaySnapshot {
language_aware: bool, language_aware: bool,
inlay_highlight_style: Option<HighlightStyle>, inlay_highlight_style: Option<HighlightStyle>,
suggestion_highlight_style: Option<HighlightStyle>, suggestion_highlight_style: Option<HighlightStyle>,
) -> DisplayChunks<'_> { ) -> DisplayChunks<'a> {
self.block_snapshot.chunks( self.block_snapshot.chunks(
display_rows, display_rows,
language_aware, 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( pub fn chars_at(
&self, &self,
mut point: DisplayPoint, mut point: DisplayPoint,
@ -869,12 +1011,16 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; 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 gpui::{color::Color, elements::*, test::observe, AppContext};
use language::{ use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, SelectionGoal, Buffer, Language, LanguageConfig, SelectionGoal,
}; };
use project::Project;
use rand::{prelude::*, Rng}; use rand::{prelude::*, Rng};
use settings::SettingsStore; use settings::SettingsStore;
use smol::stream::StreamExt; use smol::stream::StreamExt;
@ -1148,95 +1294,120 @@ pub mod tests {
} }
#[gpui::test(retries = 5)] #[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); 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 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 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)); let mut cx = EditorTestContext::new(cx).await;
assert_eq!( let editor = cx.editor.clone();
snapshot.text_chunks(0).collect::<String>(), let window = cx.window.clone();
"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 ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); cx.update_window(window, |cx| {
buffer.update(cx, |buffer, cx| { let text_layout_details =
buffer.edit([(ix..ix, "and ")], None, cx); editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
let font_cache = cx.font_cache().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.);
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(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)
);
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 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"
)
}); });
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] #[gpui::test]
@ -1731,6 +1902,9 @@ pub mod tests {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx)); cx.set_global(SettingsStore::test(cx));
language::init(cx); language::init(cx);
crate::init(cx);
Project::init_settings(cx);
theme::init((), cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| { cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f); store.update_user_settings::<AllLanguageSettings>(cx, f);
}); });

View file

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

View file

@ -19,8 +19,8 @@ use gpui::{
use indoc::indoc; use indoc::indoc;
use language::{ use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
LanguageRegistry, Override, Point, Override, Point,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings}; use project::project_settings::{LspSettings, ProjectSettings};
@ -851,7 +851,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
let view = cx let view = cx
.add_window(|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) build_editor(buffer.clone(), cx)
}) })
.root(cx); .root(cx);
@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
true, true,
cx, 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); view.move_right(&MoveRight, cx);
assert_eq!( assert_eq!(
@ -888,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
); );
view.move_down(&MoveDown, cx); 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!( assert_eq!(
view.selections.display_ranges(cx), view.selections.display_ranges(cx),
&[empty_range(1, "ab⋯".len())] &[empty_range(1, "ab⋯".len())]
@ -929,17 +934,18 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
view.selections.display_ranges(cx), view.selections.display_ranges(cx),
&[empty_range(1, "ab⋯e".len())] &[empty_range(1, "ab⋯e".len())]
); );
view.move_down(&MoveDown, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(2, "αβ⋯ε".len())]
);
view.move_up(&MoveUp, cx); view.move_up(&MoveUp, cx);
assert_eq!( assert_eq!(
view.selections.display_ranges(cx), view.selections.display_ranges(cx),
&[empty_range(0, "ⓐⓑ⋯ⓔ".len())] &[empty_range(1, "ab⋯e".len())]
); );
view.move_left(&MoveLeft, cx);
assert_eq!( view.move_up(&MoveUp, cx);
view.selections.display_ranges(cx),
&[empty_range(0, "ⓐⓑ⋯".len())]
);
view.move_left(&MoveLeft, cx);
assert_eq!( assert_eq!(
view.selections.display_ranges(cx), view.selections.display_ranges(cx),
&[empty_range(0, "ⓐⓑ".len())] &[empty_range(0, "ⓐⓑ".len())]
@ -949,6 +955,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
view.selections.display_ranges(cx), view.selections.display_ranges(cx),
&[empty_range(0, "".len())] &[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 { LanguageConfig {
name: "Rust".into(), name: "Rust".into(),
path_suffixes: vec!["rs".to_string()], 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() ..Default::default()
}, },
Some(tree_sitter_rust::language()), 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)), document_formatting_provider: Some(lsp::OneOf::Left(true)),
..Default::default() ..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() ..Default::default()
})) }))
.await; .await;
@ -7838,6 +7846,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
LanguageConfig { LanguageConfig {
name: "Rust".into(), name: "Rust".into(),
path_suffixes: vec!["rs".to_string()], path_suffixes: vec!["rs".to_string()],
prettier_parser_name: Some("test_parser".to_string()),
..Default::default() ..Default::default()
}, },
Some(tree_sitter_rust::language()), 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 test_plugin = "test_plugin";
let _ = language let _ = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
enabled_formatters: vec![BundledFormatter::Prettier { prettier_plugins: vec![test_plugin],
parser_name: Some("test_parser"),
plugin_names: vec![test_plugin],
}],
..Default::default() ..Default::default()
})) }))
.await; .await;

View file

@ -4,7 +4,7 @@ use super::{
MAX_LINE_LEN, MAX_LINE_LEN,
}; };
use crate::{ use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
editor_settings::ShowScrollbar, editor_settings::ShowScrollbar,
git::{diff_hunk_to_display, DisplayDiffHunk}, git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{ hover_popover::{
@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
use gpui::{ use gpui::{
color::Color, color::Color,
elements::*, elements::*,
fonts::{HighlightStyle, TextStyle, Underline}, fonts::TextStyle,
geometry::{ geometry::{
rect::RectF, rect::RectF,
vector::{vec2f, Vector2F}, vector::{vec2f, Vector2F},
@ -37,8 +37,7 @@ use gpui::{
use itertools::Itertools; use itertools::Itertools;
use json::json; use json::json;
use language::{ use language::{
language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
Selection,
}; };
use project::{ use project::{
project_settings::{GitGutterSetting, ProjectSettings}, project_settings::{GitGutterSetting, ProjectSettings},
@ -1584,56 +1583,7 @@ impl EditorElement {
.collect() .collect()
} else { } else {
let style = &self.style; let style = &self.style;
let chunks = snapshot let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
.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,
}
});
LineWithInvisibles::from_chunks( LineWithInvisibles::from_chunks(
chunks, chunks,
@ -1870,12 +1820,6 @@ impl EditorElement {
} }
} }
struct HighlightedChunk<'a> {
chunk: &'a str,
style: Option<HighlightStyle>,
is_tab: bool,
}
#[derive(Debug)] #[derive(Debug)]
pub struct LineWithInvisibles { pub struct LineWithInvisibles {
pub line: Line, pub line: Line,

View file

@ -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) { async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings { settings.defaults.inlay_hints = Some(InlayHintSettings {
@ -2400,11 +2400,13 @@ pub mod tests {
)); ));
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| { 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, 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:?}"); "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[0];
let above_query_range = &ranges[1]; let visible_query_range = &ranges[1];
let below_query_range = &ranges[2]; 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, 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:?}"); "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");

View file

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

View file

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

View file

@ -266,6 +266,8 @@ impl Line {
self.layout.len == 0 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> { pub fn index_for_x(&self, x: f32) -> Option<usize> {
if x >= self.layout.width { if x >= self.layout.width {
None 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( pub fn paint(
&self, &self,
origin: Vector2F, origin: Vector2F,

View file

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

View file

@ -226,8 +226,8 @@ impl CachedLspAdapter {
self.adapter.label_for_symbol(name, kind, language).await self.adapter.label_for_symbol(name, kind, language).await
} }
pub fn enabled_formatters(&self) -> Vec<BundledFormatter> { pub fn prettier_plugins(&self) -> &[&'static str] {
self.adapter.enabled_formatters() self.adapter.prettier_plugins()
} }
} }
@ -336,31 +336,8 @@ pub trait LspAdapter: 'static + Send + Sync {
Default::default() Default::default()
} }
fn enabled_formatters(&self) -> Vec<BundledFormatter> { fn prettier_plugins(&self) -> &[&'static str] {
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(),
}
} }
} }
@ -398,6 +375,8 @@ pub struct LanguageConfig {
pub overrides: HashMap<String, LanguageConfigOverride>, pub overrides: HashMap<String, LanguageConfigOverride>,
#[serde(default)] #[serde(default)]
pub word_characters: HashSet<char>, pub word_characters: HashSet<char>,
#[serde(default)]
pub prettier_parser_name: Option<String>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -471,6 +450,7 @@ impl Default for LanguageConfig {
overrides: Default::default(), overrides: Default::default(),
collapsed_placeholder: Default::default(), collapsed_placeholder: Default::default(),
word_characters: 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 initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
pub disk_based_diagnostics_progress_token: Option<String>, pub disk_based_diagnostics_progress_token: Option<String>,
pub disk_based_diagnostics_sources: Vec<String>, pub disk_based_diagnostics_sources: Vec<String>,
pub enabled_formatters: Vec<BundledFormatter>, pub prettier_plugins: Vec<&'static str>,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -1597,6 +1577,10 @@ impl Language {
override_id: None, override_id: None,
} }
} }
pub fn prettier_parser_name(&self) -> Option<&str> {
self.config.prettier_parser_name.as_deref()
}
} }
impl LanguageScope { impl LanguageScope {
@ -1759,7 +1743,7 @@ impl Default for FakeLspAdapter {
disk_based_diagnostics_progress_token: None, disk_based_diagnostics_progress_token: None,
initialization_options: None, initialization_options: None,
disk_based_diagnostics_sources: Vec::new(), 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() self.initialization_options.clone()
} }
fn enabled_formatters(&self) -> Vec<BundledFormatter> { fn prettier_plugins(&self) -> &[&'static str] {
self.enabled_formatters.clone() &self.prettier_plugins
} }
} }

View file

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

View file

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

View file

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

View file

@ -7,7 +7,10 @@ pub mod semantic_index_settings;
mod semantic_index_tests; mod semantic_index_tests;
use crate::semantic_index_settings::SemanticIndexSettings; 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 anyhow::{anyhow, Result};
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use db::VectorDatabase; use db::VectorDatabase;
@ -55,6 +58,19 @@ pub fn init(
.join(Path::new(RELEASE_CHANNEL_NAME.as_str())) .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
.join("embeddings_db"); .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, _>({ cx.subscribe_global::<WorkspaceCreated, _>({
move |event, cx| { move |event, cx| {
let Some(semantic_index) = SemanticIndex::global(cx) else { let Some(semantic_index) = SemanticIndex::global(cx) else {
@ -88,7 +104,7 @@ pub fn init(
let semantic_index = SemanticIndex::new( let semantic_index = SemanticIndex::new(
fs, fs,
db_file_path, db_file_path,
Arc::new(OpenAIEmbeddings::new(http_client, cx.background())), Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())),
language_registry, language_registry,
cx.clone(), cx.clone(),
) )

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ mod yank;
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
motion::{self, Motion}, motion::{self, first_non_whitespace, next_line_end, right, Motion},
object::Object, object::Object,
state::{Mode, Operator}, state::{Mode, Operator},
Vim, Vim,
@ -179,10 +179,11 @@ pub(crate) fn move_cursor(
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| { s.move_cursors_with(|map, cursor, goal| {
motion motion
.move_point(map, cursor, goal, times) .move_point(map, cursor, goal, times, &text_layout_details)
.unwrap_or((cursor, goal)) .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.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
Motion::Right.move_point(map, cursor, goal, None)
});
}); });
}); });
}); });
@ -220,11 +219,11 @@ fn insert_first_non_whitespace(
vim.switch_mode(Mode::Insert, false, cx); vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { s.move_cursors_with(|map, cursor, _| {
Motion::FirstNonWhitespace { (
display_lines: false, first_non_whitespace(map, false, cursor),
} SelectionGoal::None,
.move_point(map, cursor, goal, None) )
}); });
}); });
}); });
@ -237,8 +236,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
vim.switch_mode(Mode::Insert, false, cx); vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { s.move_cursors_with(|map, cursor, _| {
Motion::CurrentLine.move_point(map, cursor, goal, None) (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.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| { 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); let insert_point = motion::end_of_line(map, false, previous_line);
(insert_point, SelectionGoal::None) (insert_point, SelectionGoal::None)
}); });
@ -283,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
vim.start_recording(cx); vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx); vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(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| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { 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); editor.edit_with_autoindent(edits, cx);
@ -399,12 +405,26 @@ mod test {
#[gpui::test] #[gpui::test]
async fn test_j(cx: &mut gpui::TestAppContext) { async fn test_j(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]); let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert_all(indoc! {"
ˇThe qˇuick broˇwn cx.set_shared_state(indoc! {"
ˇfox jumps" aaˇaa
😃😃"
}) })
.await; .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] #[gpui::test]

View file

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

View file

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

View file

@ -255,8 +255,18 @@ mod test {
4 4
5"}) 5"})
.await; .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; .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! {" cx.assert_shared_state(indoc! {"
ˇ0 ˇ0
0 0

View file

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

View file

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

View file

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

View file

@ -653,6 +653,63 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
.await; .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] #[gpui::test]
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await; let mut cx = NeovimBackedTestContext::new(cx).await;

View file

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

View file

@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) {
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) { pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
if vim.state().mode == Mode::VisualBlock if vim.state().mode == Mode::VisualBlock
&& !matches!( && !matches!(
motion, 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 { .. }); let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { 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 { } else {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { 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) current_head = movement::left(map, selection.end)
} }
let Some((new_head, goal)) = let Some((new_head, goal)) = motion.move_point(
motion.move_point(map, current_head, selection.goal, times) map,
else { current_head,
selection.goal,
times,
&text_layout_details,
) else {
return; return;
}; };
@ -135,19 +140,23 @@ pub fn visual_block_motion(
SelectionGoal, SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>, ) -> Option<(DisplayPoint, SelectionGoal)>,
) { ) {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = &s.display_map(); let map = &s.display_map();
let mut head = s.newest_anchor().head().to_display_point(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 tail = s.oldest_anchor().tail().to_display_point(map);
let (start, end) = match s.newest_anchor().goal { let mut head_x = map.x_for_point(head, &text_layout_details);
SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), let mut tail_x = map.x_for_point(tail, &text_layout_details);
SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
_ => (tail.column(), head.column()),
};
let goal = SelectionGoal::ColumnRange { start, end };
let was_reversed = tail.column() > head.column(); let (start, end) = match s.newest_anchor().goal {
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
_ => (tail_x, head_x),
};
let mut goal = SelectionGoal::HorizontalRange { start, end };
let was_reversed = tail_x > head_x;
if !was_reversed && !preserve_goal { if !was_reversed && !preserve_goal {
head = movement::saturating_left(map, head); head = movement::saturating_left(map, head);
} }
@ -156,32 +165,56 @@ pub fn visual_block_motion(
return; return;
}; };
head = new_head; 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 { 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 { } 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 { 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 { let positions = if is_reversed {
head.column()..tail.column() head_x..tail_x
} else if head.column() == tail.column() {
head.column()..(head.column() + 1)
} else { } 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 selections = Vec::new();
let mut row = tail.row(); let mut row = tail.row();
loop { loop {
let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left); let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left); let start = DisplayPoint::new(
if columns.start <= map.line_len(row) { 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 { let selection = Selection {
id: s.new_selection_id(), id: s.new_selection_id(),
start: start.to_point(map), start: start.to_point(map),
@ -888,6 +921,28 @@ mod test {
.await; .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] #[gpui::test]
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await; let mut cx = NeovimBackedTestContext::new(cx).await;

View file

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

View file

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

View file

@ -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"}}

View file

@ -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"}}

View file

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

View file

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

View file

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

View file

@ -10,3 +10,4 @@ brackets = [
] ]
word_characters = ["-"] word_characters = ["-"]
block_comment = ["/* ", " */"] block_comment = ["/* ", " */"]
prettier_parser_name = "css"

View file

@ -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"] },
{ 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"]

View file

@ -5,3 +5,4 @@ brackets = [
{ start = "<", end = ">", close = true, newline = true }, { start = "<", end = ">", close = true, newline = true },
] ]
block_comment = ["<%#", "%>"] block_comment = ["<%#", "%>"]
scope_opt_in_language_servers = ["tailwindcss-language-server"]

View file

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

View file

@ -0,0 +1,4 @@
[
(attribute_value)
(quoted_attribute_value)
] @string

View file

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

View file

@ -11,3 +11,4 @@ brackets = [
{ start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
] ]
word_characters = ["-"] word_characters = ["-"]
prettier_parser_name = "html"

View file

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

View file

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

View file

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

View file

@ -11,3 +11,4 @@ brackets = [
] ]
collapsed_placeholder = "/* ... */" collapsed_placeholder = "/* ... */"
word_characters = ["$"] word_characters = ["$"]
scope_opt_in_language_servers = ["tailwindcss-language-server"]

View file

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

View file

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

View file

@ -0,0 +1,7 @@
(comment) @comment
[
(raw_text)
(attribute_value)
(quoted_attribute_value)
] @string

View file

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

View file

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

View file

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

View file

@ -13,3 +13,4 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
] ]
word_characters = ["#", "$"] word_characters = ["#", "$"]
prettier_parser_name = "typescript"

View file

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

View file

@ -9,3 +9,4 @@ brackets = [
] ]
increase_indent_pattern = ":\\s*[|>]?\\s*$" increase_indent_pattern = ":\\s*[|>]?\\s*$"
prettier_parser_name = "yaml"