mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-27 04:44:30 +00:00
Merge branch 'main' into notifications
This commit is contained in:
commit
234ccbe51f
73 changed files with 2196 additions and 961 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -91,6 +91,7 @@ dependencies = [
|
|||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"matrixmultiply",
|
||||
|
@ -9146,6 +9147,7 @@ name = "vcs_menu"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"picker",
|
||||
|
|
|
@ -11,6 +11,7 @@ doctest = false
|
|||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
language = { path = "../language" }
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
pub mod completion;
|
||||
pub mod embedding;
|
||||
pub mod models;
|
||||
pub mod templates;
|
||||
|
|
|
@ -53,6 +53,8 @@ pub struct OpenAIRequest {
|
|||
pub model: String,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
pub stop: Vec<String>,
|
||||
pub temperature: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
|
|||
use async_trait::async_trait;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::executor::Background;
|
||||
use gpui::serde_json;
|
||||
use gpui::{serde_json, ViewContext};
|
||||
use isahc::http::StatusCode;
|
||||
use isahc::prelude::Configurable;
|
||||
use isahc::{AsyncBody, Response};
|
||||
|
@ -20,9 +20,11 @@ use std::sync::Arc;
|
|||
use std::time::{Duration, Instant};
|
||||
use tiktoken_rs::{cl100k_base, CoreBPE};
|
||||
use util::http::{HttpClient, Request};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::completion::OPENAI_API_URL;
|
||||
|
||||
lazy_static! {
|
||||
static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
|
||||
static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
|
||||
}
|
||||
|
||||
|
@ -87,6 +89,7 @@ impl Embedding {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAIEmbeddings {
|
||||
pub api_key: Option<String>,
|
||||
pub client: Arc<dyn HttpClient>,
|
||||
pub executor: Arc<Background>,
|
||||
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
|
||||
|
@ -166,11 +169,36 @@ impl EmbeddingProvider for DummyEmbeddings {
|
|||
const OPENAI_INPUT_LIMIT: usize = 8190;
|
||||
|
||||
impl OpenAIEmbeddings {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Background>) -> Self {
|
||||
pub fn authenticate(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.api_key.is_none() {
|
||||
let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
|
||||
Some(api_key)
|
||||
} else if let Some((_, api_key)) = cx
|
||||
.platform()
|
||||
.read_credentials(OPENAI_API_URL)
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
String::from_utf8(api_key).log_err()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(api_key) = api_key {
|
||||
self.api_key = Some(api_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
api_key: Option<String>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
executor: Arc<Background>,
|
||||
) -> Self {
|
||||
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
|
||||
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
|
||||
|
||||
OpenAIEmbeddings {
|
||||
api_key,
|
||||
client,
|
||||
executor,
|
||||
rate_limit_count_rx,
|
||||
|
@ -237,8 +265,9 @@ impl OpenAIEmbeddings {
|
|||
#[async_trait]
|
||||
impl EmbeddingProvider for OpenAIEmbeddings {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
OPENAI_API_KEY.as_ref().is_some()
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
fn max_tokens_per_batch(&self) -> usize {
|
||||
50000
|
||||
}
|
||||
|
@ -265,9 +294,9 @@ impl EmbeddingProvider for OpenAIEmbeddings {
|
|||
const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
|
||||
const MAX_RETRIES: usize = 4;
|
||||
|
||||
let api_key = OPENAI_API_KEY
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("no api key"))?;
|
||||
let Some(api_key) = self.api_key.clone() else {
|
||||
return Err(anyhow!("no open ai key provided"));
|
||||
};
|
||||
|
||||
let mut request_number = 0;
|
||||
let mut rate_limiting = false;
|
||||
|
@ -276,7 +305,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
|
|||
while request_number < MAX_RETRIES {
|
||||
response = self
|
||||
.send_request(
|
||||
api_key,
|
||||
&api_key,
|
||||
spans.iter().map(|x| &**x).collect(),
|
||||
request_timeout,
|
||||
)
|
||||
|
|
66
crates/ai/src/models.rs
Normal file
66
crates/ai/src/models.rs
Normal 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))
|
||||
}
|
||||
}
|
350
crates/ai/src/templates/base.rs
Normal file
350
crates/ai/src/templates/base.rs
Normal 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);
|
||||
}
|
||||
}
|
160
crates/ai/src/templates/file_context.rs
Normal file
160
crates/ai/src/templates/file_context.rs
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
95
crates/ai/src/templates/generate.rs
Normal file
95
crates/ai/src/templates/generate.rs
Normal 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))
|
||||
}
|
||||
}
|
5
crates/ai/src/templates/mod.rs
Normal file
5
crates/ai/src/templates/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod base;
|
||||
pub mod file_context;
|
||||
pub mod generate;
|
||||
pub mod preamble;
|
||||
pub mod repository_context;
|
52
crates/ai/src/templates/preamble.rs
Normal file
52
crates/ai/src/templates/preamble.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
94
crates/ai/src/templates/repository_context.rs
Normal file
94
crates/ai/src/templates/repository_context.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
|
||||
codegen::{self, Codegen, CodegenKind},
|
||||
prompts::{generate_content_prompt, PromptCodeSnippet},
|
||||
prompts::generate_content_prompt,
|
||||
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
|
||||
SavedMessage,
|
||||
};
|
||||
use ai::completion::{
|
||||
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
|
||||
use ai::{
|
||||
completion::{
|
||||
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
|
||||
},
|
||||
templates::repository_context::PromptCodeSnippet,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Local};
|
||||
|
@ -609,6 +612,18 @@ impl AssistantPanel {
|
|||
|
||||
let project = pending_assist.project.clone();
|
||||
|
||||
let project_name = if let Some(project) = project.upgrade(cx) {
|
||||
Some(
|
||||
project
|
||||
.read(cx)
|
||||
.worktree_root_names(cx)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.inline_prompt_history
|
||||
.retain(|prompt| prompt != user_prompt);
|
||||
self.inline_prompt_history.push_back(user_prompt.into());
|
||||
|
@ -646,7 +661,19 @@ impl AssistantPanel {
|
|||
None
|
||||
};
|
||||
|
||||
let codegen_kind = codegen.read(cx).kind().clone();
|
||||
// Higher Temperature increases the randomness of model outputs.
|
||||
// If Markdown or No Language is Known, increase the randomness for more creative output
|
||||
// If Code, decrease temperature to get more deterministic outputs
|
||||
let temperature = if let Some(language) = language_name.clone() {
|
||||
if language.to_string() != "Markdown".to_string() {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let user_prompt = user_prompt.to_string();
|
||||
|
||||
let snippets = if retrieve_context {
|
||||
|
@ -668,14 +695,7 @@ impl AssistantPanel {
|
|||
let snippets = cx.spawn(|_, cx| async move {
|
||||
let mut snippets = Vec::new();
|
||||
for result in search_results.await {
|
||||
snippets.push(PromptCodeSnippet::new(result, &cx));
|
||||
|
||||
// snippets.push(result.buffer.read_with(&cx, |buffer, _| {
|
||||
// buffer
|
||||
// .snapshot()
|
||||
// .text_for_range(result.range)
|
||||
// .collect::<String>()
|
||||
// }));
|
||||
snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx));
|
||||
}
|
||||
snippets
|
||||
});
|
||||
|
@ -696,11 +716,11 @@ impl AssistantPanel {
|
|||
generate_content_prompt(
|
||||
user_prompt,
|
||||
language_name,
|
||||
&buffer,
|
||||
buffer,
|
||||
range,
|
||||
codegen_kind,
|
||||
snippets,
|
||||
model_name,
|
||||
project_name,
|
||||
)
|
||||
});
|
||||
|
||||
|
@ -717,18 +737,23 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
// I Don't know if we want to return a ? here.
|
||||
let prompt = prompt.await?;
|
||||
|
||||
messages.push(RequestMessage {
|
||||
role: Role::User,
|
||||
content: prompt,
|
||||
});
|
||||
|
||||
let request = OpenAIRequest {
|
||||
model: model.full_name().into(),
|
||||
messages,
|
||||
stream: true,
|
||||
stop: vec!["|END|>".to_string()],
|
||||
temperature,
|
||||
};
|
||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -1718,6 +1743,8 @@ impl Conversation {
|
|||
.map(|message| message.to_open_ai_message(self.buffer.read(cx)))
|
||||
.collect(),
|
||||
stream: true,
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
};
|
||||
|
||||
let stream = stream_completion(api_key, cx.background().clone(), request);
|
||||
|
@ -2002,6 +2029,8 @@ impl Conversation {
|
|||
model: self.model.full_name().to_string(),
|
||||
messages: messages.collect(),
|
||||
stream: true,
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
};
|
||||
|
||||
let stream = stream_completion(api_key, cx.background().clone(), request);
|
||||
|
|
|
@ -1,60 +1,13 @@
|
|||
use crate::codegen::CodegenKind;
|
||||
use gpui::AsyncAppContext;
|
||||
use ai::models::{LanguageModel, OpenAILanguageModel};
|
||||
use ai::templates::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::templates::file_context::FileContext;
|
||||
use ai::templates::generate::GenerateInlineContent;
|
||||
use ai::templates::preamble::EngineerPreamble;
|
||||
use ai::templates::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use semantic_index::SearchResult;
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use tiktoken_rs::ChatCompletionRequestMessage;
|
||||
|
||||
pub struct PromptCodeSnippet {
|
||||
path: Option<PathBuf>,
|
||||
language_name: Option<String>,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl PromptCodeSnippet {
|
||||
pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self {
|
||||
let (content, language_name, file_path) =
|
||||
search_result.buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let content = snapshot
|
||||
.text_for_range(search_result.range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let language_name = buffer
|
||||
.language()
|
||||
.and_then(|language| Some(language.name().to_string()));
|
||||
|
||||
let file_path = buffer
|
||||
.file()
|
||||
.and_then(|file| Some(file.path().to_path_buf()));
|
||||
|
||||
(content, language_name, file_path)
|
||||
});
|
||||
|
||||
PromptCodeSnippet {
|
||||
path: file_path,
|
||||
language_name,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for PromptCodeSnippet {
|
||||
fn to_string(&self) -> String {
|
||||
let path = self
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|path| Some(path.to_string_lossy().to_string()))
|
||||
.unwrap_or("".to_string());
|
||||
let language_name = self.language_name.clone().unwrap_or("".to_string());
|
||||
let content = self.content.clone();
|
||||
|
||||
format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
|
||||
}
|
||||
}
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
|
@ -170,138 +123,50 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
|
|||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<impl ToOffset>,
|
||||
kind: CodegenKind,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
) -> String {
|
||||
const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
|
||||
const RESERVED_TOKENS_FOR_GENERATION: usize = 1000;
|
||||
|
||||
let mut prompts = Vec::new();
|
||||
let range = range.to_offset(buffer);
|
||||
|
||||
// General Preamble
|
||||
if let Some(language_name) = language_name {
|
||||
prompts.push(format!("You're an expert {language_name} engineer.\n"));
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
prompts.push("You're an expert engineer.\n".to_string());
|
||||
}
|
||||
|
||||
// Snippets
|
||||
let mut snippet_position = prompts.len() - 1;
|
||||
|
||||
let mut content = String::new();
|
||||
content.extend(buffer.text_for_range(0..range.start));
|
||||
if range.start == range.end {
|
||||
content.push_str("<|START|>");
|
||||
} else {
|
||||
content.push_str("<|START|");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.clone()));
|
||||
if range.start != range.end {
|
||||
content.push_str("|END|>");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
||||
|
||||
prompts.push("The file you are currently working on has the following content:\n".to_string());
|
||||
|
||||
if let Some(language_name) = language_name {
|
||||
let language_name = language_name.to_lowercase();
|
||||
prompts.push(format!("```{language_name}\n{content}\n```"));
|
||||
} 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, ¤t_messages)
|
||||
{
|
||||
let max_token_count = tiktoken_rs::model::get_context_size(model);
|
||||
let intermediate_token_count = if max_token_count > current_token_count {
|
||||
max_token_count - current_token_count
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION {
|
||||
0
|
||||
} else {
|
||||
intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION
|
||||
}
|
||||
} else {
|
||||
// If tiktoken fails to count token count, assume we have no space remaining.
|
||||
0
|
||||
None
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - add repository name to snippet
|
||||
// - add file path
|
||||
// - add language
|
||||
if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) {
|
||||
let mut template = "You are working inside a large repository, here are a few code snippets that may be useful";
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
for search_result in search_results {
|
||||
let mut snippet_prompt = template.to_string();
|
||||
let snippet = search_result.to_string();
|
||||
writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap();
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
let token_count = encoding
|
||||
.encode_with_special_tokens(snippet_prompt.as_str())
|
||||
.len();
|
||||
if token_count <= remaining_token_count {
|
||||
if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT {
|
||||
prompts.insert(snippet_position, snippet_prompt);
|
||||
snippet_position += 1;
|
||||
remaining_token_count -= token_count;
|
||||
// If you have already added the template to the prompt, remove the template.
|
||||
template = "";
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompts.join("\n")
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -15,8 +15,8 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te
|
|||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
||||
tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
|
||||
Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||
LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use lsp::LanguageServerId;
|
||||
|
@ -4530,6 +4530,7 @@ async fn test_prettier_formatting_buffer(
|
|||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
prettier_parser_name: Some("test_parser".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
|
@ -4537,10 +4538,7 @@ async fn test_prettier_formatting_buffer(
|
|||
let test_plugin = "test_plugin";
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("test_parser"),
|
||||
plugin_names: vec![test_plugin],
|
||||
}],
|
||||
prettier_plugins: vec![test_plugin],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
|
|
@ -466,7 +466,11 @@ impl CollabTitlebarItem {
|
|||
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.branch_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let view = cx.add_view(|cx| build_branch_list(workspace, cx));
|
||||
let Some(view) =
|
||||
cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
PickerEvent::Dismiss => {
|
||||
|
|
|
@ -6,7 +6,6 @@ mod face_pile;
|
|||
pub mod notification_panel;
|
||||
pub mod notifications;
|
||||
mod panel_settings;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use call::{report_call_event_for_room, ActiveCall, Room};
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
|
@ -46,7 +45,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
collab_panel::init(cx);
|
||||
chat_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
||||
cx.add_global_action(toggle_screen_sharing);
|
||||
cx.add_global_action(toggle_mute);
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -5,22 +5,24 @@ mod tab_map;
|
|||
mod wrap_map;
|
||||
|
||||
use crate::{
|
||||
link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
|
||||
MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
|
||||
EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
fonts::{FontId, HighlightStyle},
|
||||
fonts::{FontId, HighlightStyle, Underline},
|
||||
text_layout::{Line, RunStyle},
|
||||
Entity, ModelContext, ModelHandle,
|
||||
};
|
||||
use inlay_map::InlayMap;
|
||||
use language::{
|
||||
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
|
||||
};
|
||||
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::TabMap;
|
||||
use wrap_map::WrapMap;
|
||||
|
@ -316,6 +318,12 @@ pub struct Highlights<'a> {
|
|||
pub suggestion_highlight_style: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
pub struct HighlightedChunk<'a> {
|
||||
pub chunk: &'a str,
|
||||
pub style: Option<HighlightStyle>,
|
||||
pub is_tab: bool,
|
||||
}
|
||||
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
pub fold_snapshot: fold_map::FoldSnapshot,
|
||||
|
@ -485,7 +493,7 @@ impl DisplaySnapshot {
|
|||
language_aware: bool,
|
||||
inlay_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> DisplayChunks<'_> {
|
||||
) -> DisplayChunks<'a> {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
|
@ -498,6 +506,140 @@ impl DisplaySnapshot {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn highlighted_chunks<'a>(
|
||||
&'a self,
|
||||
display_rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
style: &'a EditorStyle,
|
||||
) -> impl Iterator<Item = HighlightedChunk<'a>> {
|
||||
self.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(style.theme.hint),
|
||||
Some(style.theme.suggestion),
|
||||
)
|
||||
.map(|chunk| {
|
||||
let mut highlight_style = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(&style.syntax));
|
||||
|
||||
if let Some(chunk_highlight) = chunk.highlight_style {
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
highlight_style.highlight(chunk_highlight);
|
||||
} else {
|
||||
highlight_style = Some(chunk_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic_highlight = HighlightStyle::default();
|
||||
|
||||
if chunk.is_unnecessary {
|
||||
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
|
||||
}
|
||||
|
||||
if let Some(severity) = chunk.diagnostic_severity {
|
||||
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
|
||||
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
|
||||
let diagnostic_style = super::diagnostic_style(severity, true, style);
|
||||
diagnostic_highlight.underline = Some(Underline {
|
||||
color: Some(diagnostic_style.message.text.color),
|
||||
thickness: 1.0.into(),
|
||||
squiggly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
highlight_style.highlight(diagnostic_highlight);
|
||||
} else {
|
||||
highlight_style = Some(diagnostic_highlight);
|
||||
}
|
||||
|
||||
HighlightedChunk {
|
||||
chunk: chunk.text,
|
||||
style: highlight_style,
|
||||
is_tab: chunk.is_tab,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lay_out_line_for_row(
|
||||
&self,
|
||||
display_row: u32,
|
||||
TextLayoutDetails {
|
||||
font_cache,
|
||||
text_layout_cache,
|
||||
editor_style,
|
||||
}: &TextLayoutDetails,
|
||||
) -> Line {
|
||||
let mut styles = Vec::new();
|
||||
let mut line = String::new();
|
||||
let mut ended_in_newline = false;
|
||||
|
||||
let range = display_row..display_row + 1;
|
||||
for chunk in self.highlighted_chunks(range, false, editor_style) {
|
||||
line.push_str(chunk.chunk);
|
||||
|
||||
let text_style = if let Some(style) = chunk.style {
|
||||
editor_style
|
||||
.text
|
||||
.clone()
|
||||
.highlight(style, font_cache)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
|
||||
} else {
|
||||
Cow::Borrowed(&editor_style.text)
|
||||
};
|
||||
ended_in_newline = chunk.chunk.ends_with("\n");
|
||||
|
||||
styles.push((
|
||||
chunk.chunk.len(),
|
||||
RunStyle {
|
||||
font_id: text_style.font_id,
|
||||
color: text_style.color,
|
||||
underline: text_style.underline,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// our pixel positioning logic assumes each line ends in \n,
|
||||
// this is almost always true except for the last line which
|
||||
// may have no trailing newline.
|
||||
if !ended_in_newline && display_row == self.max_point().row() {
|
||||
line.push_str("\n");
|
||||
|
||||
styles.push((
|
||||
"\n".len(),
|
||||
RunStyle {
|
||||
font_id: editor_style.text.font_id,
|
||||
color: editor_style.text_color,
|
||||
underline: editor_style.text.underline,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
|
||||
}
|
||||
|
||||
pub fn x_for_point(
|
||||
&self,
|
||||
display_point: DisplayPoint,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> f32 {
|
||||
let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
|
||||
layout_line.x_for_index(display_point.column() as usize)
|
||||
}
|
||||
|
||||
pub fn column_for_x(
|
||||
&self,
|
||||
display_row: u32,
|
||||
x_coordinate: f32,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> u32 {
|
||||
let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
|
||||
layout_line.closest_index_for_x(x_coordinate) as u32
|
||||
}
|
||||
|
||||
pub fn chars_at(
|
||||
&self,
|
||||
mut point: DisplayPoint,
|
||||
|
@ -869,12 +1011,16 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::{movement, test::marked_display_snapshot};
|
||||
use crate::{
|
||||
movement,
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
};
|
||||
use gpui::{color::Color, elements::*, test::observe, AppContext};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
Buffer, Language, LanguageConfig, SelectionGoal,
|
||||
};
|
||||
use project::Project;
|
||||
use rand::{prelude::*, Rng};
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
|
@ -1148,95 +1294,120 @@ pub mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(retries = 5)]
|
||||
fn test_soft_wraps(cx: &mut AppContext) {
|
||||
async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
init_test(cx, |_| {});
|
||||
|
||||
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)
|
||||
cx.update(|cx| {
|
||||
init_test(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)
|
||||
);
|
||||
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 mut cx = EditorTestContext::new(cx).await;
|
||||
let editor = cx.editor.clone();
|
||||
let window = cx.window.clone();
|
||||
|
||||
let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(ix..ix, "and ")], None, cx);
|
||||
cx.update_window(window, |cx| {
|
||||
let text_layout_details =
|
||||
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]
|
||||
|
@ -1731,6 +1902,9 @@ pub mod tests {
|
|||
cx.foreground().forbid_parking();
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init((), cx);
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
||||
});
|
||||
|
|
|
@ -71,6 +71,7 @@ use link_go_to_definition::{
|
|||
};
|
||||
use log::error;
|
||||
use lsp::LanguageServerId;
|
||||
use movement::TextLayoutDetails;
|
||||
use multi_buffer::ToOffsetUtf16;
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
||||
|
@ -3476,6 +3477,14 @@ impl Editor {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails {
|
||||
TextLayoutDetails {
|
||||
font_cache: cx.font_cache().clone(),
|
||||
text_layout_cache: cx.text_layout_cache().clone(),
|
||||
editor_style: self.style(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn splice_inlay_hints(
|
||||
&self,
|
||||
to_remove: Vec<InlayId>,
|
||||
|
@ -5410,6 +5419,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.transact(cx, |this, cx| {
|
||||
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
||||
|
@ -5433,7 +5443,10 @@ impl Editor {
|
|||
|
||||
*head.column_mut() += 1;
|
||||
head = display_map.clip_point(head, Bias::Right);
|
||||
selection.collapse_to(head, SelectionGoal::Column(head.column()));
|
||||
let goal = SelectionGoal::HorizontalPosition(
|
||||
display_map.x_for_point(head, &text_layout_details),
|
||||
);
|
||||
selection.collapse_to(head, goal);
|
||||
|
||||
let transpose_start = display_map
|
||||
.buffer_snapshot
|
||||
|
@ -5697,13 +5710,21 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::up(map, selection.start, selection.goal, false);
|
||||
let (cursor, goal) = movement::up(
|
||||
map,
|
||||
selection.start,
|
||||
selection.goal,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
})
|
||||
|
@ -5731,22 +5752,33 @@ impl Editor {
|
|||
Autoscroll::fit()
|
||||
};
|
||||
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) =
|
||||
movement::up_by_rows(map, selection.end, row_count, selection.goal, false);
|
||||
let (cursor, goal) = movement::up_by_rows(
|
||||
map,
|
||||
selection.end,
|
||||
row_count,
|
||||
selection.goal,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
|
||||
s.move_heads_with(|map, head, goal| {
|
||||
movement::up(map, head, goal, false, &text_layout_details)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -5758,13 +5790,20 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::down(map, selection.end, selection.goal, false);
|
||||
let (cursor, goal) = movement::down(
|
||||
map,
|
||||
selection.end,
|
||||
selection.goal,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
});
|
||||
|
@ -5802,22 +5841,32 @@ impl Editor {
|
|||
Autoscroll::fit()
|
||||
};
|
||||
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) =
|
||||
movement::down_by_rows(map, selection.end, row_count, selection.goal, false);
|
||||
let (cursor, goal) = movement::down_by_rows(
|
||||
map,
|
||||
selection.end,
|
||||
row_count,
|
||||
selection.goal,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
|
||||
s.move_heads_with(|map, head, goal| {
|
||||
movement::down(map, head, goal, false, &text_layout_details)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -6336,11 +6385,14 @@ impl Editor {
|
|||
fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut selections = self.selections.all::<Point>(cx);
|
||||
let text_layout_details = self.text_layout_details(cx);
|
||||
let mut state = self.add_selections_state.take().unwrap_or_else(|| {
|
||||
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
|
||||
let range = oldest_selection.display_range(&display_map).sorted();
|
||||
let columns = cmp::min(range.start.column(), range.end.column())
|
||||
..cmp::max(range.start.column(), range.end.column());
|
||||
|
||||
let start_x = display_map.x_for_point(range.start, &text_layout_details);
|
||||
let end_x = display_map.x_for_point(range.end, &text_layout_details);
|
||||
let positions = start_x.min(end_x)..start_x.max(end_x);
|
||||
|
||||
selections.clear();
|
||||
let mut stack = Vec::new();
|
||||
|
@ -6348,8 +6400,9 @@ impl Editor {
|
|||
if let Some(selection) = self.selections.build_columnar_selection(
|
||||
&display_map,
|
||||
row,
|
||||
&columns,
|
||||
&positions,
|
||||
oldest_selection.reversed,
|
||||
&text_layout_details,
|
||||
) {
|
||||
stack.push(selection.id);
|
||||
selections.push(selection);
|
||||
|
@ -6377,12 +6430,15 @@ impl Editor {
|
|||
let range = selection.display_range(&display_map).sorted();
|
||||
debug_assert_eq!(range.start.row(), range.end.row());
|
||||
let mut row = range.start.row();
|
||||
let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal
|
||||
let positions = if let SelectionGoal::HorizontalRange { start, end } =
|
||||
selection.goal
|
||||
{
|
||||
start..end
|
||||
} else {
|
||||
cmp::min(range.start.column(), range.end.column())
|
||||
..cmp::max(range.start.column(), range.end.column())
|
||||
let start_x = display_map.x_for_point(range.start, &text_layout_details);
|
||||
let end_x = display_map.x_for_point(range.end, &text_layout_details);
|
||||
|
||||
start_x.min(end_x)..start_x.max(end_x)
|
||||
};
|
||||
|
||||
while row != end_row {
|
||||
|
@ -6395,8 +6451,9 @@ impl Editor {
|
|||
if let Some(new_selection) = self.selections.build_columnar_selection(
|
||||
&display_map,
|
||||
row,
|
||||
&columns,
|
||||
&positions,
|
||||
selection.reversed,
|
||||
&text_layout_details,
|
||||
) {
|
||||
state.stack.push(new_selection.id);
|
||||
if above {
|
||||
|
@ -6690,6 +6747,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.transact(cx, |this, cx| {
|
||||
let mut selections = this.selections.all::<Point>(cx);
|
||||
let mut edits = Vec::new();
|
||||
|
@ -6932,7 +6990,10 @@ impl Editor {
|
|||
point.row += 1;
|
||||
point = snapshot.clip_point(point, Bias::Left);
|
||||
let display_point = point.to_display_point(display_snapshot);
|
||||
(display_point, SelectionGoal::Column(display_point.column()))
|
||||
let goal = SelectionGoal::HorizontalPosition(
|
||||
display_snapshot.x_for_point(display_point, &text_layout_details),
|
||||
);
|
||||
(display_point, goal)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ use gpui::{
|
|||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||
BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
|
||||
LanguageRegistry, Override, Point,
|
||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
||||
Override, Point,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
|
@ -851,7 +851,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||
|
||||
let view = cx
|
||||
.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
|
||||
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
|
||||
build_editor(buffer.clone(), cx)
|
||||
})
|
||||
.root(cx);
|
||||
|
@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||
true,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε\n");
|
||||
assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
|
||||
|
||||
view.move_right(&MoveRight, cx);
|
||||
assert_eq!(
|
||||
|
@ -888,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||
);
|
||||
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(1, "ab⋯e".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(1, "ab⋯".len())]
|
||||
|
@ -929,17 +934,18 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||
view.selections.display_ranges(cx),
|
||||
&[empty_range(1, "ab⋯e".len())]
|
||||
);
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(2, "αβ⋯ε".len())]
|
||||
);
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐⓑ⋯ⓔ".len())]
|
||||
&[empty_range(1, "ab⋯e".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐⓑ⋯".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐⓑ".len())]
|
||||
|
@ -949,6 +955,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "ⓐ".len())]
|
||||
);
|
||||
view.move_left(&MoveLeft, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "".len())]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5084,6 +5095,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
|||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
// Enable Prettier formatting for the same buffer, and ensure
|
||||
// LSP is called instead of Prettier.
|
||||
prettier_parser_name: Some("test_parser".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
|
@ -5094,12 +5108,6 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
|||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
// Enable Prettier formatting for the same buffer, and ensure
|
||||
// LSP is called instead of Prettier.
|
||||
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("test_parser"),
|
||||
plugin_names: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
@ -7838,6 +7846,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
|||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
prettier_parser_name: Some("test_parser".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
|
@ -7846,10 +7855,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
|||
let test_plugin = "test_plugin";
|
||||
let _ = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("test_parser"),
|
||||
plugin_names: vec![test_plugin],
|
||||
}],
|
||||
prettier_plugins: vec![test_plugin],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
|
|
@ -4,7 +4,7 @@ use super::{
|
|||
MAX_LINE_LEN,
|
||||
};
|
||||
use crate::{
|
||||
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
|
||||
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
|
||||
editor_settings::ShowScrollbar,
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hover_popover::{
|
||||
|
@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
|
|||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
fonts::{HighlightStyle, TextStyle, Underline},
|
||||
fonts::TextStyle,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
|
@ -37,8 +37,7 @@ use gpui::{
|
|||
use itertools::Itertools;
|
||||
use json::json;
|
||||
use language::{
|
||||
language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
|
||||
Selection,
|
||||
language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
|
||||
};
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
|
@ -1584,56 +1583,7 @@ impl EditorElement {
|
|||
.collect()
|
||||
} else {
|
||||
let style = &self.style;
|
||||
let chunks = snapshot
|
||||
.chunks(
|
||||
rows.clone(),
|
||||
true,
|
||||
Some(style.theme.hint),
|
||||
Some(style.theme.suggestion),
|
||||
)
|
||||
.map(|chunk| {
|
||||
let mut highlight_style = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(&style.syntax));
|
||||
|
||||
if let Some(chunk_highlight) = chunk.highlight_style {
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
highlight_style.highlight(chunk_highlight);
|
||||
} else {
|
||||
highlight_style = Some(chunk_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic_highlight = HighlightStyle::default();
|
||||
|
||||
if chunk.is_unnecessary {
|
||||
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
|
||||
}
|
||||
|
||||
if let Some(severity) = chunk.diagnostic_severity {
|
||||
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
|
||||
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
|
||||
let diagnostic_style = super::diagnostic_style(severity, true, style);
|
||||
diagnostic_highlight.underline = Some(Underline {
|
||||
color: Some(diagnostic_style.message.text.color),
|
||||
thickness: 1.0.into(),
|
||||
squiggly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
highlight_style.highlight(diagnostic_highlight);
|
||||
} else {
|
||||
highlight_style = Some(diagnostic_highlight);
|
||||
}
|
||||
|
||||
HighlightedChunk {
|
||||
chunk: chunk.text,
|
||||
style: highlight_style,
|
||||
is_tab: chunk.is_tab,
|
||||
}
|
||||
});
|
||||
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
|
||||
|
||||
LineWithInvisibles::from_chunks(
|
||||
chunks,
|
||||
|
@ -1870,12 +1820,6 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
struct HighlightedChunk<'a> {
|
||||
chunk: &'a str,
|
||||
style: Option<HighlightStyle>,
|
||||
is_tab: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LineWithInvisibles {
|
||||
pub line: Line,
|
||||
|
|
|
@ -2138,7 +2138,7 @@ pub mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
|
@ -2400,11 +2400,13 @@ pub mod tests {
|
|||
));
|
||||
cx.foreground().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
|
||||
let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
|
||||
ranges.sort_by_key(|r| r.start);
|
||||
|
||||
assert_eq!(ranges.len(), 3,
|
||||
"On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
|
||||
let visible_query_range = &ranges[0];
|
||||
let above_query_range = &ranges[1];
|
||||
let above_query_range = &ranges[0];
|
||||
let visible_query_range = &ranges[1];
|
||||
let below_query_range = &ranges[2];
|
||||
assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
|
||||
"Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::{char_kind, CharKind, ToOffset, ToPoint};
|
||||
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
|
||||
use gpui::{FontCache, TextLayoutCache};
|
||||
use language::Point;
|
||||
use std::ops::Range;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum FindRange {
|
||||
|
@ -9,6 +10,14 @@ pub enum FindRange {
|
|||
MultiLine,
|
||||
}
|
||||
|
||||
/// TextLayoutDetails encompasses everything we need to move vertically
|
||||
/// taking into account variable width characters.
|
||||
pub struct TextLayoutDetails {
|
||||
pub font_cache: Arc<FontCache>,
|
||||
pub text_layout_cache: Arc<TextLayoutCache>,
|
||||
pub editor_style: EditorStyle,
|
||||
}
|
||||
|
||||
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
|
@ -47,8 +56,16 @@ pub fn up(
|
|||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
up_by_rows(map, start, 1, goal, preserve_column_at_start)
|
||||
up_by_rows(
|
||||
map,
|
||||
start,
|
||||
1,
|
||||
goal,
|
||||
preserve_column_at_start,
|
||||
text_layout_details,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn down(
|
||||
|
@ -56,8 +73,16 @@ pub fn down(
|
|||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
down_by_rows(map, start, 1, goal, preserve_column_at_end)
|
||||
down_by_rows(
|
||||
map,
|
||||
start,
|
||||
1,
|
||||
goal,
|
||||
preserve_column_at_end,
|
||||
text_layout_details,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn up_by_rows(
|
||||
|
@ -66,11 +91,13 @@ pub fn up_by_rows(
|
|||
row_count: u32,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => map.column_to_chars(start.row(), start.column()),
|
||||
let mut goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x,
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
|
||||
SelectionGoal::HorizontalRange { end, .. } => end,
|
||||
_ => map.x_for_point(start, text_layout_details),
|
||||
};
|
||||
|
||||
let prev_row = start.row().saturating_sub(row_count);
|
||||
|
@ -79,19 +106,19 @@ pub fn up_by_rows(
|
|||
Bias::Left,
|
||||
);
|
||||
if point.row() < start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
|
||||
} else if preserve_column_at_start {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = DisplayPoint::new(0, 0);
|
||||
goal_column = 0;
|
||||
goal_x = 0.0;
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Left);
|
||||
if clipped_point.row() < point.row() {
|
||||
clipped_point = map.clip_point(point, Bias::Right);
|
||||
}
|
||||
(clipped_point, SelectionGoal::Column(goal_column))
|
||||
(clipped_point, SelectionGoal::HorizontalPosition(goal_x))
|
||||
}
|
||||
|
||||
pub fn down_by_rows(
|
||||
|
@ -100,29 +127,31 @@ pub fn down_by_rows(
|
|||
row_count: u32,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => map.column_to_chars(start.row(), start.column()),
|
||||
let mut goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x,
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
|
||||
SelectionGoal::HorizontalRange { end, .. } => end,
|
||||
_ => map.x_for_point(start, text_layout_details),
|
||||
};
|
||||
|
||||
let new_row = start.row() + row_count;
|
||||
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
||||
if point.row() > start.row() {
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
|
||||
} else if preserve_column_at_end {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = map.max_point();
|
||||
goal_column = map.column_to_chars(point.row(), point.column())
|
||||
goal_x = map.x_for_point(point, text_layout_details)
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Right);
|
||||
if clipped_point.row() > point.row() {
|
||||
clipped_point = map.clip_point(point, Bias::Left);
|
||||
}
|
||||
(clipped_point, SelectionGoal::Column(goal_column))
|
||||
(clipped_point, SelectionGoal::HorizontalPosition(goal_x))
|
||||
}
|
||||
|
||||
pub fn line_beginning(
|
||||
|
@ -396,9 +425,11 @@ pub fn split_display_range_by_lines(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
|
||||
InlayId, MultiBuffer,
|
||||
display_map::Inlay,
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use util::post_inc;
|
||||
|
||||
|
@ -691,123 +722,173 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
|
||||
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
|
||||
async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
});
|
||||
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
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 2),
|
||||
SelectionGoal::Column(2),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::None,
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
|
||||
);
|
||||
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();
|
||||
|
||||
// Move up and down within first excerpt
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::Column(4),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 3),
|
||||
SelectionGoal::Column(4),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
||||
);
|
||||
let 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));
|
||||
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(6, 5),
|
||||
SelectionGoal::Column(5),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(3, 4),
|
||||
SelectionGoal::Column(5),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
|
||||
);
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||
|
||||
// Can't move down off the end
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(7, 0),
|
||||
SelectionGoal::Column(0),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(7, 2),
|
||||
SelectionGoal::Column(2),
|
||||
false
|
||||
),
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
|
||||
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 2),
|
||||
SelectionGoal::HorizontalPosition(col_2_x),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::None,
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
|
||||
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) {
|
||||
|
@ -815,5 +896,6 @@ mod tests {
|
|||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
Project::init_settings(cx);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
cell::Ref,
|
||||
cmp, iter, mem,
|
||||
iter, mem,
|
||||
ops::{Deref, DerefMut, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ use util::post_inc;
|
|||
|
||||
use crate::{
|
||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
movement::TextLayoutDetails,
|
||||
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
|
||||
};
|
||||
|
||||
|
@ -305,23 +306,29 @@ impl SelectionsCollection {
|
|||
&mut self,
|
||||
display_map: &DisplaySnapshot,
|
||||
row: u32,
|
||||
columns: &Range<u32>,
|
||||
positions: &Range<f32>,
|
||||
reversed: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> Option<Selection<Point>> {
|
||||
let is_empty = columns.start == columns.end;
|
||||
let is_empty = positions.start == positions.end;
|
||||
let line_len = display_map.line_len(row);
|
||||
if columns.start < line_len || (is_empty && columns.start == line_len) {
|
||||
let start = DisplayPoint::new(row, columns.start);
|
||||
let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
|
||||
|
||||
let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
|
||||
|
||||
let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
|
||||
if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
|
||||
let start = DisplayPoint::new(row, start_col);
|
||||
let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
|
||||
let end = DisplayPoint::new(row, end_col);
|
||||
|
||||
Some(Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
start: start.to_point(display_map),
|
||||
end: end.to_point(display_map),
|
||||
reversed,
|
||||
goal: SelectionGoal::ColumnRange {
|
||||
start: columns.start,
|
||||
end: columns.end,
|
||||
goal: SelectionGoal::HorizontalRange {
|
||||
start: positions.start,
|
||||
end: positions.end,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
|
|
@ -266,6 +266,8 @@ impl Line {
|
|||
self.layout.len == 0
|
||||
}
|
||||
|
||||
/// index_for_x returns the character containing the given x coordinate.
|
||||
/// (e.g. to handle a mouse-click)
|
||||
pub fn index_for_x(&self, x: f32) -> Option<usize> {
|
||||
if x >= self.layout.width {
|
||||
None
|
||||
|
@ -281,6 +283,28 @@ impl Line {
|
|||
}
|
||||
}
|
||||
|
||||
/// closest_index_for_x returns the character boundary closest to the given x coordinate
|
||||
/// (e.g. to handle aligning up/down arrow keys)
|
||||
pub fn closest_index_for_x(&self, x: f32) -> usize {
|
||||
let mut prev_index = 0;
|
||||
let mut prev_x = 0.0;
|
||||
|
||||
for run in self.layout.runs.iter() {
|
||||
for glyph in run.glyphs.iter() {
|
||||
if glyph.position.x() >= x {
|
||||
if glyph.position.x() - x < x - prev_x {
|
||||
return glyph.index;
|
||||
} else {
|
||||
return prev_index;
|
||||
}
|
||||
}
|
||||
prev_index = glyph.index;
|
||||
prev_x = glyph.position.x();
|
||||
}
|
||||
}
|
||||
prev_index
|
||||
}
|
||||
|
||||
pub fn paint(
|
||||
&self,
|
||||
origin: Vector2F,
|
||||
|
|
|
@ -201,7 +201,7 @@ pub struct CodeAction {
|
|||
pub lsp_action: lsp::CodeAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Operation {
|
||||
Buffer(text::Operation),
|
||||
|
||||
|
@ -224,7 +224,7 @@ pub enum Operation {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Event {
|
||||
Operation(Operation),
|
||||
Edited,
|
||||
|
|
|
@ -226,8 +226,8 @@ impl CachedLspAdapter {
|
|||
self.adapter.label_for_symbol(name, kind, language).await
|
||||
}
|
||||
|
||||
pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
self.adapter.enabled_formatters()
|
||||
pub fn prettier_plugins(&self) -> &[&'static str] {
|
||||
self.adapter.prettier_plugins()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,31 +336,8 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
Default::default()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BundledFormatter {
|
||||
Prettier {
|
||||
// See https://prettier.io/docs/en/options.html#parser for a list of valid values.
|
||||
// Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
|
||||
// There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
|
||||
//
|
||||
// But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
|
||||
// For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
|
||||
parser_name: Option<&'static str>,
|
||||
plugin_names: Vec<&'static str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl BundledFormatter {
|
||||
pub fn prettier(parser_name: &'static str) -> Self {
|
||||
Self::Prettier {
|
||||
parser_name: Some(parser_name),
|
||||
plugin_names: Vec::new(),
|
||||
}
|
||||
fn prettier_plugins(&self) -> &[&'static str] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,6 +375,8 @@ pub struct LanguageConfig {
|
|||
pub overrides: HashMap<String, LanguageConfigOverride>,
|
||||
#[serde(default)]
|
||||
pub word_characters: HashSet<char>,
|
||||
#[serde(default)]
|
||||
pub prettier_parser_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
@ -471,6 +450,7 @@ impl Default for LanguageConfig {
|
|||
overrides: Default::default(),
|
||||
collapsed_placeholder: Default::default(),
|
||||
word_characters: Default::default(),
|
||||
prettier_parser_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -496,7 +476,7 @@ pub struct FakeLspAdapter {
|
|||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
pub disk_based_diagnostics_sources: Vec<String>,
|
||||
pub enabled_formatters: Vec<BundledFormatter>,
|
||||
pub prettier_plugins: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -1597,6 +1577,10 @@ impl Language {
|
|||
override_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prettier_parser_name(&self) -> Option<&str> {
|
||||
self.config.prettier_parser_name.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageScope {
|
||||
|
@ -1759,7 +1743,7 @@ impl Default for FakeLspAdapter {
|
|||
disk_based_diagnostics_progress_token: None,
|
||||
initialization_options: None,
|
||||
disk_based_diagnostics_sources: Vec::new(),
|
||||
enabled_formatters: Vec::new(),
|
||||
prettier_plugins: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1817,8 +1801,8 @@ impl LspAdapter for Arc<FakeLspAdapter> {
|
|||
self.initialization_options.clone()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
self.enabled_formatters.clone()
|
||||
fn prettier_plugins(&self) -> &[&'static str] {
|
||||
&self.prettier_plugins
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncAppContext, ModelHandle};
|
||||
use language::language_settings::language_settings;
|
||||
use language::{Buffer, BundledFormatter, Diff};
|
||||
use language::{Buffer, Diff};
|
||||
use lsp::{LanguageServer, LanguageServerId};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -242,40 +242,16 @@ impl Prettier {
|
|||
Self::Real(local) => {
|
||||
let params = buffer.read_with(cx, |buffer, cx| {
|
||||
let buffer_language = buffer.language();
|
||||
let parsers_with_plugins = buffer_language
|
||||
.into_iter()
|
||||
.flat_map(|language| {
|
||||
language
|
||||
.lsp_adapters()
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.enabled_formatters())
|
||||
.filter_map(|formatter| match formatter {
|
||||
BundledFormatter::Prettier {
|
||||
parser_name,
|
||||
plugin_names,
|
||||
} => Some((parser_name, plugin_names)),
|
||||
})
|
||||
})
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut parsers_with_plugins, (parser_name, plugins)| {
|
||||
match parser_name {
|
||||
Some(parser_name) => parsers_with_plugins
|
||||
.entry(parser_name)
|
||||
.or_insert_with(HashSet::default)
|
||||
.extend(plugins),
|
||||
None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
|
||||
existing_plugins.extend(plugins.iter());
|
||||
}),
|
||||
}
|
||||
parsers_with_plugins
|
||||
},
|
||||
);
|
||||
|
||||
let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
|
||||
if parsers_with_plugins.len() > 1 {
|
||||
log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
|
||||
}
|
||||
let parser_with_plugins = buffer_language.and_then(|l| {
|
||||
let prettier_parser = l.prettier_parser_name()?;
|
||||
let mut prettier_plugins = l
|
||||
.lsp_adapters()
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.prettier_plugins())
|
||||
.collect::<Vec<_>>();
|
||||
prettier_plugins.dedup();
|
||||
Some((prettier_parser, prettier_plugins))
|
||||
});
|
||||
|
||||
let prettier_node_modules = self.prettier_dir().join("node_modules");
|
||||
anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
|
||||
|
@ -296,7 +272,7 @@ impl Prettier {
|
|||
}
|
||||
None
|
||||
};
|
||||
let (parser, located_plugins) = match selected_parser_with_plugins {
|
||||
let (parser, located_plugins) = match parser_with_plugins {
|
||||
Some((parser, plugins)) => {
|
||||
// Tailwind plugin requires being added last
|
||||
// https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
|
||||
|
|
|
@ -39,11 +39,11 @@ use language::{
|
|||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
serialize_anchor, serialize_version, split_operations,
|
||||
},
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
|
||||
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
|
||||
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
|
||||
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
|
||||
OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
|
||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
|
@ -8352,12 +8352,7 @@ impl Project {
|
|||
let Some(buffer_language) = buffer.language() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
if !buffer_language
|
||||
.lsp_adapters()
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.enabled_formatters())
|
||||
.any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
|
||||
{
|
||||
if buffer_language.prettier_parser_name().is_none() {
|
||||
return Task::ready(None);
|
||||
}
|
||||
|
||||
|
@ -8510,16 +8505,15 @@ impl Project {
|
|||
};
|
||||
|
||||
let mut prettier_plugins = None;
|
||||
for formatter in new_language
|
||||
.lsp_adapters()
|
||||
.into_iter()
|
||||
.flat_map(|adapter| adapter.enabled_formatters())
|
||||
{
|
||||
match formatter {
|
||||
BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
|
||||
.get_or_insert_with(|| HashSet::default())
|
||||
.extend(plugin_names),
|
||||
}
|
||||
if new_language.prettier_parser_name().is_some() {
|
||||
prettier_plugins
|
||||
.get_or_insert_with(|| HashSet::default())
|
||||
.extend(
|
||||
new_language
|
||||
.lsp_adapters()
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.prettier_plugins()),
|
||||
)
|
||||
}
|
||||
let Some(prettier_plugins) = prettier_plugins else {
|
||||
return Task::ready(Ok(()));
|
||||
|
|
|
@ -351,33 +351,32 @@ impl View for ProjectSearchView {
|
|||
SemanticIndexStatus::NotAuthenticated => {
|
||||
major_text = Cow::Borrowed("Not Authenticated");
|
||||
show_minor_text = false;
|
||||
Some(
|
||||
"API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
|
||||
.to_string(),
|
||||
)
|
||||
Some(vec![
|
||||
"API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
|
||||
.to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
|
||||
}
|
||||
SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
|
||||
SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
|
||||
SemanticIndexStatus::Indexing {
|
||||
remaining_files,
|
||||
rate_limit_expiry,
|
||||
} => {
|
||||
if remaining_files == 0 {
|
||||
Some(format!("Indexing..."))
|
||||
Some(vec![format!("Indexing...")])
|
||||
} else {
|
||||
if let Some(rate_limit_expiry) = rate_limit_expiry {
|
||||
let remaining_seconds =
|
||||
rate_limit_expiry.duration_since(Instant::now());
|
||||
if remaining_seconds > Duration::from_secs(0) {
|
||||
Some(format!(
|
||||
Some(vec![format!(
|
||||
"Remaining files to index (rate limit resets in {}s): {}",
|
||||
remaining_seconds.as_secs(),
|
||||
remaining_files
|
||||
))
|
||||
)])
|
||||
} else {
|
||||
Some(format!("Remaining files to index: {}", remaining_files))
|
||||
Some(vec![format!("Remaining files to index: {}", remaining_files)])
|
||||
}
|
||||
} else {
|
||||
Some(format!("Remaining files to index: {}", remaining_files))
|
||||
Some(vec![format!("Remaining files to index: {}", remaining_files)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -394,9 +393,11 @@ impl View for ProjectSearchView {
|
|||
} else {
|
||||
match current_mode {
|
||||
SearchMode::Semantic => {
|
||||
let mut minor_text = Vec::new();
|
||||
let mut minor_text: Vec<String> = Vec::new();
|
||||
minor_text.push("".into());
|
||||
minor_text.extend(semantic_status);
|
||||
if let Some(semantic_status) = semantic_status {
|
||||
minor_text.extend(semantic_status);
|
||||
}
|
||||
if show_minor_text {
|
||||
minor_text
|
||||
.push("Simply explain the code you are looking to find.".into());
|
||||
|
|
|
@ -7,7 +7,10 @@ pub mod semantic_index_settings;
|
|||
mod semantic_index_tests;
|
||||
|
||||
use crate::semantic_index_settings::SemanticIndexSettings;
|
||||
use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
|
||||
use ai::{
|
||||
completion::OPENAI_API_URL,
|
||||
embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::VectorDatabase;
|
||||
|
@ -55,6 +58,19 @@ pub fn init(
|
|||
.join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
|
||||
.join("embeddings_db");
|
||||
|
||||
let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
|
||||
Some(api_key)
|
||||
} else if let Some((_, api_key)) = cx
|
||||
.platform()
|
||||
.read_credentials(OPENAI_API_URL)
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
String::from_utf8(api_key).log_err()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cx.subscribe_global::<WorkspaceCreated, _>({
|
||||
move |event, cx| {
|
||||
let Some(semantic_index) = SemanticIndex::global(cx) else {
|
||||
|
@ -88,7 +104,7 @@ pub fn init(
|
|||
let semantic_index = SemanticIndex::new(
|
||||
fs,
|
||||
db_file_path,
|
||||
Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
|
||||
Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())),
|
||||
language_registry,
|
||||
cx.clone(),
|
||||
)
|
||||
|
|
|
@ -2,14 +2,15 @@ use crate::{Anchor, BufferSnapshot, TextDimension};
|
|||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum SelectionGoal {
|
||||
None,
|
||||
Column(u32),
|
||||
ColumnRange { start: u32, end: u32 },
|
||||
HorizontalPosition(f32),
|
||||
HorizontalRange { start: f32, end: f32 },
|
||||
WrappedHorizontalPosition((u32, f32)),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Selection<T> {
|
||||
pub id: usize,
|
||||
pub start: T,
|
||||
|
|
|
@ -7,6 +7,7 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
fuzzy = {path = "../fuzzy"}
|
||||
fs = {path = "../fs"}
|
||||
gpui = {path = "../gpui"}
|
||||
picker = {path = "../picker"}
|
||||
util = {path = "../util"}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{anyhow, bail, Result};
|
||||
use fs::repository::Branch;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
|
@ -22,18 +23,9 @@ pub type BranchList = Picker<BranchListDelegate>;
|
|||
pub fn build_branch_list(
|
||||
workspace: ViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<BranchList>,
|
||||
) -> BranchList {
|
||||
Picker::new(
|
||||
BranchListDelegate {
|
||||
matches: vec![],
|
||||
workspace,
|
||||
selected_index: 0,
|
||||
last_query: String::default(),
|
||||
branch_name_trailoff_after: 29,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.picker.clone())
|
||||
) -> Result<BranchList> {
|
||||
Ok(Picker::new(BranchListDelegate::new(workspace, 29, cx)?, cx)
|
||||
.with_theme(|theme| theme.picker.clone()))
|
||||
}
|
||||
|
||||
fn toggle(
|
||||
|
@ -43,31 +35,24 @@ fn toggle(
|
|||
) -> Option<Task<Result<()>>> {
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
// Modal branch picker has a longer trailoff than a popover one.
|
||||
let delegate = BranchListDelegate::new(cx.handle(), 70, cx)?;
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
let workspace = cx.handle();
|
||||
cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
BranchListDelegate {
|
||||
matches: vec![],
|
||||
workspace,
|
||||
selected_index: 0,
|
||||
last_query: String::default(),
|
||||
/// Modal branch picker has a longer trailoff than a popover one.
|
||||
branch_name_trailoff_after: 70,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.picker.clone())
|
||||
.with_max_size(800., 1200.)
|
||||
Picker::new(delegate, cx)
|
||||
.with_theme(|theme| theme.picker.clone())
|
||||
.with_max_size(800., 1200.)
|
||||
})
|
||||
});
|
||||
})?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})??;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
matches: Vec<StringMatch>,
|
||||
all_branches: Vec<Branch>,
|
||||
workspace: ViewHandle<Workspace>,
|
||||
selected_index: usize,
|
||||
last_query: String,
|
||||
|
@ -76,6 +61,31 @@ pub struct BranchListDelegate {
|
|||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
fn new(
|
||||
workspace: ViewHandle<Workspace>,
|
||||
branch_name_trailoff_after: usize,
|
||||
cx: &AppContext,
|
||||
) -> Result<Self> {
|
||||
let project = workspace.read(cx).project().read(&cx);
|
||||
|
||||
let Some(worktree) = project.visible_worktrees(cx).next() else {
|
||||
bail!("Cannot update branch list as there are no visible worktrees")
|
||||
};
|
||||
let mut cwd = worktree.read(cx).abs_path().to_path_buf();
|
||||
cwd.push(".git");
|
||||
let Some(repo) = project.fs().open_repo(&cwd) else {
|
||||
bail!("Project does not have associated git repository.")
|
||||
};
|
||||
let all_branches = repo.lock().branches()?;
|
||||
Ok(Self {
|
||||
matches: vec![],
|
||||
workspace,
|
||||
all_branches,
|
||||
selected_index: 0,
|
||||
last_query: Default::default(),
|
||||
branch_name_trailoff_after,
|
||||
})
|
||||
}
|
||||
fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
|
||||
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
|
||||
self.workspace.update(cx, |model, ctx| {
|
||||
|
@ -83,6 +93,7 @@ impl BranchListDelegate {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for BranchListDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Select branch...".into()
|
||||
|
@ -102,45 +113,28 @@ impl PickerDelegate for BranchListDelegate {
|
|||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let Some(candidates) = picker
|
||||
.read_with(&mut cx, |view, cx| {
|
||||
let delegate = view.delegate();
|
||||
let project = delegate.workspace.read(cx).project().read(&cx);
|
||||
|
||||
let Some(worktree) = project.visible_worktrees(cx).next() else {
|
||||
bail!("Cannot update branch list as there are no visible worktrees")
|
||||
};
|
||||
let mut cwd = worktree.read(cx).abs_path().to_path_buf();
|
||||
cwd.push(".git");
|
||||
let Some(repo) = project.fs().open_repo(&cwd) else {
|
||||
bail!("Project does not have associated git repository.")
|
||||
};
|
||||
let mut branches = repo.lock().branches()?;
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
|
||||
});
|
||||
branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
|
||||
}
|
||||
Ok(branches
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
char_bag: command.name.chars().collect(),
|
||||
string: command.name.into(),
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let candidates = picker.read_with(&mut cx, |view, _| {
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
let mut branches = view.delegate().all_branches.clone();
|
||||
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
|
||||
});
|
||||
branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
|
||||
}
|
||||
branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
char_bag: command.name.chars().collect(),
|
||||
string: command.name.into(),
|
||||
})
|
||||
.collect::<Vec<StringMatchCandidate>>()
|
||||
});
|
||||
let Some(candidates) = candidates.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use std::cmp;
|
||||
|
||||
use editor::{
|
||||
char_kind,
|
||||
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
|
||||
movement::{self, find_boundary, find_preceding_boundary, FindRange},
|
||||
movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
|
||||
Bias, CharKind, DisplayPoint, ToOffset,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
||||
|
@ -361,6 +359,7 @@ impl Motion {
|
|||
point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
maybe_times: Option<usize>,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> Option<(DisplayPoint, SelectionGoal)> {
|
||||
let times = maybe_times.unwrap_or(1);
|
||||
use Motion::*;
|
||||
|
@ -370,16 +369,16 @@ impl Motion {
|
|||
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
||||
Down {
|
||||
display_lines: false,
|
||||
} => down(map, point, goal, times),
|
||||
} => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
|
||||
Down {
|
||||
display_lines: true,
|
||||
} => down_display(map, point, goal, times),
|
||||
} => down_display(map, point, goal, times, &text_layout_details),
|
||||
Up {
|
||||
display_lines: false,
|
||||
} => up(map, point, goal, times),
|
||||
} => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
|
||||
Up {
|
||||
display_lines: true,
|
||||
} => up_display(map, point, goal, times),
|
||||
} => up_display(map, point, goal, times, &text_layout_details),
|
||||
Right => (right(map, point, times), SelectionGoal::None),
|
||||
NextWordStart { ignore_punctuation } => (
|
||||
next_word_start(map, point, *ignore_punctuation, times),
|
||||
|
@ -442,10 +441,15 @@ impl Motion {
|
|||
selection: &mut Selection<DisplayPoint>,
|
||||
times: Option<usize>,
|
||||
expand_to_surrounding_newline: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> bool {
|
||||
if let Some((new_head, goal)) =
|
||||
self.move_point(map, selection.head(), selection.goal, times)
|
||||
{
|
||||
if let Some((new_head, goal)) = self.move_point(
|
||||
map,
|
||||
selection.head(),
|
||||
selection.goal,
|
||||
times,
|
||||
&text_layout_details,
|
||||
) {
|
||||
selection.set_head(new_head, goal);
|
||||
|
||||
if self.linewise() {
|
||||
|
@ -530,35 +534,85 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
|
|||
point
|
||||
}
|
||||
|
||||
fn down(
|
||||
pub(crate) fn start_of_relative_buffer_row(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
times: isize,
|
||||
) -> DisplayPoint {
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
let target = start.row() as isize + times;
|
||||
let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
|
||||
|
||||
map.clip_point(
|
||||
map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
.clip_point(FoldPoint::new(new_row, 0), Bias::Right),
|
||||
),
|
||||
Bias::Right,
|
||||
)
|
||||
}
|
||||
|
||||
fn up_down_buffer_rows(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
times: isize,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
let begin_folded_line = map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
.clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
|
||||
);
|
||||
let select_nth_wrapped_row = point.row() - begin_folded_line.row();
|
||||
|
||||
let goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
let (goal_wrap, goal_x) = match goal {
|
||||
SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
|
||||
SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
|
||||
SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
|
||||
_ => {
|
||||
goal = SelectionGoal::Column(start.column());
|
||||
start.column()
|
||||
let x = map.x_for_point(point, text_layout_details);
|
||||
goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
|
||||
(select_nth_wrapped_row, x)
|
||||
}
|
||||
};
|
||||
|
||||
let new_row = cmp::min(
|
||||
start.row() + times as u32,
|
||||
map.fold_snapshot.max_point().row(),
|
||||
);
|
||||
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
|
||||
let point = map.fold_point_to_display_point(
|
||||
let target = start.row() as isize + times;
|
||||
let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
|
||||
|
||||
let mut begin_folded_line = map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
.clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
|
||||
.clip_point(FoldPoint::new(new_row, 0), Bias::Left),
|
||||
);
|
||||
|
||||
// clip twice to "clip at end of line"
|
||||
(map.clip_point(point, Bias::Left), goal)
|
||||
let mut i = 0;
|
||||
while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
|
||||
let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
|
||||
if map
|
||||
.display_point_to_fold_point(next_folded_line, Bias::Right)
|
||||
.row()
|
||||
== new_row
|
||||
{
|
||||
i += 1;
|
||||
begin_folded_line = next_folded_line;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let new_col = if i == goal_wrap {
|
||||
map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
|
||||
} else {
|
||||
map.line_len(begin_folded_line.row())
|
||||
};
|
||||
|
||||
(
|
||||
map.clip_point(
|
||||
DisplayPoint::new(begin_folded_line.row(), new_col),
|
||||
Bias::Left,
|
||||
),
|
||||
goal,
|
||||
)
|
||||
}
|
||||
|
||||
fn down_display(
|
||||
|
@ -566,49 +620,24 @@ fn down_display(
|
|||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
for _ in 0..times {
|
||||
(point, goal) = movement::down(map, point, goal, true);
|
||||
(point, goal) = movement::down(map, point, goal, true, text_layout_details);
|
||||
}
|
||||
|
||||
(point, goal)
|
||||
}
|
||||
|
||||
pub(crate) fn up(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
|
||||
let goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => {
|
||||
goal = SelectionGoal::Column(start.column());
|
||||
start.column()
|
||||
}
|
||||
};
|
||||
|
||||
let new_row = start.row().saturating_sub(times as u32);
|
||||
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
|
||||
let point = map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
.clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
|
||||
);
|
||||
|
||||
(map.clip_point(point, Bias::Left), goal)
|
||||
}
|
||||
|
||||
fn up_display(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
for _ in 0..times {
|
||||
(point, goal) = movement::up(map, point, goal, true);
|
||||
(point, goal) = movement::up(map, point, goal, true, &text_layout_details);
|
||||
}
|
||||
|
||||
(point, goal)
|
||||
|
@ -707,7 +736,7 @@ fn previous_word_start(
|
|||
point
|
||||
}
|
||||
|
||||
fn first_non_whitespace(
|
||||
pub(crate) fn first_non_whitespace(
|
||||
map: &DisplaySnapshot,
|
||||
display_lines: bool,
|
||||
from: DisplayPoint,
|
||||
|
@ -886,13 +915,17 @@ fn find_backward(
|
|||
}
|
||||
|
||||
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
let correct_line = down(map, point, SelectionGoal::None, times).0;
|
||||
let correct_line = start_of_relative_buffer_row(map, point, times as isize);
|
||||
first_non_whitespace(map, false, correct_line)
|
||||
}
|
||||
|
||||
fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
pub(crate) fn next_line_end(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
if times > 1 {
|
||||
point = down(map, point, SelectionGoal::None, times - 1).0;
|
||||
point = start_of_relative_buffer_row(map, point, times as isize - 1);
|
||||
}
|
||||
end_of_line(map, false, point)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ mod yank;
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
motion::{self, Motion},
|
||||
motion::{self, first_non_whitespace, next_line_end, right, Motion},
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
Vim,
|
||||
|
@ -179,10 +179,11 @@ pub(crate) fn move_cursor(
|
|||
cx: &mut WindowContext,
|
||||
) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
motion
|
||||
.move_point(map, cursor, goal, times)
|
||||
.move_point(map, cursor, goal, times, &text_layout_details)
|
||||
.unwrap_or((cursor, goal))
|
||||
})
|
||||
})
|
||||
|
@ -195,9 +196,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
|||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::Right.move_point(map, cursor, goal, None)
|
||||
});
|
||||
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -220,11 +219,11 @@ fn insert_first_non_whitespace(
|
|||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::FirstNonWhitespace {
|
||||
display_lines: false,
|
||||
}
|
||||
.move_point(map, cursor, goal, None)
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
(
|
||||
first_non_whitespace(map, false, cursor),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -237,8 +236,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
|||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::CurrentLine.move_point(map, cursor, goal, None)
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
(next_line_end(map, cursor, 1), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -268,7 +267,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
|||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
|
||||
let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
|
||||
let insert_point = motion::end_of_line(map, false, previous_line);
|
||||
(insert_point, SelectionGoal::None)
|
||||
});
|
||||
|
@ -283,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
|||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (map, old_selections) = editor.selections.all_display(cx);
|
||||
|
||||
|
@ -301,7 +301,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
|||
});
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::CurrentLine.move_point(map, cursor, goal, None)
|
||||
Motion::CurrentLine.move_point(
|
||||
map,
|
||||
cursor,
|
||||
goal,
|
||||
None,
|
||||
&text_layout_details,
|
||||
)
|
||||
});
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
|
@ -399,12 +405,26 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuick broˇwn
|
||||
ˇfox jumps"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
aaˇaa
|
||||
😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
aaaa
|
||||
😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
for marked_position in cx.each_marked_position(indoc! {"
|
||||
ˇThe qˇuick broˇwn
|
||||
ˇfox jumps"
|
||||
}) {
|
||||
cx.assert_neovim_compatible(&marked_position, ["j"]).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_
|
|||
use editor::{
|
||||
char_kind,
|
||||
display_map::DisplaySnapshot,
|
||||
movement::{self, FindRange},
|
||||
movement::{self, FindRange, TextLayoutDetails},
|
||||
scroll::autoscroll::Autoscroll,
|
||||
CharKind, DisplayPoint,
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
|||
| Motion::StartOfLine { .. }
|
||||
);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
@ -27,9 +28,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
|||
s.move_with(|map, selection| {
|
||||
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
|
||||
{
|
||||
expand_changed_word_selection(map, selection, times, ignore_punctuation)
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
)
|
||||
} else {
|
||||
motion.expand_selection(map, selection, times, false)
|
||||
motion.expand_selection(map, selection, times, false, &text_layout_details)
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -81,6 +88,7 @@ fn expand_changed_word_selection(
|
|||
selection: &mut Selection<DisplayPoint>,
|
||||
times: Option<usize>,
|
||||
ignore_punctuation: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> bool {
|
||||
if times.is_none() || times.unwrap() == 1 {
|
||||
let scope = map
|
||||
|
@ -103,11 +111,22 @@ fn expand_changed_word_selection(
|
|||
});
|
||||
true
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
.expand_selection(map, selection, None, false)
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ use language::Point;
|
|||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
|
@ -14,7 +15,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
|||
s.move_with(|map, selection| {
|
||||
let original_head = selection.head();
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
motion.expand_selection(map, selection, times, true);
|
||||
motion.expand_selection(map, selection, times, true, &text_layout_details);
|
||||
|
||||
// Motion::NextWordStart on an empty line should delete it.
|
||||
if let Motion::NextWordStart {
|
||||
|
|
|
@ -255,8 +255,18 @@ mod test {
|
|||
4
|
||||
5"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g", "g", "ctrl-x"])
|
||||
|
||||
cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
«1ˇ»
|
||||
«2ˇ»
|
||||
«3ˇ» 2
|
||||
«4ˇ»
|
||||
«5ˇ»"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇ0
|
||||
0
|
||||
|
|
|
@ -30,6 +30,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
|
@ -168,8 +169,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
let mut cursor = anchor.to_display_point(map);
|
||||
if *line_mode {
|
||||
if !before {
|
||||
cursor =
|
||||
movement::down(map, cursor, SelectionGoal::None, false).0;
|
||||
cursor = movement::down(
|
||||
map,
|
||||
cursor,
|
||||
SelectionGoal::None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
.0;
|
||||
}
|
||||
cursor = movement::indented_line_beginning(map, cursor, true);
|
||||
} else if !is_multiline {
|
||||
|
|
|
@ -32,10 +32,17 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
|
|||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.start == selection.end {
|
||||
Motion::Right.expand_selection(map, selection, count, true);
|
||||
Motion::Right.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
count,
|
||||
true,
|
||||
&text_layout_details,
|
||||
);
|
||||
}
|
||||
if line_mode {
|
||||
// in Visual mode when the selection contains the newline at the end
|
||||
|
@ -43,7 +50,13 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
|
|||
if !selection.is_empty() && selection.end.column() == 0 {
|
||||
selection.end = movement::left(map, selection.end);
|
||||
}
|
||||
Motion::CurrentLine.expand_selection(map, selection, None, false);
|
||||
Motion::CurrentLine.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
||||
display_lines: false,
|
||||
})
|
||||
|
@ -52,6 +65,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
|
|||
selection.start,
|
||||
selection.goal,
|
||||
None,
|
||||
&text_layout_details,
|
||||
) {
|
||||
selection.start = point;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use gpui::WindowContext;
|
|||
|
||||
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
|
@ -11,7 +12,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
|
|||
s.move_with(|map, selection| {
|
||||
let original_position = (selection.head(), selection.goal);
|
||||
original_positions.insert(selection.id, original_position);
|
||||
motion.expand_selection(map, selection, times, true);
|
||||
motion.expand_selection(map, selection, times, true, &text_layout_details);
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
|
|
|
@ -653,6 +653,63 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
|
|||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_wrap(12).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
aaˇaa
|
||||
😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
aaaa
|
||||
😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
123456789012aaˇaa
|
||||
123456789012😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
123456789012aaaa
|
||||
123456789012😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
123456789012aaˇaa
|
||||
123456789012😃😃"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
123456789012aaaa
|
||||
123456789012😃ˇ😃"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
123456789012aaaaˇaaaaaaaa123456789012
|
||||
wow
|
||||
123456789012😃😃😃😃😃😃123456789012"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
123456789012aaaaaaaaaaaa123456789012
|
||||
wow
|
||||
123456789012😃😃ˇ😃😃😃😃123456789012"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
|
|
@ -25,7 +25,7 @@ pub use mode_indicator::ModeIndicator;
|
|||
use motion::Motion;
|
||||
use normal::normal_replace;
|
||||
use serde::Deserialize;
|
||||
use settings::{Setting, SettingsStore};
|
||||
use settings::{update_settings_file, Setting, SettingsStore};
|
||||
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use visual::{visual_block_motion, visual_replace};
|
||||
|
@ -48,6 +48,7 @@ actions!(
|
|||
vim,
|
||||
[Tab, Enter, Object, InnerObject, FindForward, FindBackward]
|
||||
);
|
||||
actions!(workspace, [ToggleVimMode]);
|
||||
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
@ -88,6 +89,14 @@ pub fn init(cx: &mut AppContext) {
|
|||
Vim::active_editor_input_ignored("\n".into(), cx)
|
||||
});
|
||||
|
||||
cx.add_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let currently_enabled = settings::get::<VimModeSetting>(cx).0;
|
||||
update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
|
||||
*setting = Some(!currently_enabled)
|
||||
})
|
||||
});
|
||||
|
||||
// Any time settings change, update vim mode to match. The Vim struct
|
||||
// will be initialized as disabled by default, so we filter its commands
|
||||
// out when starting up.
|
||||
|
@ -581,7 +590,7 @@ impl Setting for VimModeSetting {
|
|||
fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
|
||||
if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
|
||||
if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx);
|
||||
} else {
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
|
|
|
@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
if vim.state().mode == Mode::VisualBlock
|
||||
&& !matches!(
|
||||
motion,
|
||||
|
@ -67,7 +68,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
|
|||
{
|
||||
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
|
||||
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
|
||||
motion.move_point(map, point, goal, times)
|
||||
motion.move_point(map, point, goal, times, &text_layout_details)
|
||||
})
|
||||
} else {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
|
@ -89,9 +90,13 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
|
|||
current_head = movement::left(map, selection.end)
|
||||
}
|
||||
|
||||
let Some((new_head, goal)) =
|
||||
motion.move_point(map, current_head, selection.goal, times)
|
||||
else {
|
||||
let Some((new_head, goal)) = motion.move_point(
|
||||
map,
|
||||
current_head,
|
||||
selection.goal,
|
||||
times,
|
||||
&text_layout_details,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -135,19 +140,23 @@ pub fn visual_block_motion(
|
|||
SelectionGoal,
|
||||
) -> Option<(DisplayPoint, SelectionGoal)>,
|
||||
) {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let map = &s.display_map();
|
||||
let mut head = s.newest_anchor().head().to_display_point(map);
|
||||
let mut tail = s.oldest_anchor().tail().to_display_point(map);
|
||||
|
||||
let (start, end) = match s.newest_anchor().goal {
|
||||
SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
|
||||
SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
|
||||
_ => (tail.column(), head.column()),
|
||||
};
|
||||
let goal = SelectionGoal::ColumnRange { start, end };
|
||||
let mut head_x = map.x_for_point(head, &text_layout_details);
|
||||
let mut tail_x = map.x_for_point(tail, &text_layout_details);
|
||||
|
||||
let 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 {
|
||||
head = movement::saturating_left(map, head);
|
||||
}
|
||||
|
@ -156,32 +165,56 @@ pub fn visual_block_motion(
|
|||
return;
|
||||
};
|
||||
head = new_head;
|
||||
head_x = map.x_for_point(head, &text_layout_details);
|
||||
|
||||
let is_reversed = tail.column() > head.column();
|
||||
let is_reversed = tail_x > head_x;
|
||||
if was_reversed && !is_reversed {
|
||||
tail = movement::left(map, tail)
|
||||
tail = movement::saturating_left(map, tail);
|
||||
tail_x = map.x_for_point(tail, &text_layout_details);
|
||||
} else if !was_reversed && is_reversed {
|
||||
tail = movement::right(map, tail)
|
||||
tail = movement::saturating_right(map, tail);
|
||||
tail_x = map.x_for_point(tail, &text_layout_details);
|
||||
}
|
||||
if !is_reversed && !preserve_goal {
|
||||
head = movement::saturating_right(map, head)
|
||||
head = movement::saturating_right(map, head);
|
||||
head_x = map.x_for_point(head, &text_layout_details);
|
||||
}
|
||||
|
||||
let columns = if is_reversed {
|
||||
head.column()..tail.column()
|
||||
} else if head.column() == tail.column() {
|
||||
head.column()..(head.column() + 1)
|
||||
let positions = if is_reversed {
|
||||
head_x..tail_x
|
||||
} else {
|
||||
tail.column()..head.column()
|
||||
tail_x..head_x
|
||||
};
|
||||
|
||||
if !preserve_goal {
|
||||
goal = SelectionGoal::HorizontalRange {
|
||||
start: positions.start,
|
||||
end: positions.end,
|
||||
};
|
||||
}
|
||||
|
||||
let mut selections = Vec::new();
|
||||
let mut row = tail.row();
|
||||
|
||||
loop {
|
||||
let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
|
||||
let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
|
||||
if columns.start <= map.line_len(row) {
|
||||
let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
|
||||
let start = DisplayPoint::new(
|
||||
row,
|
||||
layed_out_line.closest_index_for_x(positions.start) as u32,
|
||||
);
|
||||
let mut end = DisplayPoint::new(
|
||||
row,
|
||||
layed_out_line.closest_index_for_x(positions.end) as u32,
|
||||
);
|
||||
if end <= start {
|
||||
if start.column() == map.line_len(start.row()) {
|
||||
end = start;
|
||||
} else {
|
||||
end = movement::saturating_right(map, start);
|
||||
}
|
||||
}
|
||||
|
||||
if positions.start <= layed_out_line.width() {
|
||||
let selection = Selection {
|
||||
id: s.new_selection_id(),
|
||||
start: start.to_point(map),
|
||||
|
@ -888,6 +921,28 @@ mod test {
|
|||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog
|
||||
"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"The «quˇ»ick brown
|
||||
fox «juˇ»mps over
|
||||
the lazy dog
|
||||
"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
{"Key":"ctrl-v"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"«1ˇ»\n«2ˇ»\n«3ˇ» 2\n«4ˇ»\n«5ˇ»","mode":"VisualBlock"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"ˇ0\n0\n0 2\n0\n0","mode":"Normal"}}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
{"Put":{"state":"aaˇaa\n😃😃"}}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
|
||||
|
|
5
crates/vim/test_data/test_visual_block_issue_2123.json
Normal file
5
crates/vim/test_data/test_visual_block_issue_2123.json
Normal 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"}}
|
15
crates/vim/test_data/test_wrapped_motions.json
Normal file
15
crates/vim/test_data/test_wrapped_motions.json
Normal 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"}}
|
|
@ -1,3 +1,4 @@
|
|||
use ai::completion::OPENAI_API_URL;
|
||||
use ai::embedding::OpenAIEmbeddings;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{self, UserStore};
|
||||
|
@ -17,6 +18,7 @@ use std::{cmp, env, fs};
|
|||
use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
|
||||
use util::http::{self};
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
use util::ResultExt;
|
||||
use zed::languages;
|
||||
|
||||
#[derive(Deserialize, Clone, Serialize)]
|
||||
|
@ -469,12 +471,26 @@ fn main() {
|
|||
.join("embeddings_db");
|
||||
|
||||
let languages = languages.clone();
|
||||
|
||||
let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
|
||||
Some(api_key)
|
||||
} else if let Some((_, api_key)) = cx
|
||||
.platform()
|
||||
.read_credentials(OPENAI_API_URL)
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
String::from_utf8(api_key).log_err()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let fs = fs.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let semantic_index = SemanticIndex::new(
|
||||
fs.clone(),
|
||||
db_file_path,
|
||||
Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
|
||||
Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())),
|
||||
languages.clone(),
|
||||
cx.clone(),
|
||||
)
|
||||
|
|
|
@ -76,7 +76,10 @@ pub fn init(
|
|||
elixir::ElixirLspSetting::ElixirLs => language(
|
||||
"elixir",
|
||||
tree_sitter_elixir::language(),
|
||||
vec![Arc::new(elixir::ElixirLspAdapter)],
|
||||
vec![
|
||||
Arc::new(elixir::ElixirLspAdapter),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
],
|
||||
),
|
||||
elixir::ElixirLspSetting::NextLs => language(
|
||||
"elixir",
|
||||
|
@ -101,7 +104,10 @@ pub fn init(
|
|||
language(
|
||||
"heex",
|
||||
tree_sitter_heex::language(),
|
||||
vec![Arc::new(elixir::ElixirLspAdapter)],
|
||||
vec![
|
||||
Arc::new(elixir::ElixirLspAdapter),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
],
|
||||
);
|
||||
language(
|
||||
"json",
|
||||
|
@ -167,7 +173,10 @@ pub fn init(
|
|||
language(
|
||||
"erb",
|
||||
tree_sitter_embedded_template::language(),
|
||||
vec![Arc::new(ruby::RubyLanguageServer)],
|
||||
vec![
|
||||
Arc::new(ruby::RubyLanguageServer),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
],
|
||||
);
|
||||
language("scheme", tree_sitter_scheme::language(), vec![]);
|
||||
language("racket", tree_sitter_racket::language(), vec![]);
|
||||
|
@ -184,16 +193,18 @@ pub fn init(
|
|||
language(
|
||||
"svelte",
|
||||
tree_sitter_svelte::language(),
|
||||
vec![Arc::new(svelte::SvelteLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
))],
|
||||
vec![
|
||||
Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
],
|
||||
);
|
||||
language(
|
||||
"php",
|
||||
tree_sitter_php::language(),
|
||||
vec![Arc::new(php::IntelephenseLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
))],
|
||||
vec![
|
||||
Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
],
|
||||
);
|
||||
|
||||
language("elm", tree_sitter_elm::language(), vec![]);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -96,10 +96,6 @@ impl LspAdapter for CssLspAdapter {
|
|||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("css")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -10,3 +10,4 @@ brackets = [
|
|||
]
|
||||
word_characters = ["-"]
|
||||
block_comment = ["/* ", " */"]
|
||||
prettier_parser_name = "css"
|
||||
|
|
|
@ -9,3 +9,8 @@ brackets = [
|
|||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
|
||||
[overrides.string]
|
||||
word_characters = ["-"]
|
||||
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||
|
|
|
@ -5,3 +5,4 @@ brackets = [
|
|||
{ start = "<", end = ">", close = true, newline = true },
|
||||
]
|
||||
block_comment = ["<%#", "%>"]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
|
|
|
@ -5,3 +5,8 @@ brackets = [
|
|||
{ start = "<", end = ">", close = true, newline = true },
|
||||
]
|
||||
block_comment = ["<%!-- ", " --%>"]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
|
||||
[overrides.string]
|
||||
word_characters = ["-"]
|
||||
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||
|
|
4
crates/zed/src/languages/heex/overrides.scm
Normal file
4
crates/zed/src/languages/heex/overrides.scm
Normal file
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
] @string
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -96,10 +96,6 @@ impl LspAdapter for HtmlLspAdapter {
|
|||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("html")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -11,3 +11,4 @@ brackets = [
|
|||
{ start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
|
||||
]
|
||||
word_characters = ["-"]
|
||||
prettier_parser_name = "html"
|
||||
|
|
|
@ -15,6 +15,7 @@ brackets = [
|
|||
]
|
||||
word_characters = ["$", "#"]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
prettier_parser_name = "babel"
|
||||
|
||||
[overrides.element]
|
||||
line_comment = { remove = true }
|
||||
|
|
|
@ -4,9 +4,7 @@ use collections::HashMap;
|
|||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{
|
||||
BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -146,10 +144,6 @@ impl LspAdapter for JsonLspAdapter {
|
|||
async fn language_ids(&self) -> HashMap<String, String> {
|
||||
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("json")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -7,3 +7,4 @@ brackets = [
|
|||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
prettier_parser_name = "json"
|
||||
|
|
|
@ -11,3 +11,4 @@ brackets = [
|
|||
]
|
||||
collapsed_placeholder = "/* ... */"
|
||||
word_characters = ["$"]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -96,11 +96,8 @@ impl LspAdapter for SvelteLspAdapter {
|
|||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("svelte"),
|
||||
plugin_names: vec!["prettier-plugin-svelte"],
|
||||
}]
|
||||
fn prettier_plugins(&self) -> &[&'static str] {
|
||||
&["prettier-plugin-svelte"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ brackets = [
|
|||
{ start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
prettier_parser_name = "svelte"
|
||||
|
||||
[overrides.element]
|
||||
line_comment = { remove = true }
|
||||
block_comment = ["{/* ", " */}"]
|
||||
[overrides.string]
|
||||
word_characters = ["-"]
|
||||
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||
|
|
7
crates/zed/src/languages/svelte/overrides.scm
Normal file
7
crates/zed/src/languages/svelte/overrides.scm
Normal file
|
@ -0,0 +1,7 @@
|
|||
(comment) @comment
|
||||
|
||||
[
|
||||
(raw_text)
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
] @string
|
|
@ -6,7 +6,7 @@ use futures::{
|
|||
FutureExt, StreamExt,
|
||||
};
|
||||
use gpui::AppContext;
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::{json, Value};
|
||||
|
@ -117,22 +117,21 @@ impl LspAdapter for TailwindLspAdapter {
|
|||
}
|
||||
|
||||
async fn language_ids(&self) -> HashMap<String, String> {
|
||||
HashMap::from_iter(
|
||||
[
|
||||
("HTML".to_string(), "html".to_string()),
|
||||
("CSS".to_string(), "css".to_string()),
|
||||
("JavaScript".to_string(), "javascript".to_string()),
|
||||
("TSX".to_string(), "typescriptreact".to_string()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
HashMap::from_iter([
|
||||
("HTML".to_string(), "html".to_string()),
|
||||
("CSS".to_string(), "css".to_string()),
|
||||
("JavaScript".to_string(), "javascript".to_string()),
|
||||
("TSX".to_string(), "typescriptreact".to_string()),
|
||||
("Svelte".to_string(), "svelte".to_string()),
|
||||
("Elixir".to_string(), "phoenix-heex".to_string()),
|
||||
("HEEX".to_string(), "phoenix-heex".to_string()),
|
||||
("ERB".to_string(), "erb".to_string()),
|
||||
("PHP".to_string(), "php".to_string()),
|
||||
])
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::Prettier {
|
||||
parser_name: None,
|
||||
plugin_names: vec!["prettier-plugin-tailwindcss"],
|
||||
}]
|
||||
fn prettier_plugins(&self) -> &[&'static str] {
|
||||
&["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ brackets = [
|
|||
]
|
||||
word_characters = ["#", "$"]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
prettier_parser_name = "typescript"
|
||||
|
||||
[overrides.element]
|
||||
line_comment = { remove = true }
|
||||
|
|
|
@ -4,7 +4,7 @@ use async_tar::Archive;
|
|||
use async_trait::async_trait;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use gpui::AppContext;
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::{json, Value};
|
||||
|
@ -161,10 +161,6 @@ impl LspAdapter for TypeScriptLspAdapter {
|
|||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("typescript")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_ts_server_binary(
|
||||
|
@ -313,10 +309,6 @@ impl LspAdapter for EsLintLspAdapter {
|
|||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("babel")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_eslint_server_binary(
|
||||
|
|
|
@ -13,3 +13,4 @@ brackets = [
|
|||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
word_characters = ["#", "$"]
|
||||
prettier_parser_name = "typescript"
|
||||
|
|
|
@ -3,8 +3,7 @@ use async_trait::async_trait;
|
|||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{
|
||||
language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
|
@ -109,10 +108,6 @@ impl LspAdapter for YamlLspAdapter {
|
|||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("yaml")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -9,3 +9,4 @@ brackets = [
|
|||
]
|
||||
|
||||
increase_indent_pattern = ":\\s*[|>]?\\s*$"
|
||||
prettier_parser_name = "yaml"
|
||||
|
|
Loading…
Reference in a new issue