Merge branch 'main' into agent2-history
This commit is contained in:
commit
999449424e
80 changed files with 3263 additions and 1900 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -3077,6 +3077,7 @@ dependencies = [
|
|||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
|
@ -20203,8 +20204,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
|||
|
||||
[[package]]
|
||||
name = "yawc"
|
||||
version = "0.2.4"
|
||||
source = "git+https://github.com/deviant-forks/yawc?rev=1899688f3e69ace4545aceb97b2a13881cf26142#1899688f3e69ace4545aceb97b2a13881cf26142"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19a5d82922135b4ae73a079a4ffb5501e9aadb4d785b8c660eaa0a8b899028c5"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.1",
|
||||
|
|
|
@ -582,6 +582,7 @@ serde_json_lenient = { version = "0.2", features = [
|
|||
"raw_value",
|
||||
] }
|
||||
serde_repr = "0.1"
|
||||
serde_urlencoded = "0.7"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
|
@ -659,9 +660,7 @@ which = "6.0.0"
|
|||
windows-core = "0.61"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new
|
||||
# version is released.
|
||||
yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" }
|
||||
yawc = "0.2.5"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
|
|
|
@ -286,6 +286,8 @@
|
|||
// bracket, brace, single or double quote characters.
|
||||
// For example, when you select text and type (, Zed will surround the text with ().
|
||||
"use_auto_surround": true,
|
||||
/// Whether indentation should be adjusted based on the context whilst typing.
|
||||
"auto_indent": true,
|
||||
// Whether indentation of pasted content should be adjusted based on the context.
|
||||
"auto_indent_on_paste": true,
|
||||
// Controls how the editor handles the autoclosed characters.
|
||||
|
|
|
@ -689,6 +689,7 @@ pub struct AcpThread {
|
|||
session_id: acp::SessionId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AcpThreadEvent {
|
||||
NewEntry,
|
||||
TitleUpdated,
|
||||
|
@ -1225,17 +1226,21 @@ impl AcpThread {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
self.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage {
|
||||
id: message_id.clone(),
|
||||
content: block,
|
||||
chunks: message,
|
||||
checkpoint: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
self.run_turn(cx, async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage {
|
||||
id: message_id.clone(),
|
||||
content: block,
|
||||
chunks: message,
|
||||
checkpoint: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let old_checkpoint = git_store
|
||||
.update(cx, |git, cx| git.checkpoint(cx))?
|
||||
.await
|
||||
|
|
|
@ -206,7 +206,7 @@ mod test_support {
|
|||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use futures::future::try_join_all;
|
||||
use futures::{channel::oneshot, future::try_join_all};
|
||||
use gpui::{AppContext as _, WeakEntity};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
|
@ -214,11 +214,16 @@ mod test_support {
|
|||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StubAgentConnection {
|
||||
sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
|
||||
sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
|
||||
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
|
||||
next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
|
||||
}
|
||||
|
||||
struct Session {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
response_tx: Option<oneshot::Sender<acp::StopReason>>,
|
||||
}
|
||||
|
||||
impl StubAgentConnection {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -246,15 +251,33 @@ mod test_support {
|
|||
update: acp::SessionUpdate,
|
||||
cx: &mut App,
|
||||
) {
|
||||
assert!(
|
||||
self.next_prompt_updates.lock().is_empty(),
|
||||
"Use either send_update or set_next_prompt_updates"
|
||||
);
|
||||
|
||||
self.sessions
|
||||
.lock()
|
||||
.get(&session_id)
|
||||
.unwrap()
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(update.clone(), cx).unwrap();
|
||||
thread.handle_session_update(update, cx).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
|
||||
self.sessions
|
||||
.lock()
|
||||
.get_mut(&session_id)
|
||||
.unwrap()
|
||||
.response_tx
|
||||
.take()
|
||||
.expect("No pending turn")
|
||||
.send(stop_reason)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for StubAgentConnection {
|
||||
|
@ -271,7 +294,13 @@ mod test_support {
|
|||
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
||||
let thread =
|
||||
cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx));
|
||||
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||
self.sessions.lock().insert(
|
||||
session_id,
|
||||
Session {
|
||||
thread: thread.downgrade(),
|
||||
response_tx: None,
|
||||
},
|
||||
);
|
||||
Task::ready(Ok(thread))
|
||||
}
|
||||
|
||||
|
@ -289,47 +318,70 @@ mod test_support {
|
|||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<acp::PromptResponse>> {
|
||||
let sessions = self.sessions.lock();
|
||||
let thread = sessions.get(¶ms.session_id).unwrap();
|
||||
let mut sessions = self.sessions.lock();
|
||||
let Session {
|
||||
thread,
|
||||
response_tx,
|
||||
} = sessions.get_mut(¶ms.session_id).unwrap();
|
||||
let mut tasks = vec![];
|
||||
for update in self.next_prompt_updates.lock().drain(..) {
|
||||
let thread = thread.clone();
|
||||
let update = update.clone();
|
||||
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
|
||||
&& let Some(options) = self.permission_requests.get(&tool_call.id)
|
||||
{
|
||||
Some((tool_call.clone(), options.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let task = cx.spawn(async move |cx| {
|
||||
if let Some((tool_call, options)) = permission_request {
|
||||
let permission = thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(
|
||||
tool_call.clone().into(),
|
||||
options.clone(),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
permission?.await?;
|
||||
}
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(update.clone(), cx).unwrap();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
cx.spawn(async move |_| {
|
||||
try_join_all(tasks).await?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
if self.next_prompt_updates.lock().is_empty() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
response_tx.replace(tx);
|
||||
cx.spawn(async move |_| {
|
||||
let stop_reason = rx.await?;
|
||||
Ok(acp::PromptResponse { stop_reason })
|
||||
})
|
||||
})
|
||||
} else {
|
||||
for update in self.next_prompt_updates.lock().drain(..) {
|
||||
let thread = thread.clone();
|
||||
let update = update.clone();
|
||||
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
|
||||
&update
|
||||
&& let Some(options) = self.permission_requests.get(&tool_call.id)
|
||||
{
|
||||
Some((tool_call.clone(), options.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let task = cx.spawn(async move |cx| {
|
||||
if let Some((tool_call, options)) = permission_request {
|
||||
let permission = thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(
|
||||
tool_call.clone().into(),
|
||||
options.clone(),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
permission?.await?;
|
||||
}
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(update.clone(), cx).unwrap();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
try_join_all(tasks).await?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
|
||||
unimplemented!()
|
||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||
if let Some(end_turn_tx) = self
|
||||
.sessions
|
||||
.lock()
|
||||
.get_mut(session_id)
|
||||
.unwrap()
|
||||
.response_tx
|
||||
.take()
|
||||
{
|
||||
end_turn_tx.send(acp::StopReason::Canceled).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn session_editor(
|
||||
|
|
|
@ -517,11 +517,18 @@ impl NativeAgent {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.models.refresh_list(cx);
|
||||
|
||||
let default_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|m| m.model.clone());
|
||||
|
||||
for session in self.sessions.values_mut() {
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
let model_id = LanguageModels::model_id(&thread.model());
|
||||
if let Some(model) = self.models.model_from_id(&model_id) {
|
||||
thread.set_model(model.clone(), cx);
|
||||
if thread.model().is_none()
|
||||
&& let Some(model) = default_model.clone()
|
||||
{
|
||||
thread.set_model(model);
|
||||
cx.notify();
|
||||
}
|
||||
let summarization_model = registry
|
||||
.read(cx)
|
||||
|
@ -764,13 +771,15 @@ impl AgentModelSelector for NativeAgentConnection {
|
|||
else {
|
||||
return Task::ready(Err(anyhow!("Session not found")));
|
||||
};
|
||||
let model = thread.read(cx).model().clone();
|
||||
let Some(model) = thread.read(cx).model() else {
|
||||
return Task::ready(Err(anyhow!("Model not found")));
|
||||
};
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Provider not found")));
|
||||
};
|
||||
Task::ready(Ok(LanguageModels::map_language_model_to_info(
|
||||
&model, &provider,
|
||||
model, &provider,
|
||||
)))
|
||||
}
|
||||
|
||||
|
@ -821,20 +830,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
let available_count = registry.available_models(cx).count();
|
||||
log::debug!("Total available models: {}", available_count);
|
||||
|
||||
let default_model = registry
|
||||
.default_model()
|
||||
.and_then(|default_model| {
|
||||
dbg!("here!");
|
||||
agent
|
||||
.models
|
||||
.model_from_id(&LanguageModels::model_id(&default_model.model))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
log::warn!("No default model configured in settings");
|
||||
anyhow!(
|
||||
"No default model. Please configure a default model in settings."
|
||||
)
|
||||
})?;
|
||||
let default_model = registry.default_model().and_then(|default_model| {
|
||||
agent
|
||||
.models
|
||||
.model_from_id(&LanguageModels::model_id(&default_model.model))
|
||||
});
|
||||
|
||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||
|
||||
|
@ -997,13 +997,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
log::debug!("Message id: {:?}", id);
|
||||
log::debug!("Message content: {:?}", content);
|
||||
|
||||
Ok(thread.update(cx, |thread, cx| {
|
||||
log::info!(
|
||||
"Sending message to thread with model: {:?}",
|
||||
thread.model().name()
|
||||
);
|
||||
thread.send(id, content, cx)
|
||||
}))
|
||||
thread.update(cx, |thread, cx| thread.send(id, content, cx))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1235,7 +1229,7 @@ mod tests {
|
|||
agent.read_with(cx, |agent, _| {
|
||||
let session = agent.sessions.get(&session_id).unwrap();
|
||||
session.thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.model().id().0, "fake");
|
||||
assert_eq!(thread.model().unwrap().id().0, "fake");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ async fn test_echo(cx: &mut TestAppContext) {
|
|||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx)
|
||||
})
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
|
@ -73,6 +74,7 @@ async fn test_thinking(cx: &mut TestAppContext) {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
|
@ -101,9 +103,11 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
|
|||
|
||||
project_context.borrow_mut().shell = "test-shell".into();
|
||||
thread.update(cx, |thread, _| thread.add_tool(EchoTool));
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
assert_eq!(
|
||||
|
@ -136,9 +140,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||
let fake_model = model.as_fake();
|
||||
|
||||
// Send initial user message and verify it's cached
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Message 1"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Message 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
|
@ -157,9 +163,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||
cx.run_until_parked();
|
||||
|
||||
// Send another user message and verify only the latest is cached
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Message 2"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Message 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
|
@ -191,9 +199,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||
|
||||
// Simulate a tool call and verify that the latest tool result is cached
|
||||
thread.update(cx, |thread, _| thread.add_tool(EchoTool));
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Use the echo tool"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Use the echo tool"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let tool_use = LanguageModelToolUse {
|
||||
|
@ -273,6 +283,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
|
||||
|
@ -291,6 +302,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
|
||||
|
@ -322,10 +334,12 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
|
|||
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
|
||||
|
||||
// Test a tool call that's likely to complete *before* streaming stops.
|
||||
let mut events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(WordListTool);
|
||||
thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
|
||||
});
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(WordListTool);
|
||||
thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut saw_partial_tool_use = false;
|
||||
while let Some(event) = events.next().await {
|
||||
|
@ -371,10 +385,12 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let mut events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
});
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
|
@ -501,9 +517,11 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let mut events = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
});
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
|
@ -528,10 +546,12 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
});
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let tool_use = LanguageModelToolUse {
|
||||
id: "tool_id_1".into(),
|
||||
|
@ -644,10 +664,12 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
});
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let tool_use = LanguageModelToolUse {
|
||||
|
@ -677,9 +699,11 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||
.is::<language_model::ToolUseLimitReachedError>()
|
||||
);
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), vec!["ghi"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), vec!["ghi"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
assert_eq!(
|
||||
|
@ -788,6 +812,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
|
@ -855,10 +880,12 @@ async fn test_profiles(cx: &mut TestAppContext) {
|
|||
cx.run_until_parked();
|
||||
|
||||
// Test that test-1 profile (default) has echo and delay tools
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-1".into()));
|
||||
thread.send(UserMessageId::new(), ["test"], cx);
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-1".into()));
|
||||
thread.send(UserMessageId::new(), ["test"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
|
@ -873,10 +900,12 @@ async fn test_profiles(cx: &mut TestAppContext) {
|
|||
fake_model.end_last_completion_stream();
|
||||
|
||||
// Switch to test-2 profile, and verify that it has only the infinite tool.
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-2".into()));
|
||||
thread.send(UserMessageId::new(), ["test2"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-2".into()));
|
||||
thread.send(UserMessageId::new(), ["test2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
assert_eq!(pending_completions.len(), 1);
|
||||
|
@ -894,15 +923,17 @@ async fn test_profiles(cx: &mut TestAppContext) {
|
|||
async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
|
||||
|
||||
let mut events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(InfiniteTool);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(
|
||||
UserMessageId::new(),
|
||||
["Call the echo tool, then call the infinite tool, then explain their output"],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(InfiniteTool);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(
|
||||
UserMessageId::new(),
|
||||
["Call the echo tool, then call the infinite tool, then explain their output"],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Wait until both tools are called.
|
||||
let mut expected_tools = vec!["Echo", "Infinite Tool"];
|
||||
|
@ -958,6 +989,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
thread.update(cx, |thread, _cx| {
|
||||
|
@ -976,16 +1008,20 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events_1 = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 1"], cx)
|
||||
});
|
||||
let events_1 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey 1!");
|
||||
cx.run_until_parked();
|
||||
|
||||
let events_2 = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 2"], cx)
|
||||
});
|
||||
let events_2 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey 2!");
|
||||
fake_model
|
||||
|
@ -1003,9 +1039,11 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events_1 = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 1"], cx)
|
||||
});
|
||||
let events_1 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey 1!");
|
||||
fake_model
|
||||
|
@ -1013,9 +1051,11 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
|
|||
fake_model.end_last_completion_stream();
|
||||
let events_1 = events_1.collect::<Vec<_>>().await;
|
||||
|
||||
let events_2 = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 2"], cx)
|
||||
});
|
||||
let events_2 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey 2!");
|
||||
fake_model
|
||||
|
@ -1032,9 +1072,11 @@ async fn test_refusal(cx: &mut TestAppContext) {
|
|||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello"], cx)
|
||||
});
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
|
@ -1080,9 +1122,11 @@ async fn test_truncate(cx: &mut TestAppContext) {
|
|||
let fake_model = model.as_fake();
|
||||
|
||||
let message_id = UserMessageId::new();
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(message_id.clone(), ["Hello"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_id.clone(), ["Hello"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
|
@ -1121,9 +1165,11 @@ async fn test_truncate(cx: &mut TestAppContext) {
|
|||
});
|
||||
|
||||
// Ensure we can still send a new message after truncation.
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hi"], cx)
|
||||
});
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hi"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
thread.update(cx, |thread, _cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(),
|
||||
|
@ -1289,9 +1335,11 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
|||
thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool));
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let mut events = thread.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Think"], cx)
|
||||
});
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Think"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Simulate streaming partial input.
|
||||
|
@ -1505,8 +1553,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
|||
context_server_registry,
|
||||
action_log,
|
||||
templates,
|
||||
model.clone(),
|
||||
None,
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -494,8 +494,7 @@ pub struct Thread {
|
|||
profile_id: AgentProfileId,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
templates: Arc<Templates>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||
model: Option<Arc<dyn LanguageModel>>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
@ -508,8 +507,7 @@ impl Thread {
|
|||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
action_log: Entity<ActionLog>,
|
||||
templates: Arc<Templates>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||
model: Option<Arc<dyn LanguageModel>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||
|
@ -848,22 +846,12 @@ impl Thread {
|
|||
&self.action_log
|
||||
}
|
||||
|
||||
pub fn model(&self) -> &Arc<dyn LanguageModel> {
|
||||
&self.model
|
||||
pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> {
|
||||
self.model.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
self.model = model;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn set_summarization_model(
|
||||
&mut self,
|
||||
model: Option<Arc<dyn LanguageModel>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.summarization_model = model;
|
||||
cx.notify()
|
||||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>) {
|
||||
self.model = Some(model);
|
||||
}
|
||||
|
||||
pub fn completion_mode(&self) -> CompletionMode {
|
||||
|
@ -932,7 +920,7 @@ impl Thread {
|
|||
cx.notify();
|
||||
|
||||
log::info!("Total messages in thread: {}", self.messages.len());
|
||||
Ok(self.run_turn(cx))
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
/// Sending a message results in the model streaming a response, which could include tool calls.
|
||||
|
@ -947,7 +935,9 @@ impl Thread {
|
|||
where
|
||||
T: Into<UserMessageContent>,
|
||||
{
|
||||
log::info!("Thread::send called with model: {:?}", self.model.name());
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {:?}", model.name());
|
||||
self.advance_prompt_id();
|
||||
|
||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
|
@ -982,7 +972,7 @@ impl Thread {
|
|||
);
|
||||
let request = this.update(cx, |this, cx| {
|
||||
this.build_completion_request(completion_intent, cx)
|
||||
})?;
|
||||
})??;
|
||||
|
||||
log::info!("Calling model.stream_completion");
|
||||
let mut events = model.stream_completion(request, cx).await?;
|
||||
|
@ -1074,7 +1064,7 @@ impl Thread {
|
|||
.ok();
|
||||
}),
|
||||
});
|
||||
events_rx
|
||||
Ok(events_rx)
|
||||
}
|
||||
|
||||
pub fn generate_title_if_needed(&mut self, cx: &mut Context<Self>) {
|
||||
|
@ -1342,7 +1332,7 @@ impl Thread {
|
|||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
});
|
||||
let supports_images = self.model.supports_images();
|
||||
let supports_images = self.model().map_or(false, |model| model.supports_images());
|
||||
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
||||
log::info!("Running tool {}", tool_use.name);
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
|
@ -1429,7 +1419,9 @@ impl Thread {
|
|||
&self,
|
||||
completion_intent: CompletionIntent,
|
||||
cx: &mut App,
|
||||
) -> LanguageModelRequest {
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::debug!("Building completion request");
|
||||
log::debug!("Completion intent: {:?}", completion_intent);
|
||||
log::debug!("Completion mode: {:?}", self.completion_mode);
|
||||
|
@ -1445,9 +1437,7 @@ impl Thread {
|
|||
Some(LanguageModelRequestTool {
|
||||
name: tool_name,
|
||||
description: tool.description().to_string(),
|
||||
input_schema: tool
|
||||
.input_schema(self.model.tool_input_format())
|
||||
.log_err()?,
|
||||
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
@ -1466,20 +1456,22 @@ impl Thread {
|
|||
tools,
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AgentSettings::temperature_for_model(self.model(), cx),
|
||||
temperature: AgentSettings::temperature_for_model(&model, cx),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
log::debug!("Completion request built successfully");
|
||||
request
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
fn tools<'a>(&'a self, cx: &'a App) -> Result<impl Iterator<Item = &'a Arc<dyn AnyAgentTool>>> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
let profile = AgentSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&self.profile_id)
|
||||
.context("profile not found")?;
|
||||
let provider_id = self.model.provider_id();
|
||||
let provider_id = model.provider_id();
|
||||
|
||||
Ok(self
|
||||
.tools
|
||||
|
|
|
@ -5,8 +5,8 @@ use agent::{TextThreadStore, ThreadStore};
|
|||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, TextStyleRefinement,
|
||||
WeakEntity, Window,
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||
TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use project::Project;
|
||||
|
@ -61,33 +61,44 @@ impl EntryViewState {
|
|||
AgentThreadEntry::UserMessage(message) => {
|
||||
let has_id = message.id.is_some();
|
||||
let chunks = message.chunks.clone();
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.text_thread_store.clone(),
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if !has_id {
|
||||
editor.set_read_only(true, cx);
|
||||
if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) {
|
||||
if !editor.focus_handle(cx).is_focused(window) {
|
||||
// Only update if we are not editing.
|
||||
// If we are, cancelling the edit will set the message to the newest content.
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_message(chunks, window, cx);
|
||||
});
|
||||
}
|
||||
editor.set_message(chunks, window, cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&message_editor, move |_, editor, event, cx| {
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::MessageEditorEvent(editor, *event),
|
||||
} else {
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.text_thread_store.clone(),
|
||||
"Edit message - @ to include context",
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if !has_id {
|
||||
editor.set_read_only(true, cx);
|
||||
}
|
||||
editor.set_message(chunks, window, cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&message_editor, move |_, editor, event, cx| {
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::MessageEditorEvent(editor, *event),
|
||||
})
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
self.set_entry(index, Entry::UserMessage(message_editor));
|
||||
.detach();
|
||||
self.set_entry(index, Entry::UserMessage(message_editor));
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
|
||||
|
|
|
@ -71,6 +71,7 @@ impl MessageEditor {
|
|||
project: Entity<Project>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
placeholder: impl Into<Arc<str>>,
|
||||
mode: EditorMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -94,7 +95,7 @@ impl MessageEditor {
|
|||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let mut editor = Editor::new(mode, buffer, None, window, cx);
|
||||
editor.set_placeholder_text("Message the agent - @ to include files", cx);
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_use_modal_editing(true);
|
||||
|
@ -1276,6 +1277,7 @@ mod tests {
|
|||
project.clone(),
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
|
@ -1473,6 +1475,7 @@ mod tests {
|
|||
project.clone(),
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
min_lines: 1,
|
||||
|
|
|
@ -95,7 +95,9 @@ impl ProfileProvider for Entity<agent2::Thread> {
|
|||
}
|
||||
|
||||
fn profiles_supported(&self, cx: &App) -> bool {
|
||||
self.read(cx).model().supports_tools()
|
||||
self.read(cx)
|
||||
.model()
|
||||
.map_or(false, |model| model.supports_tools())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,6 +161,7 @@ impl AcpThreadView {
|
|||
project.clone(),
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
"Message the agent - @ to include context",
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
max_lines: Some(MAX_EDITOR_LINES),
|
||||
|
@ -447,7 +450,9 @@ impl AcpThreadView {
|
|||
match event {
|
||||
MessageEditorEvent::Send => self.send(window, cx),
|
||||
MessageEditorEvent::Cancel => self.cancel_generation(cx),
|
||||
MessageEditorEvent::Focus => {}
|
||||
MessageEditorEvent::Focus => {
|
||||
self.cancel_editing(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -492,12 +497,41 @@ impl AcpThreadView {
|
|||
}
|
||||
|
||||
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(thread) = self.thread() {
|
||||
if thread.read(cx).status() != ThreadStatus::Idle {
|
||||
self.stop_current_and_send_new_message(window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(window, cx));
|
||||
self.send_impl(contents, window, cx)
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(thread) = self.thread().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
|
||||
let contents = self
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(window, cx));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cancelled.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.send_impl(contents, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn send_impl(
|
||||
&mut self,
|
||||
contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>,
|
||||
|
@ -765,44 +799,98 @@ impl AcpThreadView {
|
|||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let primary = match &entry {
|
||||
AgentThreadEntry::UserMessage(message) => div()
|
||||
.id(("user_message", entry_ix))
|
||||
.py_4()
|
||||
.px_2()
|
||||
.children(message.id.clone().and_then(|message_id| {
|
||||
message.checkpoint.as_ref()?.show.then(|| {
|
||||
Button::new("restore-checkpoint", "Restore Checkpoint")
|
||||
.icon(IconName::Undo)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::Start)
|
||||
.label_size(LabelSize::XSmall)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.rewind(&message_id, cx);
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.gap_1p5()
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.text_xs()
|
||||
.children(
|
||||
self.entry_view_state
|
||||
.read(cx)
|
||||
.entry(entry_ix)
|
||||
.and_then(|entry| entry.message_editor())
|
||||
.map(|editor| {
|
||||
self.render_sent_message_editor(entry_ix, editor, cx)
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any(),
|
||||
AgentThreadEntry::UserMessage(message) => {
|
||||
let Some(editor) = self
|
||||
.entry_view_state
|
||||
.read(cx)
|
||||
.entry(entry_ix)
|
||||
.and_then(|entry| entry.message_editor())
|
||||
.cloned()
|
||||
else {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
let editing = self.editing_message == Some(entry_ix);
|
||||
let editor_focus = editor.focus_handle(cx).is_focused(window);
|
||||
let focus_border = cx.theme().colors().border_focused;
|
||||
|
||||
div()
|
||||
.id(("user_message", entry_ix))
|
||||
.py_4()
|
||||
.px_2()
|
||||
.children(message.id.clone().and_then(|message_id| {
|
||||
message.checkpoint.as_ref()?.show.then(|| {
|
||||
Button::new("restore-checkpoint", "Restore Checkpoint")
|
||||
.icon(IconName::Undo)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::Start)
|
||||
.label_size(LabelSize::XSmall)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.rewind(&message_id, cx);
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.p_3()
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.when(editing && !editor_focus, |this| this.border_dashed())
|
||||
.border_color(cx.theme().colors().border)
|
||||
.map(|this|{
|
||||
if editor_focus {
|
||||
this.border_color(focus_border)
|
||||
} else {
|
||||
this.hover(|s| s.border_color(focus_border.opacity(0.8)))
|
||||
}
|
||||
})
|
||||
.text_xs()
|
||||
.child(editor.clone().into_any_element()),
|
||||
)
|
||||
.when(editor_focus, |this|
|
||||
this.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_neg_3p5()
|
||||
.right_3()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(Self::cancel_editing))
|
||||
)
|
||||
.child(
|
||||
IconButton::new("regenerate", IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text(
|
||||
"Editing will restart the thread from this point."
|
||||
))
|
||||
.on_click(cx.listener({
|
||||
let editor = editor.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.regenerate(
|
||||
entry_ix, &editor, window, cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||
let style = default_markdown_style(false, window, cx);
|
||||
let message_body = v_flex()
|
||||
|
@ -877,20 +965,12 @@ impl AcpThreadView {
|
|||
if let Some(editing_index) = self.editing_message.as_ref()
|
||||
&& *editing_index < entry_ix
|
||||
{
|
||||
let backdrop = div()
|
||||
.id(("backdrop", entry_ix))
|
||||
.size_full()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.opacity(0.8)
|
||||
.block_mouse_except_scroll()
|
||||
.on_click(cx.listener(Self::cancel_editing));
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.child(primary)
|
||||
.child(backdrop)
|
||||
.opacity(0.2)
|
||||
.block_mouse_except_scroll()
|
||||
.id("overlay")
|
||||
.on_click(cx.listener(Self::cancel_editing))
|
||||
.into_any_element()
|
||||
} else {
|
||||
primary
|
||||
|
@ -2467,12 +2547,15 @@ impl AcpThreadView {
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn as_native_connection(&self, cx: &App) -> Option<Rc<agent2::NativeAgentConnection>> {
|
||||
pub(crate) fn as_native_connection(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> Option<Rc<agent2::NativeAgentConnection>> {
|
||||
let acp_thread = self.thread()?.read(cx);
|
||||
acp_thread.connection().clone().downcast()
|
||||
}
|
||||
|
||||
fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
|
||||
pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
|
||||
let acp_thread = self.thread()?.read(cx);
|
||||
self.as_native_connection(cx)?
|
||||
.thread(acp_thread.session_id(), cx)
|
||||
|
@ -2503,7 +2586,10 @@ impl AcpThreadView {
|
|||
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let thread = self.as_native_thread(cx)?.read(cx);
|
||||
|
||||
if !thread.model().supports_burn_mode() {
|
||||
if thread
|
||||
.model()
|
||||
.map_or(true, |model| !model.supports_burn_mode())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -2532,110 +2618,13 @@ impl AcpThreadView {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_sent_message_editor(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
editor: &Entity<MessageEditor>,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
v_flex().w_full().gap_2().child(editor.clone()).when(
|
||||
self.editing_message == Some(entry_ix),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new("Editing will restart the thread from this point.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(self.render_sent_message_editor_buttons(entry_ix, editor, cx)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sent_message_editor_buttons(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
editor: &Entity<MessageEditor>,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.flex_1()
|
||||
.justify_end()
|
||||
.child(
|
||||
IconButton::new("cancel-edit-message", IconName::Close)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Cancel Edit",
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(Self::cancel_editing)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("confirm-edit-message", IconName::Return)
|
||||
.disabled(editor.read(cx).is_empty(cx))
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Regenerate",
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let editor = editor.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.regenerate(entry_ix, &editor, window, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.thread().map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(self.thread().is_none() || is_editor_empty)
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.send(window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
|
||||
let is_generating = self.thread().map_or(false, |thread| {
|
||||
thread.read(cx).status() != ThreadStatus::Idle
|
||||
});
|
||||
|
||||
if is_generating && is_editor_empty {
|
||||
IconButton::new("stop-generation", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
|
@ -2644,6 +2633,29 @@ impl AcpThreadView {
|
|||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
|
||||
.into_any_element()
|
||||
} else {
|
||||
let send_btn_tooltip = if is_editor_empty && !is_generating {
|
||||
"Type to Send"
|
||||
} else if is_generating {
|
||||
"Stop and Send Message"
|
||||
} else {
|
||||
"Send"
|
||||
};
|
||||
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.style(ButtonStyle::Filled)
|
||||
.map(|this| {
|
||||
if is_editor_empty && !is_generating {
|
||||
this.disabled(true).icon_color(Color::Muted)
|
||||
} else {
|
||||
this.icon_color(Color::Accent)
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.send(window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3247,7 +3259,10 @@ impl AcpThreadView {
|
|||
cx: &mut Context<Self>,
|
||||
) -> Option<Callout> {
|
||||
let thread = self.as_native_thread(cx)?;
|
||||
let supports_burn_mode = thread.read(cx).model().supports_burn_mode();
|
||||
let supports_burn_mode = thread
|
||||
.read(cx)
|
||||
.model()
|
||||
.map_or(false, |model| model.supports_burn_mode());
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
|
@ -3619,7 +3634,7 @@ pub(crate) mod tests {
|
|||
async fn test_drop(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await;
|
||||
let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
|
||||
let weak_view = thread_view.downgrade();
|
||||
drop(thread_view);
|
||||
assert!(!weak_view.is_upgradable());
|
||||
|
@ -3629,7 +3644,7 @@ pub(crate) mod tests {
|
|||
async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
|
@ -3814,8 +3829,12 @@ pub(crate) mod tests {
|
|||
}
|
||||
|
||||
impl StubAgentServer<StubAgentConnection> {
|
||||
fn default() -> Self {
|
||||
Self::new(StubAgentConnection::default())
|
||||
fn default_response() -> Self {
|
||||
let conn = StubAgentConnection::new();
|
||||
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
|
||||
content: "Default response".into(),
|
||||
}]);
|
||||
Self::new(conn)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4229,4 +4248,221 @@ pub(crate) mod tests {
|
|||
assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
|
||||
let (thread_view, cx) =
|
||||
setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
|
||||
add_to_workspace(thread_view.clone(), cx);
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Original message to edit", window, cx);
|
||||
});
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
thread_view.send(window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
|
||||
let thread = view.thread().unwrap().read(cx);
|
||||
assert_eq!(thread.entries().len(), 1);
|
||||
|
||||
let editor = view
|
||||
.entry_view_state
|
||||
.read(cx)
|
||||
.entry(0)
|
||||
.unwrap()
|
||||
.message_editor()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
(editor, thread.session_id().clone())
|
||||
});
|
||||
|
||||
// Focus
|
||||
cx.focus(&user_message_editor);
|
||||
|
||||
thread_view.read_with(cx, |view, _cx| {
|
||||
assert_eq!(view.editing_message, Some(0));
|
||||
});
|
||||
|
||||
// Edit
|
||||
user_message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Edited message content", window, cx);
|
||||
});
|
||||
|
||||
thread_view.read_with(cx, |view, _cx| {
|
||||
assert_eq!(view.editing_message, Some(0));
|
||||
});
|
||||
|
||||
// Finish streaming response
|
||||
cx.update(|_, cx| {
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
connection.end_turn(session_id, acp::StopReason::EndTurn);
|
||||
});
|
||||
|
||||
thread_view.read_with(cx, |view, _cx| {
|
||||
assert_eq!(view.editing_message, Some(0));
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should still be editing
|
||||
cx.update(|window, cx| {
|
||||
assert!(user_message_editor.focus_handle(cx).is_focused(window));
|
||||
assert_eq!(thread_view.read(cx).editing_message, Some(0));
|
||||
assert_eq!(
|
||||
user_message_editor.read(cx).text(cx),
|
||||
"Edited message content"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_interrupt(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
|
||||
let (thread_view, cx) =
|
||||
setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
|
||||
add_to_workspace(thread_view.clone(), cx);
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Message 1", window, cx);
|
||||
});
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
thread_view.send(window, cx);
|
||||
});
|
||||
|
||||
let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
|
||||
let thread = view.thread().unwrap();
|
||||
|
||||
(thread.clone(), thread.read(cx).session_id().clone())
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update(|_, cx| {
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk {
|
||||
content: "Message 1 resp".into(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
indoc::indoc! {"
|
||||
## User
|
||||
|
||||
Message 1
|
||||
|
||||
## Assistant
|
||||
|
||||
Message 1 resp
|
||||
|
||||
"}
|
||||
)
|
||||
});
|
||||
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Message 2", window, cx);
|
||||
});
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
thread_view.send(window, cx);
|
||||
});
|
||||
|
||||
cx.update(|_, cx| {
|
||||
// Simulate a response sent after beginning to cancel
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk {
|
||||
content: "onse".into(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Last Message 1 response should appear before Message 2
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
indoc::indoc! {"
|
||||
## User
|
||||
|
||||
Message 1
|
||||
|
||||
## Assistant
|
||||
|
||||
Message 1 response
|
||||
|
||||
## User
|
||||
|
||||
Message 2
|
||||
|
||||
"}
|
||||
)
|
||||
});
|
||||
|
||||
cx.update(|_, cx| {
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk {
|
||||
content: "Message 2 response".into(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
indoc::indoc! {"
|
||||
## User
|
||||
|
||||
Message 1
|
||||
|
||||
## Assistant
|
||||
|
||||
Message 1 response
|
||||
|
||||
## User
|
||||
|
||||
Message 2
|
||||
|
||||
## Assistant
|
||||
|
||||
Message 2 response
|
||||
|
||||
"}
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T
|
|||
use language_model::LanguageModelRegistry;
|
||||
use language_models::{
|
||||
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
|
||||
provider::open_ai_compatible::AvailableModel,
|
||||
provider::open_ai_compatible::{AvailableModel, ModelCapabilities},
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
|
||||
use ui::{
|
||||
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
|
||||
};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
|
@ -69,11 +71,19 @@ impl AddLlmProviderInput {
|
|||
}
|
||||
}
|
||||
|
||||
struct ModelCapabilityToggles {
|
||||
pub supports_tools: ToggleState,
|
||||
pub supports_images: ToggleState,
|
||||
pub supports_parallel_tool_calls: ToggleState,
|
||||
pub supports_prompt_cache_key: ToggleState,
|
||||
}
|
||||
|
||||
struct ModelInput {
|
||||
name: Entity<SingleLineInput>,
|
||||
max_completion_tokens: Entity<SingleLineInput>,
|
||||
max_output_tokens: Entity<SingleLineInput>,
|
||||
max_tokens: Entity<SingleLineInput>,
|
||||
capabilities: ModelCapabilityToggles,
|
||||
}
|
||||
|
||||
impl ModelInput {
|
||||
|
@ -100,11 +110,23 @@ impl ModelInput {
|
|||
cx,
|
||||
);
|
||||
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
|
||||
let ModelCapabilities {
|
||||
tools,
|
||||
images,
|
||||
parallel_tool_calls,
|
||||
prompt_cache_key,
|
||||
} = ModelCapabilities::default();
|
||||
Self {
|
||||
name: model_name,
|
||||
max_completion_tokens,
|
||||
max_output_tokens,
|
||||
max_tokens,
|
||||
capabilities: ModelCapabilityToggles {
|
||||
supports_tools: tools.into(),
|
||||
supports_images: images.into(),
|
||||
supports_parallel_tool_calls: parallel_tool_calls.into(),
|
||||
supports_prompt_cache_key: prompt_cache_key.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,6 +158,12 @@ impl ModelInput {
|
|||
.text(cx)
|
||||
.parse::<u64>()
|
||||
.map_err(|_| SharedString::from("Max Tokens must be a number"))?,
|
||||
capabilities: ModelCapabilities {
|
||||
tools: self.capabilities.supports_tools.selected(),
|
||||
images: self.capabilities.supports_images.selected(),
|
||||
parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(),
|
||||
prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -322,6 +350,55 @@ impl AddLlmProviderModal {
|
|||
.child(model.max_output_tokens.clone()),
|
||||
)
|
||||
.child(model.max_tokens.clone())
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools)
|
||||
.label("Supports tools")
|
||||
.on_click(cx.listener(move |this, checked, _window, cx| {
|
||||
this.input.models[ix].capabilities.supports_tools = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new(("supports-images", ix), model.capabilities.supports_images)
|
||||
.label("Supports images")
|
||||
.on_click(cx.listener(move |this, checked, _window, cx| {
|
||||
this.input.models[ix].capabilities.supports_images = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new(
|
||||
("supports-parallel-tool-calls", ix),
|
||||
model.capabilities.supports_parallel_tool_calls,
|
||||
)
|
||||
.label("Supports parallel_tool_calls")
|
||||
.on_click(cx.listener(
|
||||
move |this, checked, _window, cx| {
|
||||
this.input.models[ix]
|
||||
.capabilities
|
||||
.supports_parallel_tool_calls = *checked;
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new(
|
||||
("supports-prompt-cache-key", ix),
|
||||
model.capabilities.supports_prompt_cache_key,
|
||||
)
|
||||
.label("Supports prompt_cache_key")
|
||||
.on_click(cx.listener(
|
||||
move |this, checked, _window, cx| {
|
||||
this.input.models[ix].capabilities.supports_prompt_cache_key =
|
||||
*checked;
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.when(has_more_than_one_model, |this| {
|
||||
this.child(
|
||||
Button::new(("remove-model", ix), "Remove Model")
|
||||
|
@ -562,6 +639,93 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_model_input_default_capabilities(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let model_input = ModelInput::new(window, cx);
|
||||
model_input.name.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("somemodel", window, cx);
|
||||
});
|
||||
});
|
||||
assert_eq!(
|
||||
model_input.capabilities.supports_tools,
|
||||
ToggleState::Selected
|
||||
);
|
||||
assert_eq!(
|
||||
model_input.capabilities.supports_images,
|
||||
ToggleState::Unselected
|
||||
);
|
||||
assert_eq!(
|
||||
model_input.capabilities.supports_parallel_tool_calls,
|
||||
ToggleState::Unselected
|
||||
);
|
||||
assert_eq!(
|
||||
model_input.capabilities.supports_prompt_cache_key,
|
||||
ToggleState::Unselected
|
||||
);
|
||||
|
||||
let parsed_model = model_input.parse(cx).unwrap();
|
||||
assert_eq!(parsed_model.capabilities.tools, true);
|
||||
assert_eq!(parsed_model.capabilities.images, false);
|
||||
assert_eq!(parsed_model.capabilities.parallel_tool_calls, false);
|
||||
assert_eq!(parsed_model.capabilities.prompt_cache_key, false);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let mut model_input = ModelInput::new(window, cx);
|
||||
model_input.name.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("somemodel", window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
model_input.capabilities.supports_tools = ToggleState::Unselected;
|
||||
model_input.capabilities.supports_images = ToggleState::Unselected;
|
||||
model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected;
|
||||
model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
|
||||
|
||||
let parsed_model = model_input.parse(cx).unwrap();
|
||||
assert_eq!(parsed_model.capabilities.tools, false);
|
||||
assert_eq!(parsed_model.capabilities.images, false);
|
||||
assert_eq!(parsed_model.capabilities.parallel_tool_calls, false);
|
||||
assert_eq!(parsed_model.capabilities.prompt_cache_key, false);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let mut model_input = ModelInput::new(window, cx);
|
||||
model_input.name.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("somemodel", window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
model_input.capabilities.supports_tools = ToggleState::Selected;
|
||||
model_input.capabilities.supports_images = ToggleState::Unselected;
|
||||
model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected;
|
||||
model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
|
||||
|
||||
let parsed_model = model_input.parse(cx).unwrap();
|
||||
assert_eq!(parsed_model.name, "somemodel");
|
||||
assert_eq!(parsed_model.capabilities.tools, true);
|
||||
assert_eq!(parsed_model.capabilities.images, false);
|
||||
assert_eq!(parsed_model.capabilities.parallel_tool_calls, true);
|
||||
assert_eq!(parsed_model.capabilities.prompt_cache_key, false);
|
||||
});
|
||||
}
|
||||
|
||||
async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
|
|
|
@ -577,6 +577,7 @@ impl AgentPanel {
|
|||
panel.width = serialized_panel.width.map(|w| w.round());
|
||||
if let Some(selected_agent) = serialized_panel.selected_agent {
|
||||
panel.selected_agent = selected_agent;
|
||||
panel.new_agent_thread(selected_agent, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
|
@ -1659,16 +1660,53 @@ impl AgentPanel {
|
|||
menu
|
||||
}
|
||||
|
||||
pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context<Self>) {
|
||||
pub fn set_selected_agent(
|
||||
&mut self,
|
||||
agent: AgentType,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent;
|
||||
self.serialize(cx);
|
||||
self.new_agent_thread(agent, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_agent(&self) -> AgentType {
|
||||
self.selected_agent
|
||||
}
|
||||
|
||||
pub fn new_agent_thread(
|
||||
&mut self,
|
||||
agent: AgentType,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
NewThread {
|
||||
from_thread_id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AgentType::TextThread => {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}
|
||||
AgentType::NativeAgent => {
|
||||
self.new_external_thread(Some(crate::ExternalAgent::NativeAgent), window, cx)
|
||||
}
|
||||
AgentType::Gemini => {
|
||||
self.new_external_thread(Some(crate::ExternalAgent::Gemini), window, cx)
|
||||
}
|
||||
AgentType::ClaudeCode => {
|
||||
self.new_external_thread(Some(crate::ExternalAgent::ClaudeCode), window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentPanel {
|
||||
|
@ -2256,16 +2294,13 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
AgentType::Zed,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.dispatch_action(
|
||||
NewThread::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
@ -2285,13 +2320,13 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
AgentType::TextThread,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
@ -2310,19 +2345,13 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
AgentType::NativeAgent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::NativeAgent),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
@ -2343,19 +2372,13 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
AgentType::Gemini,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
@ -2374,19 +2397,13 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::ClaudeCode),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
@ -2637,7 +2654,13 @@ impl AgentPanel {
|
|||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
|
||||
ActiveView::History | ActiveView::Configuration => false,
|
||||
ActiveView::ExternalAgentThread { thread_view, .. }
|
||||
if thread_view.read(cx).as_native_thread(cx).is_none() =>
|
||||
{
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
let history_is_empty = self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
|
||||
|
@ -2652,9 +2675,6 @@ impl AgentPanel {
|
|||
|
||||
history_is_empty || !has_configured_non_zed_providers
|
||||
}
|
||||
ActiveView::ExternalAgentThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ pub struct NewExternalAgentThread {
|
|||
agent: Option<ExternalAgent>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
|
|
|
@ -44,6 +44,7 @@ rpc = { workspace = true, features = ["gpui"] }
|
|||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
|
|
|
@ -1410,6 +1410,12 @@ impl Client {
|
|||
|
||||
open_url_tx.send(url).log_err();
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CallbackParams {
|
||||
pub user_id: String,
|
||||
pub access_token: String,
|
||||
}
|
||||
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
|
@ -1420,17 +1426,13 @@ impl Client {
|
|||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
let callback_params: CallbackParams =
|
||||
serde_urlencoded::from_str(url.query().unwrap_or_default())
|
||||
.context(
|
||||
"failed to parse sign-in callback query parameters",
|
||||
)?;
|
||||
|
||||
let post_auth_url =
|
||||
http.build_url("/native_app_signin_succeeded");
|
||||
|
@ -1445,8 +1447,8 @@ impl Client {
|
|||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id.context("missing user_id parameter")?,
|
||||
access_token.context("missing access_token parameter")?,
|
||||
callback_params.user_id,
|
||||
callback_params.access_token,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ use util::{ResultExt, maybe};
|
|||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PythonDebugAdapter {
|
||||
base_venv_path: OnceCell<Result<Arc<Path>, String>>,
|
||||
debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
|
||||
}
|
||||
|
||||
|
@ -91,14 +92,12 @@ impl PythonDebugAdapter {
|
|||
})
|
||||
}
|
||||
|
||||
async fn fetch_wheel(delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
|
||||
let system_python = Self::system_python_name(delegate)
|
||||
.await
|
||||
.ok_or_else(|| String::from("Could not find a Python installation"))?;
|
||||
let command: &OsStr = system_python.as_ref();
|
||||
async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
|
||||
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
|
||||
std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
|
||||
let installation_succeeded = util::command::new_smol_command(command)
|
||||
let system_python = self.base_venv_path(delegate).await?;
|
||||
|
||||
let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
|
||||
.args([
|
||||
"-m",
|
||||
"pip",
|
||||
|
@ -114,7 +113,7 @@ impl PythonDebugAdapter {
|
|||
.status
|
||||
.success();
|
||||
if !installation_succeeded {
|
||||
return Err("debugpy installation failed".into());
|
||||
return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
|
||||
}
|
||||
|
||||
let wheel_path = std::fs::read_dir(&download_dir)
|
||||
|
@ -139,7 +138,7 @@ impl PythonDebugAdapter {
|
|||
Ok(Arc::from(wheel_path.path()))
|
||||
}
|
||||
|
||||
async fn maybe_fetch_new_wheel(delegate: &Arc<dyn DapDelegate>) {
|
||||
async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
|
||||
let latest_release = delegate
|
||||
.http_client()
|
||||
.get(
|
||||
|
@ -191,7 +190,7 @@ impl PythonDebugAdapter {
|
|||
)
|
||||
.await
|
||||
.ok()?;
|
||||
Self::fetch_wheel(delegate).await.ok()?;
|
||||
self.fetch_wheel(delegate).await.ok()?;
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
|
@ -204,7 +203,7 @@ impl PythonDebugAdapter {
|
|||
) -> Result<Arc<Path>, String> {
|
||||
self.debugpy_whl_base_path
|
||||
.get_or_init(|| async move {
|
||||
Self::maybe_fetch_new_wheel(delegate).await;
|
||||
self.maybe_fetch_new_wheel(delegate).await;
|
||||
Ok(Arc::from(
|
||||
debug_adapters_dir()
|
||||
.join(Self::ADAPTER_NAME)
|
||||
|
@ -217,6 +216,45 @@ impl PythonDebugAdapter {
|
|||
.clone()
|
||||
}
|
||||
|
||||
async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
|
||||
self.base_venv_path
|
||||
.get_or_init(|| async {
|
||||
let base_python = Self::system_python_name(delegate)
|
||||
.await
|
||||
.ok_or_else(|| String::from("Could not find a Python installation"))?;
|
||||
|
||||
let did_succeed = util::command::new_smol_command(base_python)
|
||||
.args(["-m", "venv", "zed_base_venv"])
|
||||
.current_dir(
|
||||
paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()),
|
||||
)
|
||||
.spawn()
|
||||
.map_err(|e| format!("{e:#?}"))?
|
||||
.status()
|
||||
.await
|
||||
.map_err(|e| format!("{e:#?}"))?
|
||||
.success();
|
||||
if !did_succeed {
|
||||
return Err("Failed to create base virtual environment".into());
|
||||
}
|
||||
|
||||
const DIR: &'static str = if cfg!(target_os = "windows") {
|
||||
"Scripts"
|
||||
} else {
|
||||
"bin"
|
||||
};
|
||||
Ok(Arc::from(
|
||||
paths::debug_adapters_dir()
|
||||
.join(Self::DEBUG_ADAPTER_NAME.as_ref())
|
||||
.join("zed_base_venv")
|
||||
.join(DIR)
|
||||
.join("python3")
|
||||
.as_ref(),
|
||||
))
|
||||
})
|
||||
.await
|
||||
.clone()
|
||||
}
|
||||
async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
|
||||
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
|
||||
let mut name = None;
|
||||
|
|
|
@ -48,7 +48,7 @@ pub struct Inlay {
|
|||
impl Inlay {
|
||||
pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
|
||||
let mut text = hint.text();
|
||||
if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') {
|
||||
if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
|
||||
text.push(" ");
|
||||
}
|
||||
if hint.padding_left && text.chars_at(0).next() != Some(' ') {
|
||||
|
@ -1305,6 +1305,29 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inlay_hint_padding_with_multibyte_chars() {
|
||||
assert_eq!(
|
||||
Inlay::hint(
|
||||
0,
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String("🎨".to_string()),
|
||||
position: text::Anchor::default(),
|
||||
padding_left: true,
|
||||
padding_right: true,
|
||||
tooltip: None,
|
||||
kind: None,
|
||||
resolve_state: ResolveState::Resolved,
|
||||
},
|
||||
)
|
||||
.text
|
||||
.to_string(),
|
||||
" 🎨 ",
|
||||
"Should pad single emoji correctly"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_basic_inlays(cx: &mut App) {
|
||||
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
|
||||
|
|
|
@ -16022,38 +16022,24 @@ impl Editor {
|
|||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let location_task = editor.update(cx, |_, cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
let language_server_name = project
|
||||
.language_server_statuses(cx)
|
||||
.find(|(id, _)| server_id == *id)
|
||||
.map(|(_, status)| status.name.clone());
|
||||
language_server_name.map(|language_server_name| {
|
||||
project.open_local_buffer_via_lsp(
|
||||
lsp_location.uri.clone(),
|
||||
server_id,
|
||||
language_server_name,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
project.open_local_buffer_via_lsp(lsp_location.uri.clone(), server_id, cx)
|
||||
})
|
||||
})?;
|
||||
let location = match location_task {
|
||||
Some(task) => Some({
|
||||
let target_buffer_handle = task.await.context("open local buffer")?;
|
||||
let range = target_buffer_handle.read_with(cx, |target_buffer, _| {
|
||||
let target_start = target_buffer
|
||||
.clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left);
|
||||
let target_end = target_buffer
|
||||
.clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left);
|
||||
target_buffer.anchor_after(target_start)
|
||||
..target_buffer.anchor_before(target_end)
|
||||
})?;
|
||||
Location {
|
||||
buffer: target_buffer_handle,
|
||||
range,
|
||||
}
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
let location = Some({
|
||||
let target_buffer_handle = location_task.await.context("open local buffer")?;
|
||||
let range = target_buffer_handle.read_with(cx, |target_buffer, _| {
|
||||
let target_start = target_buffer
|
||||
.clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left);
|
||||
let target_end = target_buffer
|
||||
.clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left);
|
||||
target_buffer.anchor_after(target_start)
|
||||
..target_buffer.anchor_before(target_end)
|
||||
})?;
|
||||
Location {
|
||||
buffer: target_buffer_handle,
|
||||
range,
|
||||
}
|
||||
});
|
||||
Ok(location)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8214,6 +8214,216 @@ async fn test_autoindent(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoindent_disabled(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
|
||||
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let text = "fn a() {}";
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
|
||||
editor
|
||||
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([5..5, 8..8, 9..9])
|
||||
});
|
||||
editor.newline(&Newline, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
indoc!(
|
||||
"
|
||||
fn a(
|
||||
|
||||
) {
|
||||
|
||||
}
|
||||
"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
&[
|
||||
Point::new(1, 0)..Point::new(1, 0),
|
||||
Point::new(3, 0)..Point::new(3, 0),
|
||||
Point::new(5, 0)..Point::new(5, 0)
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.auto_indent = Some(true);
|
||||
settings.languages.0.insert(
|
||||
"python".into(),
|
||||
LanguageSettingsContent {
|
||||
auto_indent: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let injected_language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
name: "python".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_python::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
name: LanguageName::new("rust"),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_injection_query(
|
||||
r#"
|
||||
(macro_invocation
|
||||
macro: (identifier) @_macro_name
|
||||
(token_tree) @injection.content
|
||||
(#set! injection.language "python"))
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
cx.language_registry().add(injected_language);
|
||||
cx.language_registry().add(language.clone());
|
||||
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
|
||||
cx.set_state(&r#"struct A {ˇ}"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc!(
|
||||
"struct A {
|
||||
ˇ
|
||||
}"
|
||||
));
|
||||
|
||||
cx.set_state(&r#"select_biased!(ˇ)"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Default::default(), window, cx);
|
||||
editor.handle_input("def ", window, cx);
|
||||
editor.handle_input("(", window, cx);
|
||||
editor.newline(&Default::default(), window, cx);
|
||||
editor.handle_input("a", window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc!(
|
||||
"select_biased!(
|
||||
def (
|
||||
aˇ
|
||||
)
|
||||
)"
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoindent_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
|
@ -40,14 +40,15 @@ use git::{
|
|||
};
|
||||
use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
|
||||
Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent,
|
||||
ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun,
|
||||
TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
|
||||
linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black,
|
||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||
transparent_black,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::{
|
||||
|
@ -60,7 +61,7 @@ use multi_buffer::{
|
|||
};
|
||||
|
||||
use project::{
|
||||
ProjectPath,
|
||||
Entry, ProjectPath,
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
|
||||
};
|
||||
|
@ -80,11 +81,17 @@ use std::{
|
|||
use sum_tree::Bias;
|
||||
use text::{BufferId, SelectionGoal};
|
||||
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
||||
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
|
||||
right_click_menu,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::post_inc;
|
||||
use util::{RangeExt, ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
|
||||
use workspace::{
|
||||
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
|
||||
notifications::NotifyTaskExt,
|
||||
};
|
||||
|
||||
/// Determines what kinds of highlights should be applied to a lines background.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
|
@ -3556,7 +3563,7 @@ impl EditorElement {
|
|||
jump_data: JumpData,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
) -> impl IntoElement {
|
||||
let editor = self.editor.read(cx);
|
||||
let file_status = editor
|
||||
.buffer
|
||||
|
@ -3577,126 +3584,125 @@ impl EditorElement {
|
|||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
|
||||
let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
|
||||
let filename = path
|
||||
let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root);
|
||||
let filename = relative_path
|
||||
.as_ref()
|
||||
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
|
||||
let parent_path = path.as_ref().and_then(|path| {
|
||||
let parent_path = relative_path.as_ref().and_then(|path| {
|
||||
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
||||
});
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
let header =
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.single_line()
|
||||
.when_some(
|
||||
file_status,
|
||||
|el, status| {
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.single_line()
|
||||
.when_some(file_status, |el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
|
@ -3707,49 +3713,145 @@ impl EditorElement {
|
|||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| el.strikethrough())
|
||||
},
|
||||
),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
let file = for_excerpt.buffer.file().cloned();
|
||||
let editor = self.editor.clone();
|
||||
right_click_menu("buffer-header-context-menu")
|
||||
.trigger(move |_, _, _| header)
|
||||
.menu(move |window, cx| {
|
||||
let menu_context = focus_handle.clone();
|
||||
let editor = editor.clone();
|
||||
let file = file.clone();
|
||||
ContextMenu::build(window, cx, move |mut menu, window, cx| {
|
||||
if let Some(file) = file
|
||||
&& let Some(project) = editor.read(cx).project()
|
||||
&& let Some(worktree) =
|
||||
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
|
||||
{
|
||||
let relative_path = file.path();
|
||||
let entry_for_path = worktree.read(cx).entry_for_path(relative_path);
|
||||
let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref());
|
||||
let has_relative_path =
|
||||
worktree.read(cx).root_entry().is_some_and(Entry::is_dir);
|
||||
|
||||
let parent_abs_path =
|
||||
abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||
let relative_path = has_relative_path
|
||||
.then_some(relative_path)
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
let visible_in_project_panel =
|
||||
relative_path.is_some() && worktree.read(cx).is_visible();
|
||||
let reveal_in_project_panel = entry_for_path
|
||||
.filter(|_| visible_in_project_panel)
|
||||
.map(|entry| entry.id);
|
||||
menu = menu
|
||||
.when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| {
|
||||
menu.entry(
|
||||
"Copy Path",
|
||||
Some(Box::new(zed_actions::workspace::CopyPath)),
|
||||
window.handler_for(&editor, move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
abs_path.to_string_lossy().to_string(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
.when(can_open_excerpts && is_selected && path.is_some(), |el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when_some(relative_path, |menu, relative_path| {
|
||||
menu.entry(
|
||||
"Copy Relative Path",
|
||||
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
|
||||
window.handler_for(&editor, move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
relative_path.to_string_lossy().to_string(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
|
||||
|menu| menu.separator(),
|
||||
)
|
||||
.when_some(reveal_in_project_panel, |menu, entry_id| {
|
||||
menu.entry(
|
||||
"Reveal In Project Panel",
|
||||
Some(Box::new(RevealInProjectPanel::default())),
|
||||
window.handler_for(&editor, move |editor, _, cx| {
|
||||
if let Some(project) = &mut editor.project {
|
||||
project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(
|
||||
entry_id,
|
||||
))
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(parent_abs_path, |menu, parent_abs_path| {
|
||||
menu.entry(
|
||||
"Open in Terminal",
|
||||
Some(Box::new(OpenInTerminal)),
|
||||
window.handler_for(&editor, move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenTerminal {
|
||||
working_directory: parent_abs_path.clone(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
menu.context(menu_context)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_blocks(
|
||||
|
@ -10085,6 +10187,71 @@ mod tests {
|
|||
use std::num::NonZeroU32;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
buffer,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
|
||||
|
||||
for x in 1..=100 {
|
||||
let (_, state) = cx.draw(
|
||||
Default::default(),
|
||||
size(px(200. + 0.13 * x as f32), px(500.)),
|
||||
|_, _| EditorElement::new(&editor, style.clone()),
|
||||
);
|
||||
|
||||
assert!(
|
||||
state.position_map.scroll_max.x == 0.,
|
||||
"Soft wrapped editor should have no horizontal scrolling!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx);
|
||||
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
|
||||
|
||||
for x in 1..=100 {
|
||||
let (_, state) = cx.draw(
|
||||
Default::default(),
|
||||
size(px(200. + 0.13 * x as f32), px(500.)),
|
||||
|_, _| EditorElement::new(&editor, style.clone()),
|
||||
);
|
||||
|
||||
assert!(
|
||||
state.position_map.scroll_max.x == 0.,
|
||||
"Soft wrapped editor should have no horizontal scrolling!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_shape_line_numbers(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
|
@ -1275,6 +1275,7 @@ impl ExtensionStore {
|
|||
queries,
|
||||
context_provider,
|
||||
toolchain_provider: None,
|
||||
manifest_name: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -163,6 +163,7 @@ impl HeadlessExtensionStore {
|
|||
queries: LanguageQueries::default(),
|
||||
context_provider: None,
|
||||
toolchain_provider: None,
|
||||
manifest_name: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -938,7 +938,7 @@ impl ExtensionImports for WasmState {
|
|||
binary: settings.binary.map(|binary| settings::CommandSettings {
|
||||
path: binary.path,
|
||||
arguments: binary.arguments,
|
||||
env: binary.env,
|
||||
env: binary.env.map(|env| env.into_iter().collect()),
|
||||
}),
|
||||
settings: settings.settings,
|
||||
initialization_options: settings.initialization_options,
|
||||
|
|
|
@ -116,6 +116,7 @@ pub fn init(cx: &mut App) {
|
|||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
},
|
||||
DirectoryLister::Local(
|
||||
workspace.project().clone(),
|
||||
|
|
|
@ -1833,7 +1833,9 @@ impl GitPanel {
|
|||
|
||||
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
|
||||
Some(staged_entry)
|
||||
} else if let Some(single_tracked_entry) = &self.single_tracked_entry {
|
||||
} else if self.total_staged_count() == 0
|
||||
&& let Some(single_tracked_entry) = &self.single_tracked_entry
|
||||
{
|
||||
Some(single_tracked_entry)
|
||||
} else {
|
||||
None
|
||||
|
@ -2086,6 +2088,7 @@ impl GitPanel {
|
|||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
|
|
|
@ -1278,7 +1278,7 @@ pub enum WindowBackgroundAppearance {
|
|||
}
|
||||
|
||||
/// The options that can be configured for a file dialog prompt
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathPromptOptions {
|
||||
/// Should the prompt allow files to be selected?
|
||||
pub files: bool,
|
||||
|
@ -1286,6 +1286,8 @@ pub struct PathPromptOptions {
|
|||
pub directories: bool,
|
||||
/// Should the prompt allow multiple files to be selected?
|
||||
pub multiple: bool,
|
||||
/// The prompt to show to a user when selecting a path
|
||||
pub prompt: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// What kind of prompt styling to show
|
||||
|
|
|
@ -294,6 +294,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
|
||||
.modal(true)
|
||||
.title(title)
|
||||
.accept_label(options.prompt.as_ref().map(crate::SharedString::as_str))
|
||||
.multiple(options.multiple)
|
||||
.directory(options.directories)
|
||||
.send()
|
||||
|
|
|
@ -705,6 +705,7 @@ impl Platform for MacPlatform {
|
|||
panel.setCanChooseDirectories_(options.directories.to_objc());
|
||||
panel.setCanChooseFiles_(options.files.to_objc());
|
||||
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
||||
|
||||
panel.setCanCreateDirectories(true.to_objc());
|
||||
panel.setResolvesAliases_(false.to_objc());
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
|
@ -730,6 +731,11 @@ impl Platform for MacPlatform {
|
|||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
|
||||
if let Some(prompt) = options.prompt {
|
||||
let _: () = msg_send![panel, setPrompt: ns_string(&prompt)];
|
||||
}
|
||||
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
}
|
||||
})
|
||||
|
|
|
@ -227,7 +227,7 @@ impl WindowsPlatform {
|
|||
| WM_GPUI_CLOSE_ONE_WINDOW
|
||||
| WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
|
||||
| WM_GPUI_DOCK_MENU_ACTION => {
|
||||
if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) {
|
||||
if self.handle_gpui_events(msg.message, msg.wParam, msg.lParam, &msg) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -240,7 +240,7 @@ impl WindowsPlatform {
|
|||
}
|
||||
|
||||
// Returns true if the app should quit.
|
||||
fn handle_gpui_evnets(
|
||||
fn handle_gpui_events(
|
||||
&self,
|
||||
message: u32,
|
||||
wparam: WPARAM,
|
||||
|
@ -787,6 +787,12 @@ fn file_open_dialog(
|
|||
|
||||
unsafe {
|
||||
folder_dialog.SetOptions(dialog_options)?;
|
||||
|
||||
if let Some(prompt) = options.prompt {
|
||||
let prompt: &str = &prompt;
|
||||
folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?;
|
||||
}
|
||||
|
||||
if folder_dialog.Show(window).is_err() {
|
||||
// User cancelled
|
||||
return Ok(None);
|
||||
|
|
|
@ -23,6 +23,11 @@ impl SharedString {
|
|||
pub fn new(str: impl Into<Arc<str>>) -> Self {
|
||||
SharedString(ArcCow::Owned(str.into()))
|
||||
}
|
||||
|
||||
/// Get a &str from the underlying string.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for SharedString {
|
||||
|
|
|
@ -1571,6 +1571,7 @@ impl Buffer {
|
|||
diagnostics: diagnostics.iter().cloned().collect(),
|
||||
lamport_timestamp,
|
||||
};
|
||||
|
||||
self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
|
||||
self.send_operation(op, true, cx);
|
||||
}
|
||||
|
@ -2270,13 +2271,11 @@ impl Buffer {
|
|||
}
|
||||
let new_text = new_text.into();
|
||||
if !new_text.is_empty() || !range.is_empty() {
|
||||
if let Some((prev_range, prev_text)) = edits.last_mut() {
|
||||
if prev_range.end >= range.start {
|
||||
prev_range.end = cmp::max(prev_range.end, range.end);
|
||||
*prev_text = format!("{prev_text}{new_text}").into();
|
||||
} else {
|
||||
edits.push((range, new_text));
|
||||
}
|
||||
if let Some((prev_range, prev_text)) = edits.last_mut()
|
||||
&& prev_range.end >= range.start
|
||||
{
|
||||
prev_range.end = cmp::max(prev_range.end, range.end);
|
||||
*prev_text = format!("{prev_text}{new_text}").into();
|
||||
} else {
|
||||
edits.push((range, new_text));
|
||||
}
|
||||
|
@ -2296,10 +2295,27 @@ impl Buffer {
|
|||
|
||||
if let Some((before_edit, mode)) = autoindent_request {
|
||||
let mut delta = 0isize;
|
||||
let entries = edits
|
||||
let mut previous_setting = None;
|
||||
let entries: Vec<_> = edits
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.zip(&edit_operation.as_edit().unwrap().new_text)
|
||||
.filter(|((_, (range, _)), _)| {
|
||||
let language = before_edit.language_at(range.start);
|
||||
let language_id = language.map(|l| l.id());
|
||||
if let Some((cached_language_id, auto_indent)) = previous_setting
|
||||
&& cached_language_id == language_id
|
||||
{
|
||||
auto_indent
|
||||
} else {
|
||||
// The auto-indent setting is not present in editorconfigs, hence
|
||||
// we can avoid passing the file here.
|
||||
let auto_indent =
|
||||
language_settings(language.map(|l| l.name()), None, cx).auto_indent;
|
||||
previous_setting = Some((language_id, auto_indent));
|
||||
auto_indent
|
||||
}
|
||||
})
|
||||
.map(|((ix, (range, _)), new_text)| {
|
||||
let new_text_length = new_text.len();
|
||||
let old_start = range.start.to_point(&before_edit);
|
||||
|
@ -2373,12 +2389,14 @@ impl Buffer {
|
|||
})
|
||||
.collect();
|
||||
|
||||
self.autoindent_requests.push(Arc::new(AutoindentRequest {
|
||||
before_edit,
|
||||
entries,
|
||||
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
|
||||
ignore_empty_lines: false,
|
||||
}));
|
||||
if !entries.is_empty() {
|
||||
self.autoindent_requests.push(Arc::new(AutoindentRequest {
|
||||
before_edit,
|
||||
entries,
|
||||
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
|
||||
ignore_empty_lines: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
self.end_transaction(cx);
|
||||
|
|
|
@ -44,6 +44,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
|||
use serde_json::Value;
|
||||
use settings::WorktreeId;
|
||||
use smol::future::FutureExt as _;
|
||||
use std::num::NonZeroU32;
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsStr,
|
||||
|
@ -59,7 +60,6 @@ use std::{
|
|||
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
|
||||
},
|
||||
};
|
||||
use std::{num::NonZeroU32, sync::OnceLock};
|
||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||
use task::RunnableTag;
|
||||
pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
|
||||
|
@ -67,7 +67,9 @@ pub use text_diff::{
|
|||
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister};
|
||||
pub use toolchain::{
|
||||
LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
|
||||
};
|
||||
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
|
||||
use util::serde::default_true;
|
||||
|
||||
|
@ -119,8 +121,8 @@ where
|
|||
func(cursor.deref_mut())
|
||||
}
|
||||
|
||||
static NEXT_LANGUAGE_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
|
||||
static NEXT_GRAMMAR_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
|
||||
static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
|
||||
wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine")
|
||||
});
|
||||
|
@ -165,7 +167,6 @@ pub struct CachedLspAdapter {
|
|||
pub adapter: Arc<dyn LspAdapter>,
|
||||
pub reinstall_attempt_count: AtomicU64,
|
||||
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
|
||||
manifest_name: OnceLock<Option<ManifestName>>,
|
||||
}
|
||||
|
||||
impl Debug for CachedLspAdapter {
|
||||
|
@ -201,7 +202,6 @@ impl CachedLspAdapter {
|
|||
adapter,
|
||||
cached_binary: Default::default(),
|
||||
reinstall_attempt_count: AtomicU64::new(0),
|
||||
manifest_name: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -212,7 +212,7 @@ impl CachedLspAdapter {
|
|||
pub async fn get_language_server_command(
|
||||
self: Arc<Self>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
toolchains: Option<Toolchain>,
|
||||
binary_options: LanguageServerBinaryOptions,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
|
@ -281,21 +281,6 @@ impl CachedLspAdapter {
|
|||
.cloned()
|
||||
.unwrap_or_else(|| language_name.lsp_id())
|
||||
}
|
||||
|
||||
pub fn manifest_name(&self) -> Option<ManifestName> {
|
||||
self.manifest_name
|
||||
.get_or_init(|| self.adapter.manifest_name())
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines what gets sent out as a workspace folders content
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum WorkspaceFoldersContent {
|
||||
/// Send out a single entry with the root of the workspace.
|
||||
WorktreeRoot,
|
||||
/// Send out a list of subproject roots.
|
||||
SubprojectRoots,
|
||||
}
|
||||
|
||||
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
|
||||
|
@ -327,7 +312,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
fn get_language_server_command<'a>(
|
||||
self: Arc<Self>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
toolchains: Option<Toolchain>,
|
||||
binary_options: LanguageServerBinaryOptions,
|
||||
mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
|
||||
cx: &'a mut AsyncApp,
|
||||
|
@ -402,7 +387,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
None
|
||||
|
@ -535,7 +520,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
Ok(serde_json::json!({}))
|
||||
|
@ -555,7 +540,6 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
_target_language_server_id: LanguageServerName,
|
||||
_: &dyn Fs,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<Option<Value>> {
|
||||
Ok(None)
|
||||
|
@ -587,17 +571,6 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
Ok(original)
|
||||
}
|
||||
|
||||
/// Determines whether a language server supports workspace folders.
|
||||
///
|
||||
/// And does not trip over itself in the process.
|
||||
fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
|
||||
WorkspaceFoldersContent::SubprojectRoots
|
||||
}
|
||||
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Method only implemented by the default JSON language server adapter.
|
||||
/// Used to provide dynamic reloading of the JSON schemas used to
|
||||
/// provide autocompletion and diagnostics in Zed setting and keybind
|
||||
|
@ -991,11 +964,11 @@ where
|
|||
|
||||
fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
|
||||
let sources = Vec::<String>::deserialize(d)?;
|
||||
let mut regexes = Vec::new();
|
||||
for source in sources {
|
||||
regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
|
||||
}
|
||||
Ok(regexes)
|
||||
sources
|
||||
.into_iter()
|
||||
.map(|source| regex::Regex::new(&source))
|
||||
.collect::<Result<_, _>>()
|
||||
.map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
|
||||
|
@ -1061,12 +1034,10 @@ impl<'de> Deserialize<'de> for BracketPairConfig {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
let result = Vec::<BracketPairContent>::deserialize(deserializer)?;
|
||||
let mut brackets = Vec::with_capacity(result.len());
|
||||
let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len());
|
||||
for entry in result {
|
||||
brackets.push(entry.bracket_pair);
|
||||
disabled_scopes_by_bracket_ix.push(entry.not_in);
|
||||
}
|
||||
let (brackets, disabled_scopes_by_bracket_ix) = result
|
||||
.into_iter()
|
||||
.map(|entry| (entry.bracket_pair, entry.not_in))
|
||||
.unzip();
|
||||
|
||||
Ok(BracketPairConfig {
|
||||
pairs: brackets,
|
||||
|
@ -1108,6 +1079,7 @@ pub struct Language {
|
|||
pub(crate) grammar: Option<Arc<Grammar>>,
|
||||
pub(crate) context_provider: Option<Arc<dyn ContextProvider>>,
|
||||
pub(crate) toolchain: Option<Arc<dyn ToolchainLister>>,
|
||||
pub(crate) manifest_name: Option<ManifestName>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
|
@ -1318,6 +1290,7 @@ impl Language {
|
|||
}),
|
||||
context_provider: None,
|
||||
toolchain: None,
|
||||
manifest_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1331,6 +1304,10 @@ impl Language {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_manifest(mut self, name: Option<ManifestName>) -> Self {
|
||||
self.manifest_name = name;
|
||||
self
|
||||
}
|
||||
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
|
||||
if let Some(query) = queries.highlights {
|
||||
self = self
|
||||
|
@ -1400,16 +1377,14 @@ impl Language {
|
|||
let grammar = self.grammar_mut().context("cannot mutate grammar")?;
|
||||
|
||||
let query = Query::new(&grammar.ts_language, source)?;
|
||||
let mut extra_captures = Vec::with_capacity(query.capture_names().len());
|
||||
|
||||
for name in query.capture_names().iter() {
|
||||
let kind = if *name == "run" {
|
||||
RunnableCapture::Run
|
||||
} else {
|
||||
RunnableCapture::Named(name.to_string().into())
|
||||
};
|
||||
extra_captures.push(kind);
|
||||
}
|
||||
let extra_captures: Vec<_> = query
|
||||
.capture_names()
|
||||
.iter()
|
||||
.map(|&name| match name {
|
||||
"run" => RunnableCapture::Run,
|
||||
name => RunnableCapture::Named(name.to_string().into()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
grammar.runnable_config = Some(RunnableConfig {
|
||||
extra_captures,
|
||||
|
@ -1764,6 +1739,9 @@ impl Language {
|
|||
pub fn name(&self) -> LanguageName {
|
||||
self.config.name.clone()
|
||||
}
|
||||
pub fn manifest(&self) -> Option<&ManifestName> {
|
||||
self.manifest_name.as_ref()
|
||||
}
|
||||
|
||||
pub fn code_fence_block_name(&self) -> Arc<str> {
|
||||
self.config
|
||||
|
@ -2209,7 +2187,7 @@ impl LspAdapter for FakeLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
Some(self.language_server_binary.clone())
|
||||
|
@ -2218,7 +2196,7 @@ impl LspAdapter for FakeLspAdapter {
|
|||
fn get_language_server_command<'a>(
|
||||
self: Arc<Self>,
|
||||
_: Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: LanguageServerBinaryOptions,
|
||||
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
|
||||
_: &'a mut AsyncApp,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
|
||||
LanguageServerName, LspAdapter, PLAIN_TEXT, ToolchainLister,
|
||||
LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister,
|
||||
language_settings::{
|
||||
AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings,
|
||||
},
|
||||
|
@ -172,6 +172,7 @@ pub struct AvailableLanguage {
|
|||
hidden: bool,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
loaded: bool,
|
||||
manifest_name: Option<ManifestName>,
|
||||
}
|
||||
|
||||
impl AvailableLanguage {
|
||||
|
@ -259,6 +260,7 @@ pub struct LoadedLanguage {
|
|||
pub queries: LanguageQueries,
|
||||
pub context_provider: Option<Arc<dyn ContextProvider>>,
|
||||
pub toolchain_provider: Option<Arc<dyn ToolchainLister>>,
|
||||
pub manifest_name: Option<ManifestName>,
|
||||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
|
@ -349,12 +351,14 @@ impl LanguageRegistry {
|
|||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
None,
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: Default::default(),
|
||||
toolchain_provider: None,
|
||||
context_provider: None,
|
||||
manifest_name: None,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -487,6 +491,7 @@ impl LanguageRegistry {
|
|||
grammar_name: Option<Arc<str>>,
|
||||
matcher: LanguageMatcher,
|
||||
hidden: bool,
|
||||
manifest_name: Option<ManifestName>,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
let state = &mut *self.state.write();
|
||||
|
@ -496,6 +501,7 @@ impl LanguageRegistry {
|
|||
existing_language.grammar = grammar_name;
|
||||
existing_language.matcher = matcher;
|
||||
existing_language.load = load;
|
||||
existing_language.manifest_name = manifest_name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -508,6 +514,7 @@ impl LanguageRegistry {
|
|||
load,
|
||||
hidden,
|
||||
loaded: false,
|
||||
manifest_name,
|
||||
});
|
||||
state.version += 1;
|
||||
state.reload_count += 1;
|
||||
|
@ -575,6 +582,7 @@ impl LanguageRegistry {
|
|||
grammar: language.config.grammar.clone(),
|
||||
matcher: language.config.matcher.clone(),
|
||||
hidden: language.config.hidden,
|
||||
manifest_name: None,
|
||||
load: Arc::new(|| Err(anyhow!("already loaded"))),
|
||||
loaded: true,
|
||||
});
|
||||
|
@ -914,10 +922,12 @@ impl LanguageRegistry {
|
|||
Language::new_with_id(id, loaded_language.config, grammar)
|
||||
.with_context_provider(loaded_language.context_provider)
|
||||
.with_toolchain_lister(loaded_language.toolchain_provider)
|
||||
.with_manifest(loaded_language.manifest_name)
|
||||
.with_queries(loaded_language.queries)
|
||||
} else {
|
||||
Ok(Language::new_with_id(id, loaded_language.config, None)
|
||||
.with_context_provider(loaded_language.context_provider)
|
||||
.with_manifest(loaded_language.manifest_name)
|
||||
.with_toolchain_lister(loaded_language.toolchain_provider))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,8 @@ pub struct LanguageSettings {
|
|||
/// Whether to use additional LSP queries to format (and amend) the code after
|
||||
/// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
pub use_on_type_format: bool,
|
||||
/// Whether indentation should be adjusted based on the context whilst typing.
|
||||
pub auto_indent: bool,
|
||||
/// Whether indentation of pasted content should be adjusted based on the context.
|
||||
pub auto_indent_on_paste: bool,
|
||||
/// Controls how the editor handles the autoclosed characters.
|
||||
|
@ -561,6 +563,10 @@ pub struct LanguageSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
pub linked_edits: Option<bool>,
|
||||
/// Whether indentation should be adjusted based on the context whilst typing.
|
||||
///
|
||||
/// Default: true
|
||||
pub auto_indent: Option<bool>,
|
||||
/// Whether indentation of pasted content should be adjusted based on the context.
|
||||
///
|
||||
/// Default: true
|
||||
|
@ -1517,6 +1523,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
|||
merge(&mut settings.use_autoclose, src.use_autoclose);
|
||||
merge(&mut settings.use_auto_surround, src.use_auto_surround);
|
||||
merge(&mut settings.use_on_type_format, src.use_on_type_format);
|
||||
merge(&mut settings.auto_indent, src.auto_indent);
|
||||
merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste);
|
||||
merge(
|
||||
&mut settings.always_treat_brackets_as_autoclosed,
|
||||
|
|
|
@ -12,6 +12,12 @@ impl Borrow<SharedString> for ManifestName {
|
|||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for ManifestName {
|
||||
fn borrow(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for ManifestName {
|
||||
fn from(value: SharedString) -> Self {
|
||||
Self(value)
|
||||
|
|
|
@ -385,12 +385,10 @@ pub fn deserialize_undo_map_entry(
|
|||
|
||||
/// Deserializes selections from the RPC representation.
|
||||
pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
|
||||
Arc::from(
|
||||
selections
|
||||
.into_iter()
|
||||
.filter_map(deserialize_selection)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
selections
|
||||
.into_iter()
|
||||
.filter_map(deserialize_selection)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Deserializes a [`Selection`] from the RPC representation.
|
||||
|
|
|
@ -17,7 +17,7 @@ use settings::WorktreeId;
|
|||
use crate::{LanguageName, ManifestName};
|
||||
|
||||
/// Represents a single toolchain.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Eq)]
|
||||
pub struct Toolchain {
|
||||
/// User-facing label
|
||||
pub name: SharedString,
|
||||
|
@ -27,6 +27,14 @@ pub struct Toolchain {
|
|||
pub as_json: serde_json::Value,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Toolchain {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.path.hash(state);
|
||||
self.language_name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Toolchain {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
|
||||
|
@ -64,6 +72,29 @@ pub trait LanguageToolchainStore: Send + Sync + 'static {
|
|||
) -> Option<Toolchain>;
|
||||
}
|
||||
|
||||
pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
|
||||
fn active_toolchain(
|
||||
self: Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
relative_path: &Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Toolchain>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send )]
|
||||
impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
|
||||
async fn active_toolchain(
|
||||
self: Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
relative_path: Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Toolchain> {
|
||||
self.active_toolchain(worktree_id, &relative_path, language_name, cx)
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultIndex = usize;
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ToolchainList {
|
||||
|
|
|
@ -12,8 +12,8 @@ use fs::Fs;
|
|||
use futures::{Future, FutureExt, future::join_all};
|
||||
use gpui::{App, AppContext, AsyncApp, Task};
|
||||
use language::{
|
||||
BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
|
||||
LspAdapter, LspAdapterDelegate,
|
||||
BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LspAdapter, LspAdapterDelegate,
|
||||
Toolchain,
|
||||
};
|
||||
use lsp::{
|
||||
CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
|
||||
|
@ -159,7 +159,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
|||
fn get_language_server_command<'a>(
|
||||
self: Arc<Self>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: LanguageServerBinaryOptions,
|
||||
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
|
||||
_: &'a mut AsyncApp,
|
||||
|
@ -288,7 +288,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
|
@ -336,7 +336,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
|||
target_language_server_id: LanguageServerName,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
|
|
|
@ -52,7 +52,7 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
|
|||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_language(language, grammar, matcher, hidden, load);
|
||||
.register_language(language, grammar, matcher, hidden, None, load);
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
|
|
|
@ -38,6 +38,27 @@ pub struct AvailableModel {
|
|||
pub max_tokens: u64,
|
||||
pub max_output_tokens: Option<u64>,
|
||||
pub max_completion_tokens: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub capabilities: ModelCapabilities,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ModelCapabilities {
|
||||
pub tools: bool,
|
||||
pub images: bool,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub prompt_cache_key: bool,
|
||||
}
|
||||
|
||||
impl Default for ModelCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tools: true,
|
||||
images: false,
|
||||
parallel_tool_calls: false,
|
||||
prompt_cache_key: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OpenAiCompatibleLanguageModelProvider {
|
||||
|
@ -293,17 +314,17 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
|
|||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
true
|
||||
self.model.capabilities.tools
|
||||
}
|
||||
|
||||
fn supports_images(&self) -> bool {
|
||||
false
|
||||
self.model.capabilities.images
|
||||
}
|
||||
|
||||
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
|
||||
match choice {
|
||||
LanguageModelToolChoice::Auto => true,
|
||||
LanguageModelToolChoice::Any => true,
|
||||
LanguageModelToolChoice::Auto => self.model.capabilities.tools,
|
||||
LanguageModelToolChoice::Any => self.model.capabilities.tools,
|
||||
LanguageModelToolChoice::None => true,
|
||||
}
|
||||
}
|
||||
|
@ -355,13 +376,11 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
|
|||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let supports_parallel_tool_call = true;
|
||||
let supports_prompt_cache_key = false;
|
||||
let request = into_open_ai(
|
||||
request,
|
||||
&self.model.name,
|
||||
supports_parallel_tool_call,
|
||||
supports_prompt_cache_key,
|
||||
self.model.capabilities.parallel_tool_calls,
|
||||
self.model.capabilities.prompt_cache_key,
|
||||
self.max_output_tokens(),
|
||||
None,
|
||||
);
|
||||
|
|
|
@ -28,7 +28,7 @@ impl super::LspAdapter for CLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::{Context as _, Result};
|
|||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use gpui::AsyncApp;
|
||||
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LspAdapter, LspAdapterDelegate, Toolchain};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
|
@ -43,7 +43,7 @@ impl LspAdapter for CssLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate
|
||||
|
@ -144,7 +144,7 @@ impl LspAdapter for CssLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut default_config = json!({
|
||||
|
|
|
@ -75,7 +75,7 @@ impl super::LspAdapter for GoLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
|
|
|
@ -8,8 +8,8 @@ use futures::StreamExt;
|
|||
use gpui::{App, AsyncApp, Task};
|
||||
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
|
||||
use language::{
|
||||
ContextProvider, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile as _,
|
||||
LspAdapter, LspAdapterDelegate,
|
||||
ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
|
||||
LspAdapterDelegate, Toolchain,
|
||||
};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
|
@ -303,7 +303,7 @@ impl LspAdapter for JsonLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate
|
||||
|
@ -404,7 +404,7 @@ impl LspAdapter for JsonLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let mut config = self.get_or_init_workspace_config(cx).await?;
|
||||
|
@ -529,7 +529,7 @@ impl LspAdapter for NodeVersionAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::Context as _;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
|
||||
use gpui::{App, UpdateGlobal};
|
||||
use gpui::{App, SharedString, UpdateGlobal};
|
||||
use node_runtime::NodeRuntime;
|
||||
use python::PyprojectTomlManifestProvider;
|
||||
use rust::CargoManifestProvider;
|
||||
|
@ -177,11 +177,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
|||
adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()],
|
||||
context: Some(python_context_provider),
|
||||
toolchain: Some(python_toolchain_provider),
|
||||
manifest_name: Some(SharedString::new_static("pyproject.toml").into()),
|
||||
},
|
||||
LanguageInfo {
|
||||
name: "rust",
|
||||
adapters: vec![rust_lsp_adapter],
|
||||
context: Some(rust_context_provider),
|
||||
manifest_name: Some(SharedString::new_static("Cargo.toml").into()),
|
||||
..Default::default()
|
||||
},
|
||||
LanguageInfo {
|
||||
|
@ -234,6 +236,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
|||
registration.adapters,
|
||||
registration.context,
|
||||
registration.toolchain,
|
||||
registration.manifest_name,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -340,7 +343,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
|||
Arc::from(PyprojectTomlManifestProvider),
|
||||
];
|
||||
for provider in manifest_providers {
|
||||
project::ManifestProviders::global(cx).register(provider);
|
||||
project::ManifestProvidersStore::global(cx).register(provider);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -350,6 +353,7 @@ struct LanguageInfo {
|
|||
adapters: Vec<Arc<dyn LspAdapter>>,
|
||||
context: Option<Arc<dyn ContextProvider>>,
|
||||
toolchain: Option<Arc<dyn ToolchainLister>>,
|
||||
manifest_name: Option<ManifestName>,
|
||||
}
|
||||
|
||||
fn register_language(
|
||||
|
@ -358,6 +362,7 @@ fn register_language(
|
|||
adapters: Vec<Arc<dyn LspAdapter>>,
|
||||
context: Option<Arc<dyn ContextProvider>>,
|
||||
toolchain: Option<Arc<dyn ToolchainLister>>,
|
||||
manifest_name: Option<ManifestName>,
|
||||
) {
|
||||
let config = load_config(name);
|
||||
for adapter in adapters {
|
||||
|
@ -368,12 +373,14 @@ fn register_language(
|
|||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
config.hidden,
|
||||
manifest_name.clone(),
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: load_queries(name),
|
||||
context_provider: context.clone(),
|
||||
toolchain_provider: toolchain.clone(),
|
||||
manifest_name: manifest_name.clone(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -4,13 +4,13 @@ use async_trait::async_trait;
|
|||
use collections::HashMap;
|
||||
use gpui::{App, Task};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::Toolchain;
|
||||
use language::ToolchainList;
|
||||
use language::ToolchainLister;
|
||||
use language::language_settings::language_settings;
|
||||
use language::{ContextLocation, LanguageToolchainStore};
|
||||
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
|
||||
use language::{Toolchain, WorkspaceFoldersContent};
|
||||
use lsp::LanguageServerBinary;
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
|
@ -127,7 +127,7 @@ impl LspAdapter for PythonLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
|
||||
|
@ -319,17 +319,9 @@ impl LspAdapter for PythonLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
toolchain: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let toolchain = toolchains
|
||||
.active_toolchain(
|
||||
adapter.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.update(move |cx| {
|
||||
let mut user_settings =
|
||||
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
|
||||
|
@ -397,12 +389,6 @@ impl LspAdapter for PythonLspAdapter {
|
|||
user_settings
|
||||
})
|
||||
}
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
|
||||
WorkspaceFoldersContent::WorktreeRoot
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
@ -1046,8 +1032,8 @@ impl LspAdapter for PyLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &AsyncApp,
|
||||
toolchain: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
|
||||
let env = delegate.shell_env().await;
|
||||
|
@ -1057,14 +1043,7 @@ impl LspAdapter for PyLspAdapter {
|
|||
arguments: vec![],
|
||||
})
|
||||
} else {
|
||||
let venv = toolchains
|
||||
.active_toolchain(
|
||||
delegate.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
&mut cx.clone(),
|
||||
)
|
||||
.await?;
|
||||
let venv = toolchain?;
|
||||
let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp");
|
||||
pylsp_path.exists().then(|| LanguageServerBinary {
|
||||
path: venv.path.to_string().into(),
|
||||
|
@ -1211,17 +1190,9 @@ impl LspAdapter for PyLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
toolchain: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let toolchain = toolchains
|
||||
.active_toolchain(
|
||||
adapter.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.update(move |cx| {
|
||||
let mut user_settings =
|
||||
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
|
||||
|
@ -1282,12 +1253,6 @@ impl LspAdapter for PyLspAdapter {
|
|||
user_settings
|
||||
})
|
||||
}
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
|
||||
WorkspaceFoldersContent::WorktreeRoot
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BasedPyrightLspAdapter {
|
||||
|
@ -1377,8 +1342,8 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &AsyncApp,
|
||||
toolchain: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
|
||||
let env = delegate.shell_env().await;
|
||||
|
@ -1388,15 +1353,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
} else {
|
||||
let venv = toolchains
|
||||
.active_toolchain(
|
||||
delegate.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
&mut cx.clone(),
|
||||
)
|
||||
.await?;
|
||||
let path = Path::new(venv.path.as_ref())
|
||||
let path = Path::new(toolchain?.path.as_ref())
|
||||
.parent()?
|
||||
.join(Self::BINARY_NAME);
|
||||
path.exists().then(|| LanguageServerBinary {
|
||||
|
@ -1543,17 +1500,9 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
toolchain: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let toolchain = toolchains
|
||||
.active_toolchain(
|
||||
adapter.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.update(move |cx| {
|
||||
let mut user_settings =
|
||||
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
|
||||
|
@ -1621,14 +1570,6 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
user_settings
|
||||
})
|
||||
}
|
||||
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
|
||||
fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
|
||||
WorkspaceFoldersContent::WorktreeRoot
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -109,14 +109,10 @@ impl LspAdapter for RustLspAdapter {
|
|||
SERVER_NAME.clone()
|
||||
}
|
||||
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("Cargo.toml").into())
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which("rust-analyzer".as_ref()).await?;
|
||||
|
|
|
@ -3,7 +3,7 @@ use async_trait::async_trait;
|
|||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use gpui::AsyncApp;
|
||||
use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
|
@ -50,7 +50,7 @@ impl LspAdapter for TailwindLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
|
@ -155,7 +155,7 @@ impl LspAdapter for TailwindLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let mut tailwind_user_settings = cx.update(|cx| {
|
||||
|
|
|
@ -7,7 +7,7 @@ use gpui::{App, AppContext, AsyncApp, Task};
|
|||
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
|
||||
use language::{
|
||||
ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
LspAdapterDelegate, Toolchain,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
|
@ -722,7 +722,7 @@ impl LspAdapter for TypeScriptLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let override_options = cx.update(|cx| {
|
||||
|
@ -822,7 +822,7 @@ impl LspAdapter for EsLintLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let workspace_root = delegate.worktree_root_path();
|
||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
|||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use gpui::AsyncApp;
|
||||
use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
|
@ -86,7 +86,7 @@ impl LspAdapter for VtslsLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let env = delegate.shell_env().await;
|
||||
|
@ -211,7 +211,7 @@ impl LspAdapter for VtslsLspAdapter {
|
|||
self: Arc<Self>,
|
||||
fs: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let tsdk_path = Self::tsdk_path(fs, delegate).await;
|
||||
|
|
|
@ -2,9 +2,7 @@ use anyhow::{Context as _, Result};
|
|||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use gpui::AsyncApp;
|
||||
use language::{
|
||||
LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings,
|
||||
};
|
||||
use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings};
|
||||
use lsp::{LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
|
@ -57,7 +55,7 @@ impl LspAdapter for YamlLspAdapter {
|
|||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
|
@ -135,7 +133,7 @@ impl LspAdapter for YamlLspAdapter {
|
|||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
_: Option<Toolchain>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let location = SettingsLocation {
|
||||
|
|
|
@ -432,11 +432,16 @@ pub struct ChoiceDelta {
|
|||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct OpenAiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum ResponseStreamResult {
|
||||
Ok(ResponseStreamEvent),
|
||||
Err { error: String },
|
||||
Err { error: OpenAiError },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -475,7 +480,7 @@ pub async fn stream_completion(
|
|||
match serde_json::from_str(line) {
|
||||
Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)),
|
||||
Ok(ResponseStreamResult::Err { error }) => {
|
||||
Some(Err(anyhow!(error)))
|
||||
Some(Err(anyhow!(error.message)))
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
|
@ -502,11 +507,6 @@ pub async fn stream_completion(
|
|||
error: OpenAiError,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OpenAiResponse>(&body) {
|
||||
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||
"API request to {} failed: {}",
|
||||
|
|
|
@ -500,13 +500,12 @@ impl LspCommand for PerformRename {
|
|||
mut cx: AsyncApp,
|
||||
) -> Result<ProjectTransaction> {
|
||||
if let Some(edit) = message {
|
||||
let (lsp_adapter, lsp_server) =
|
||||
let (_, lsp_server) =
|
||||
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
||||
LocalLspStore::deserialize_workspace_edit(
|
||||
lsp_store,
|
||||
edit,
|
||||
self.push_to_history,
|
||||
lsp_adapter,
|
||||
lsp_server,
|
||||
&mut cx,
|
||||
)
|
||||
|
@ -1116,18 +1115,12 @@ pub async fn location_links_from_lsp(
|
|||
}
|
||||
}
|
||||
|
||||
let (lsp_adapter, language_server) =
|
||||
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
||||
let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
||||
let mut definitions = Vec::new();
|
||||
for (origin_range, target_uri, target_range) in unresolved_links {
|
||||
let target_buffer_handle = lsp_store
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_local_buffer_via_lsp(
|
||||
target_uri,
|
||||
language_server.server_id(),
|
||||
lsp_adapter.name.clone(),
|
||||
cx,
|
||||
)
|
||||
this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
|
@ -1172,8 +1165,7 @@ pub async fn location_link_from_lsp(
|
|||
server_id: LanguageServerId,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<LocationLink> {
|
||||
let (lsp_adapter, language_server) =
|
||||
language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?;
|
||||
let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?;
|
||||
|
||||
let (origin_range, target_uri, target_range) = (
|
||||
link.origin_selection_range,
|
||||
|
@ -1183,12 +1175,7 @@ pub async fn location_link_from_lsp(
|
|||
|
||||
let target_buffer_handle = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.open_local_buffer_via_lsp(
|
||||
target_uri,
|
||||
language_server.server_id(),
|
||||
lsp_adapter.name.clone(),
|
||||
cx,
|
||||
)
|
||||
lsp_store.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
|
@ -1326,7 +1313,7 @@ impl LspCommand for GetReferences {
|
|||
mut cx: AsyncApp,
|
||||
) -> Result<Vec<Location>> {
|
||||
let mut references = Vec::new();
|
||||
let (lsp_adapter, language_server) =
|
||||
let (_, language_server) =
|
||||
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
||||
|
||||
if let Some(locations) = locations {
|
||||
|
@ -1336,7 +1323,6 @@ impl LspCommand for GetReferences {
|
|||
lsp_store.open_local_buffer_via_lsp(
|
||||
lsp_location.uri,
|
||||
language_server.server_id(),
|
||||
lsp_adapter.name.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,18 +7,12 @@ mod manifest_store;
|
|||
mod path_trie;
|
||||
mod server_tree;
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeMap, hash_map::Entry},
|
||||
ops::ControlFlow,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, path::Path, sync::Arc};
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Subscription};
|
||||
use language::{ManifestDelegate, ManifestName, ManifestQuery};
|
||||
pub use manifest_store::ManifestProviders;
|
||||
pub use manifest_store::ManifestProvidersStore;
|
||||
use path_trie::{LabelPresence, RootPathTrie, TriePath};
|
||||
use settings::{SettingsStore, WorktreeId};
|
||||
use worktree::{Event as WorktreeEvent, Snapshot, Worktree};
|
||||
|
@ -28,9 +22,7 @@ use crate::{
|
|||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
};
|
||||
|
||||
pub(crate) use server_tree::{
|
||||
AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition,
|
||||
};
|
||||
pub(crate) use server_tree::{LanguageServerTree, LanguageServerTreeNode, LaunchDisposition};
|
||||
|
||||
struct WorktreeRoots {
|
||||
roots: RootPathTrie<ManifestName>,
|
||||
|
@ -81,14 +73,6 @@ pub struct ManifestTree {
|
|||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub(crate) enum ManifestTreeEvent {
|
||||
WorktreeRemoved(WorktreeId),
|
||||
Cleared,
|
||||
}
|
||||
|
||||
impl EventEmitter<ManifestTreeEvent> for ManifestTree {}
|
||||
|
||||
impl ManifestTree {
|
||||
pub fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
|
@ -101,30 +85,28 @@ impl ManifestTree {
|
|||
worktree_roots.roots = RootPathTrie::new();
|
||||
})
|
||||
}
|
||||
cx.emit(ManifestTreeEvent::Cleared);
|
||||
}),
|
||||
],
|
||||
worktree_store,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn root_for_path(
|
||||
&mut self,
|
||||
ProjectPath { worktree_id, path }: ProjectPath,
|
||||
manifests: &mut dyn Iterator<Item = ManifestName>,
|
||||
delegate: Arc<dyn ManifestDelegate>,
|
||||
ProjectPath { worktree_id, path }: &ProjectPath,
|
||||
manifest_name: &ManifestName,
|
||||
delegate: &Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
) -> BTreeMap<ManifestName, ProjectPath> {
|
||||
debug_assert_eq!(delegate.worktree_id(), worktree_id);
|
||||
let mut roots = BTreeMap::from_iter(
|
||||
manifests.map(|manifest| (manifest, (None, LabelPresence::KnownAbsent))),
|
||||
);
|
||||
let worktree_roots = match self.root_points.entry(worktree_id) {
|
||||
) -> Option<ProjectPath> {
|
||||
debug_assert_eq!(delegate.worktree_id(), *worktree_id);
|
||||
let (mut marked_path, mut current_presence) = (None, LabelPresence::KnownAbsent);
|
||||
let worktree_roots = match self.root_points.entry(*worktree_id) {
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
let Some(worktree) = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.worktree_for_id(*worktree_id, cx)
|
||||
else {
|
||||
return Default::default();
|
||||
};
|
||||
|
@ -133,16 +115,16 @@ impl ManifestTree {
|
|||
}
|
||||
};
|
||||
|
||||
let key = TriePath::from(&*path);
|
||||
let key = TriePath::from(&**path);
|
||||
worktree_roots.read_with(cx, |this, _| {
|
||||
this.roots.walk(&key, &mut |path, labels| {
|
||||
for (label, presence) in labels {
|
||||
if let Some((marked_path, current_presence)) = roots.get_mut(label) {
|
||||
if *current_presence > *presence {
|
||||
if label == manifest_name {
|
||||
if current_presence > *presence {
|
||||
debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase");
|
||||
}
|
||||
*marked_path = Some(ProjectPath {worktree_id, path: path.clone()});
|
||||
*current_presence = *presence;
|
||||
marked_path = Some(ProjectPath {worktree_id: *worktree_id, path: path.clone()});
|
||||
current_presence = *presence;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -150,12 +132,9 @@ impl ManifestTree {
|
|||
});
|
||||
});
|
||||
|
||||
for (manifest_name, (root_path, presence)) in &mut roots {
|
||||
if *presence == LabelPresence::Present {
|
||||
continue;
|
||||
}
|
||||
|
||||
let depth = root_path
|
||||
if current_presence == LabelPresence::KnownAbsent {
|
||||
// Some part of the path is unexplored.
|
||||
let depth = marked_path
|
||||
.as_ref()
|
||||
.map(|root_path| {
|
||||
path.strip_prefix(&root_path.path)
|
||||
|
@ -165,13 +144,10 @@ impl ManifestTree {
|
|||
})
|
||||
.unwrap_or_else(|| path.components().count() + 1);
|
||||
|
||||
if depth > 0 {
|
||||
let Some(provider) = ManifestProviders::global(cx).get(manifest_name.borrow())
|
||||
else {
|
||||
log::warn!("Manifest provider `{}` not found", manifest_name.as_ref());
|
||||
continue;
|
||||
};
|
||||
|
||||
if depth > 0
|
||||
&& let Some(provider) =
|
||||
ManifestProvidersStore::global(cx).get(manifest_name.borrow())
|
||||
{
|
||||
let root = provider.search(ManifestQuery {
|
||||
path: path.clone(),
|
||||
depth,
|
||||
|
@ -182,9 +158,9 @@ impl ManifestTree {
|
|||
let root = TriePath::from(&*known_root);
|
||||
this.roots
|
||||
.insert(&root, manifest_name.clone(), LabelPresence::Present);
|
||||
*presence = LabelPresence::Present;
|
||||
*root_path = Some(ProjectPath {
|
||||
worktree_id,
|
||||
current_presence = LabelPresence::Present;
|
||||
marked_path = Some(ProjectPath {
|
||||
worktree_id: *worktree_id,
|
||||
path: known_root,
|
||||
});
|
||||
}),
|
||||
|
@ -195,25 +171,35 @@ impl ManifestTree {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
roots
|
||||
.into_iter()
|
||||
.filter_map(|(k, (path, presence))| {
|
||||
let path = path?;
|
||||
presence.eq(&LabelPresence::Present).then(|| (k, path))
|
||||
})
|
||||
.collect()
|
||||
marked_path.filter(|_| current_presence.eq(&LabelPresence::Present))
|
||||
}
|
||||
|
||||
pub(crate) fn root_for_path_or_worktree_root(
|
||||
&mut self,
|
||||
project_path: &ProjectPath,
|
||||
manifest_name: Option<&ManifestName>,
|
||||
delegate: &Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
) -> ProjectPath {
|
||||
let worktree_id = project_path.worktree_id;
|
||||
// Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree.
|
||||
manifest_name
|
||||
.and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx))
|
||||
.unwrap_or_else(|| ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_worktree_store_event(
|
||||
&mut self,
|
||||
_: Entity<WorktreeStore>,
|
||||
evt: &WorktreeStoreEvent,
|
||||
cx: &mut Context<Self>,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
match evt {
|
||||
WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
|
||||
self.root_points.remove(&worktree_id);
|
||||
cx.emit(ManifestTreeEvent::WorktreeRemoved(*worktree_id));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -223,6 +209,7 @@ impl ManifestTree {
|
|||
pub(crate) struct ManifestQueryDelegate {
|
||||
worktree: Snapshot,
|
||||
}
|
||||
|
||||
impl ManifestQueryDelegate {
|
||||
pub fn new(worktree: Snapshot) -> Self {
|
||||
Self { worktree }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{App, Global, SharedString};
|
||||
use parking_lot::RwLock;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
@ -11,13 +11,13 @@ struct ManifestProvidersState {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ManifestProviders(Arc<RwLock<ManifestProvidersState>>);
|
||||
pub struct ManifestProvidersStore(Arc<RwLock<ManifestProvidersState>>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalManifestProvider(ManifestProviders);
|
||||
struct GlobalManifestProvider(ManifestProvidersStore);
|
||||
|
||||
impl Deref for GlobalManifestProvider {
|
||||
type Target = ManifestProviders;
|
||||
type Target = ManifestProvidersStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
|
@ -26,7 +26,7 @@ impl Deref for GlobalManifestProvider {
|
|||
|
||||
impl Global for GlobalManifestProvider {}
|
||||
|
||||
impl ManifestProviders {
|
||||
impl ManifestProvidersStore {
|
||||
/// Returns the global [`ManifestStore`].
|
||||
///
|
||||
/// Inserts a default [`ManifestStore`] if one does not yet exist.
|
||||
|
@ -45,4 +45,7 @@ impl ManifestProviders {
|
|||
pub(super) fn get(&self, name: &SharedString) -> Option<Arc<dyn ManifestProvider>> {
|
||||
self.0.read().providers.get(name).cloned()
|
||||
}
|
||||
pub(crate) fn manifest_file_names(&self) -> HashSet<ManifestName> {
|
||||
self.0.read().providers.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
//!
|
||||
//! ## RPC
|
||||
//! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide
|
||||
//! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to
|
||||
//! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally.
|
||||
//! to reuse existing language server.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
|
@ -14,20 +13,23 @@ use std::{
|
|||
};
|
||||
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, AppContext as _, Entity, Subscription};
|
||||
use gpui::{App, Entity};
|
||||
use language::{
|
||||
CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
|
||||
CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, ManifestName, Toolchain,
|
||||
language_settings::AllLanguageSettings,
|
||||
};
|
||||
use lsp::LanguageServerName;
|
||||
use settings::{Settings, SettingsLocation, WorktreeId};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::{LanguageServerId, ProjectPath, project_settings::LspSettings};
|
||||
use crate::{
|
||||
LanguageServerId, ProjectPath, project_settings::LspSettings,
|
||||
toolchain_store::LocalToolchainStore,
|
||||
};
|
||||
|
||||
use super::{ManifestTree, ManifestTreeEvent};
|
||||
use super::ManifestTree;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ServersForWorktree {
|
||||
pub(crate) roots: BTreeMap<
|
||||
Arc<Path>,
|
||||
|
@ -39,7 +41,7 @@ pub struct LanguageServerTree {
|
|||
manifest_tree: Entity<ManifestTree>,
|
||||
pub(crate) instances: BTreeMap<WorktreeId, ServersForWorktree>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_subscriptions: Subscription,
|
||||
toolchains: Entity<LocalToolchainStore>,
|
||||
}
|
||||
|
||||
/// A node in language server tree represents either:
|
||||
|
@ -49,22 +51,15 @@ pub struct LanguageServerTree {
|
|||
pub struct LanguageServerTreeNode(Weak<InnerTreeNode>);
|
||||
|
||||
/// Describes a request to launch a language server.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LaunchDisposition<'a> {
|
||||
pub(crate) server_name: &'a LanguageServerName,
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct LaunchDisposition {
|
||||
pub(crate) server_name: LanguageServerName,
|
||||
/// Path to the root directory of a subproject.
|
||||
pub(crate) path: ProjectPath,
|
||||
pub(crate) settings: Arc<LspSettings>,
|
||||
pub(crate) toolchain: Option<Toolchain>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> {
|
||||
fn from(value: &'a InnerTreeNode) -> Self {
|
||||
LaunchDisposition {
|
||||
server_name: &value.name,
|
||||
path: value.path.clone(),
|
||||
settings: value.settings.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl LanguageServerTreeNode {
|
||||
/// Returns a language server ID for this node if there is one.
|
||||
/// Returns None if this node has not been initialized yet or it is no longer in the tree.
|
||||
|
@ -76,19 +71,17 @@ impl LanguageServerTreeNode {
|
|||
/// May return None if the node no longer belongs to the server tree it was created in.
|
||||
pub(crate) fn server_id_or_init(
|
||||
&self,
|
||||
init: impl FnOnce(LaunchDisposition) -> LanguageServerId,
|
||||
init: impl FnOnce(&Arc<LaunchDisposition>) -> LanguageServerId,
|
||||
) -> Option<LanguageServerId> {
|
||||
let this = self.0.upgrade()?;
|
||||
Some(
|
||||
*this
|
||||
.id
|
||||
.get_or_init(|| init(LaunchDisposition::from(&*this))),
|
||||
)
|
||||
Some(*this.id.get_or_init(|| init(&this.disposition)))
|
||||
}
|
||||
|
||||
/// Returns a language server name as the language server adapter would return.
|
||||
pub fn name(&self) -> Option<LanguageServerName> {
|
||||
self.0.upgrade().map(|node| node.name.clone())
|
||||
self.0
|
||||
.upgrade()
|
||||
.map(|node| node.disposition.server_name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,160 +94,149 @@ impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {
|
|||
#[derive(Debug)]
|
||||
pub struct InnerTreeNode {
|
||||
id: OnceLock<LanguageServerId>,
|
||||
name: LanguageServerName,
|
||||
path: ProjectPath,
|
||||
settings: Arc<LspSettings>,
|
||||
disposition: Arc<LaunchDisposition>,
|
||||
}
|
||||
|
||||
impl InnerTreeNode {
|
||||
fn new(
|
||||
name: LanguageServerName,
|
||||
server_name: LanguageServerName,
|
||||
path: ProjectPath,
|
||||
settings: impl Into<Arc<LspSettings>>,
|
||||
settings: LspSettings,
|
||||
toolchain: Option<Toolchain>,
|
||||
) -> Self {
|
||||
InnerTreeNode {
|
||||
id: Default::default(),
|
||||
name,
|
||||
path,
|
||||
settings: settings.into(),
|
||||
disposition: Arc::new(LaunchDisposition {
|
||||
server_name,
|
||||
path,
|
||||
settings: settings.into(),
|
||||
toolchain,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines how the list of adapters to query should be constructed.
|
||||
pub(crate) enum AdapterQuery<'a> {
|
||||
/// Search for roots of all adapters associated with a given language name.
|
||||
/// Layman: Look for all project roots along the queried path that have any
|
||||
/// language server associated with this language running.
|
||||
Language(&'a LanguageName),
|
||||
/// Search for roots of adapter with a given name.
|
||||
/// Layman: Look for all project roots along the queried path that have this server running.
|
||||
Adapter(&'a LanguageServerName),
|
||||
}
|
||||
|
||||
impl LanguageServerTree {
|
||||
pub(crate) fn new(
|
||||
manifest_tree: Entity<ManifestTree>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
_subscriptions: cx.subscribe(&manifest_tree, |_: &mut Self, _, event, _| {
|
||||
if event == &ManifestTreeEvent::Cleared {}
|
||||
}),
|
||||
toolchains: Entity<LocalToolchainStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
manifest_tree,
|
||||
instances: Default::default(),
|
||||
|
||||
languages,
|
||||
})
|
||||
toolchains,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all initialized language server IDs for a given path.
|
||||
pub(crate) fn get<'a>(
|
||||
&'a self,
|
||||
path: ProjectPath,
|
||||
language_name: LanguageName,
|
||||
manifest_name: Option<&ManifestName>,
|
||||
delegate: &Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
) -> impl Iterator<Item = LanguageServerId> + 'a {
|
||||
let manifest_location = self.manifest_location_for_path(&path, manifest_name, delegate, cx);
|
||||
let adapters = self.adapters_for_language(&manifest_location, &language_name, cx);
|
||||
self.get_with_adapters(manifest_location, adapters)
|
||||
}
|
||||
|
||||
/// Get all language server root points for a given path and language; the language servers might already be initialized at a given path.
|
||||
pub(crate) fn get<'a>(
|
||||
pub(crate) fn walk<'a>(
|
||||
&'a mut self,
|
||||
path: ProjectPath,
|
||||
query: AdapterQuery<'_>,
|
||||
delegate: Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
language_name: LanguageName,
|
||||
manifest_name: Option<&ManifestName>,
|
||||
delegate: &Arc<dyn ManifestDelegate>,
|
||||
cx: &'a mut App,
|
||||
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
|
||||
let settings_location = SettingsLocation {
|
||||
worktree_id: path.worktree_id,
|
||||
path: &path.path,
|
||||
};
|
||||
let adapters = match query {
|
||||
AdapterQuery::Language(language_name) => {
|
||||
self.adapters_for_language(settings_location, language_name, cx)
|
||||
}
|
||||
AdapterQuery::Adapter(language_server_name) => {
|
||||
IndexMap::from_iter(self.adapter_for_name(language_server_name).map(|adapter| {
|
||||
(
|
||||
let manifest_location = self.manifest_location_for_path(&path, manifest_name, delegate, cx);
|
||||
let adapters = self.adapters_for_language(&manifest_location, &language_name, cx);
|
||||
self.init_with_adapters(manifest_location, language_name, adapters, cx)
|
||||
}
|
||||
|
||||
fn init_with_adapters<'a>(
|
||||
&'a mut self,
|
||||
root_path: ProjectPath,
|
||||
language_name: LanguageName,
|
||||
adapters: IndexMap<LanguageServerName, (LspSettings, Arc<CachedLspAdapter>)>,
|
||||
cx: &'a App,
|
||||
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
|
||||
adapters.into_iter().map(move |(_, (settings, adapter))| {
|
||||
let root_path = root_path.clone();
|
||||
let inner_node = self
|
||||
.instances
|
||||
.entry(root_path.worktree_id)
|
||||
.or_default()
|
||||
.roots
|
||||
.entry(root_path.path.clone())
|
||||
.or_default()
|
||||
.entry(adapter.name());
|
||||
let (node, languages) = inner_node.or_insert_with(|| {
|
||||
let toolchain = self.toolchains.read(cx).active_toolchain(
|
||||
root_path.worktree_id,
|
||||
&root_path.path,
|
||||
language_name.clone(),
|
||||
);
|
||||
(
|
||||
Arc::new(InnerTreeNode::new(
|
||||
adapter.name(),
|
||||
(LspSettings::default(), BTreeSet::new(), adapter),
|
||||
)
|
||||
}))
|
||||
}
|
||||
};
|
||||
self.get_with_adapters(path, adapters, delegate, cx)
|
||||
root_path.clone(),
|
||||
settings.clone(),
|
||||
toolchain,
|
||||
)),
|
||||
Default::default(),
|
||||
)
|
||||
});
|
||||
languages.insert(language_name.clone());
|
||||
Arc::downgrade(&node).into()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_with_adapters<'a>(
|
||||
&'a mut self,
|
||||
path: ProjectPath,
|
||||
adapters: IndexMap<
|
||||
LanguageServerName,
|
||||
(LspSettings, BTreeSet<LanguageName>, Arc<CachedLspAdapter>),
|
||||
>,
|
||||
delegate: Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
|
||||
let worktree_id = path.worktree_id;
|
||||
|
||||
let mut manifest_to_adapters = BTreeMap::default();
|
||||
for (_, _, adapter) in adapters.values() {
|
||||
if let Some(manifest_name) = adapter.manifest_name() {
|
||||
manifest_to_adapters
|
||||
.entry(manifest_name)
|
||||
.or_insert_with(Vec::default)
|
||||
.push(adapter.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let roots = self.manifest_tree.update(cx, |this, cx| {
|
||||
this.root_for_path(
|
||||
path,
|
||||
&mut manifest_to_adapters.keys().cloned(),
|
||||
delegate,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let root_path = std::cell::LazyCell::new(move || ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from("".as_ref()),
|
||||
});
|
||||
adapters
|
||||
.into_iter()
|
||||
.map(move |(_, (settings, new_languages, adapter))| {
|
||||
// Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree.
|
||||
let root_path = adapter
|
||||
.manifest_name()
|
||||
.and_then(|name| roots.get(&name))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| root_path.clone());
|
||||
|
||||
let inner_node = self
|
||||
.instances
|
||||
.entry(root_path.worktree_id)
|
||||
.or_default()
|
||||
.roots
|
||||
.entry(root_path.path.clone())
|
||||
.or_default()
|
||||
.entry(adapter.name());
|
||||
let (node, languages) = inner_node.or_insert_with(|| {
|
||||
(
|
||||
Arc::new(InnerTreeNode::new(
|
||||
adapter.name(),
|
||||
root_path.clone(),
|
||||
settings.clone(),
|
||||
)),
|
||||
Default::default(),
|
||||
)
|
||||
});
|
||||
languages.extend(new_languages.iter().cloned());
|
||||
Arc::downgrade(&node).into()
|
||||
})
|
||||
&'a self,
|
||||
root_path: ProjectPath,
|
||||
adapters: IndexMap<LanguageServerName, (LspSettings, Arc<CachedLspAdapter>)>,
|
||||
) -> impl Iterator<Item = LanguageServerId> + 'a {
|
||||
adapters.into_iter().filter_map(move |(_, (_, adapter))| {
|
||||
let root_path = root_path.clone();
|
||||
let inner_node = self
|
||||
.instances
|
||||
.get(&root_path.worktree_id)?
|
||||
.roots
|
||||
.get(&root_path.path)?
|
||||
.get(&adapter.name())?;
|
||||
inner_node.0.id.get().copied()
|
||||
})
|
||||
}
|
||||
|
||||
fn adapter_for_name(&self, name: &LanguageServerName) -> Option<Arc<CachedLspAdapter>> {
|
||||
self.languages.adapter_for_name(name)
|
||||
fn manifest_location_for_path(
|
||||
&self,
|
||||
path: &ProjectPath,
|
||||
manifest_name: Option<&ManifestName>,
|
||||
delegate: &Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
) -> ProjectPath {
|
||||
// Find out what the root location of our subproject is.
|
||||
// That's where we'll look for language settings (that include a set of language servers).
|
||||
self.manifest_tree.update(cx, |this, cx| {
|
||||
this.root_for_path_or_worktree_root(path, manifest_name, delegate, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn adapters_for_language(
|
||||
&self,
|
||||
settings_location: SettingsLocation,
|
||||
manifest_location: &ProjectPath,
|
||||
language_name: &LanguageName,
|
||||
cx: &App,
|
||||
) -> IndexMap<LanguageServerName, (LspSettings, BTreeSet<LanguageName>, Arc<CachedLspAdapter>)>
|
||||
{
|
||||
) -> IndexMap<LanguageServerName, (LspSettings, Arc<CachedLspAdapter>)> {
|
||||
let settings_location = SettingsLocation {
|
||||
worktree_id: manifest_location.worktree_id,
|
||||
path: &manifest_location.path,
|
||||
};
|
||||
let settings = AllLanguageSettings::get(Some(settings_location), cx).language(
|
||||
Some(settings_location),
|
||||
Some(language_name),
|
||||
|
@ -295,14 +277,7 @@ impl LanguageServerTree {
|
|||
)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Some((
|
||||
adapter.name(),
|
||||
(
|
||||
adapter_settings,
|
||||
BTreeSet::from_iter([language_name.clone()]),
|
||||
adapter,
|
||||
),
|
||||
))
|
||||
Some((adapter.name(), (adapter_settings, adapter)))
|
||||
})
|
||||
.collect::<IndexMap<_, _>>();
|
||||
// After starting all the language servers, reorder them to reflect the desired order
|
||||
|
@ -315,17 +290,23 @@ impl LanguageServerTree {
|
|||
&language_name,
|
||||
adapters_with_settings
|
||||
.values()
|
||||
.map(|(_, _, adapter)| adapter.clone())
|
||||
.map(|(_, adapter)| adapter.clone())
|
||||
.collect(),
|
||||
);
|
||||
|
||||
adapters_with_settings
|
||||
}
|
||||
|
||||
// Rebasing a tree:
|
||||
// - Clears it out
|
||||
// - Provides you with the indirect access to the old tree while you're reinitializing a new one (by querying it).
|
||||
pub(crate) fn rebase(&mut self) -> ServerTreeRebase<'_> {
|
||||
/// Server Tree is built up incrementally via queries for distinct paths of the worktree.
|
||||
/// Results of these queries have to be invalidated when data used to build the tree changes.
|
||||
///
|
||||
/// The environment of a server tree is a set of all user settings.
|
||||
/// Rebasing a tree means invalidating it and building up a new one while reusing the old tree where applicable.
|
||||
/// We want to reuse the old tree in order to preserve as many of the running language servers as possible.
|
||||
/// E.g. if the user disables one of their language servers for Python, we don't want to shut down any language servers unaffected by this settings change.
|
||||
///
|
||||
/// Thus, [`ServerTreeRebase`] mimics the interface of a [`ServerTree`], except that it tries to find a matching language server in the old tree before handing out an uninitialized node.
|
||||
pub(crate) fn rebase(&mut self) -> ServerTreeRebase {
|
||||
ServerTreeRebase::new(self)
|
||||
}
|
||||
|
||||
|
@ -354,16 +335,16 @@ impl LanguageServerTree {
|
|||
.roots
|
||||
.entry(Arc::from(Path::new("")))
|
||||
.or_default()
|
||||
.entry(node.name.clone())
|
||||
.entry(node.disposition.server_name.clone())
|
||||
.or_insert_with(|| (node, BTreeSet::new()))
|
||||
.1
|
||||
.insert(language_name);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ServerTreeRebase<'a> {
|
||||
pub(crate) struct ServerTreeRebase {
|
||||
old_contents: BTreeMap<WorktreeId, ServersForWorktree>,
|
||||
new_tree: &'a mut LanguageServerTree,
|
||||
new_tree: LanguageServerTree,
|
||||
/// All server IDs seen in the old tree.
|
||||
all_server_ids: BTreeMap<LanguageServerId, LanguageServerName>,
|
||||
/// Server IDs we've preserved for a new iteration of the tree. `all_server_ids - rebased_server_ids` is the
|
||||
|
@ -371,9 +352,9 @@ pub(crate) struct ServerTreeRebase<'a> {
|
|||
rebased_server_ids: BTreeSet<LanguageServerId>,
|
||||
}
|
||||
|
||||
impl<'tree> ServerTreeRebase<'tree> {
|
||||
fn new(new_tree: &'tree mut LanguageServerTree) -> Self {
|
||||
let old_contents = std::mem::take(&mut new_tree.instances);
|
||||
impl ServerTreeRebase {
|
||||
fn new(old_tree: &LanguageServerTree) -> Self {
|
||||
let old_contents = old_tree.instances.clone();
|
||||
let all_server_ids = old_contents
|
||||
.values()
|
||||
.flat_map(|nodes| {
|
||||
|
@ -384,69 +365,68 @@ impl<'tree> ServerTreeRebase<'tree> {
|
|||
.id
|
||||
.get()
|
||||
.copied()
|
||||
.map(|id| (id, server.0.name.clone()))
|
||||
.map(|id| (id, server.0.disposition.server_name.clone()))
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let new_tree = LanguageServerTree::new(
|
||||
old_tree.manifest_tree.clone(),
|
||||
old_tree.languages.clone(),
|
||||
old_tree.toolchains.clone(),
|
||||
);
|
||||
Self {
|
||||
old_contents,
|
||||
new_tree,
|
||||
all_server_ids,
|
||||
new_tree,
|
||||
rebased_server_ids: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get<'a>(
|
||||
pub(crate) fn walk<'a>(
|
||||
&'a mut self,
|
||||
path: ProjectPath,
|
||||
query: AdapterQuery<'_>,
|
||||
language_name: LanguageName,
|
||||
manifest_name: Option<&ManifestName>,
|
||||
delegate: Arc<dyn ManifestDelegate>,
|
||||
cx: &mut App,
|
||||
cx: &'a mut App,
|
||||
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
|
||||
let settings_location = SettingsLocation {
|
||||
worktree_id: path.worktree_id,
|
||||
path: &path.path,
|
||||
};
|
||||
let adapters = match query {
|
||||
AdapterQuery::Language(language_name) => {
|
||||
self.new_tree
|
||||
.adapters_for_language(settings_location, language_name, cx)
|
||||
}
|
||||
AdapterQuery::Adapter(language_server_name) => {
|
||||
IndexMap::from_iter(self.new_tree.adapter_for_name(language_server_name).map(
|
||||
|adapter| {
|
||||
(
|
||||
adapter.name(),
|
||||
(LspSettings::default(), BTreeSet::new(), adapter),
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
};
|
||||
let manifest =
|
||||
self.new_tree
|
||||
.manifest_location_for_path(&path, manifest_name, &delegate, cx);
|
||||
let adapters = self
|
||||
.new_tree
|
||||
.adapters_for_language(&manifest, &language_name, cx);
|
||||
|
||||
self.new_tree
|
||||
.get_with_adapters(path, adapters, delegate, cx)
|
||||
.init_with_adapters(manifest, language_name, adapters, cx)
|
||||
.filter_map(|node| {
|
||||
// Inspect result of the query and initialize it ourselves before
|
||||
// handing it off to the caller.
|
||||
let disposition = node.0.upgrade()?;
|
||||
let live_node = node.0.upgrade()?;
|
||||
|
||||
if disposition.id.get().is_some() {
|
||||
if live_node.id.get().is_some() {
|
||||
return Some(node);
|
||||
}
|
||||
let disposition = &live_node.disposition;
|
||||
let Some((existing_node, _)) = self
|
||||
.old_contents
|
||||
.get(&disposition.path.worktree_id)
|
||||
.and_then(|worktree_nodes| worktree_nodes.roots.get(&disposition.path.path))
|
||||
.and_then(|roots| roots.get(&disposition.name))
|
||||
.filter(|(old_node, _)| disposition.settings == old_node.settings)
|
||||
.and_then(|roots| roots.get(&disposition.server_name))
|
||||
.filter(|(old_node, _)| {
|
||||
(&disposition.toolchain, &disposition.settings)
|
||||
== (
|
||||
&old_node.disposition.toolchain,
|
||||
&old_node.disposition.settings,
|
||||
)
|
||||
})
|
||||
else {
|
||||
return Some(node);
|
||||
};
|
||||
if let Some(existing_id) = existing_node.id.get() {
|
||||
self.rebased_server_ids.insert(*existing_id);
|
||||
disposition.id.set(*existing_id).ok();
|
||||
live_node.id.set(*existing_id).ok();
|
||||
}
|
||||
|
||||
Some(node)
|
||||
|
@ -454,11 +434,19 @@ impl<'tree> ServerTreeRebase<'tree> {
|
|||
}
|
||||
|
||||
/// Returns IDs of servers that are no longer referenced (and can be shut down).
|
||||
pub(crate) fn finish(self) -> BTreeMap<LanguageServerId, LanguageServerName> {
|
||||
self.all_server_ids
|
||||
.into_iter()
|
||||
.filter(|(id, _)| !self.rebased_server_ids.contains(id))
|
||||
.collect()
|
||||
pub(crate) fn finish(
|
||||
self,
|
||||
) -> (
|
||||
LanguageServerTree,
|
||||
BTreeMap<LanguageServerId, LanguageServerName>,
|
||||
) {
|
||||
(
|
||||
self.new_tree,
|
||||
self.all_server_ids
|
||||
.into_iter()
|
||||
.filter(|(id, _)| !self.rebased_server_ids.contains(id))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn server_tree(&mut self) -> &mut LanguageServerTree {
|
||||
|
|
|
@ -84,7 +84,7 @@ use lsp::{
|
|||
};
|
||||
use lsp_command::*;
|
||||
use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
|
||||
pub use manifest_tree::ManifestProviders;
|
||||
pub use manifest_tree::ManifestProvidersStore;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
pub use prettier_store::PrettierStore;
|
||||
|
@ -1115,7 +1115,11 @@ impl Project {
|
|||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
prettier_store.clone(),
|
||||
toolchain_store.clone(),
|
||||
toolchain_store
|
||||
.read(cx)
|
||||
.as_local_store()
|
||||
.expect("Toolchain store to be local")
|
||||
.clone(),
|
||||
environment.clone(),
|
||||
manifest_tree,
|
||||
languages.clone(),
|
||||
|
@ -1260,7 +1264,6 @@ impl Project {
|
|||
LspStore::new_remote(
|
||||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
Some(toolchain_store.clone()),
|
||||
languages.clone(),
|
||||
ssh_proto.clone(),
|
||||
SSH_PROJECT_ID,
|
||||
|
@ -1485,7 +1488,6 @@ impl Project {
|
|||
let mut lsp_store = LspStore::new_remote(
|
||||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
None,
|
||||
languages.clone(),
|
||||
client.clone().into(),
|
||||
remote_id,
|
||||
|
@ -3596,16 +3598,10 @@ impl Project {
|
|||
&mut self,
|
||||
abs_path: lsp::Url,
|
||||
language_server_id: LanguageServerId,
|
||||
language_server_name: LanguageServerName,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Buffer>>> {
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.open_local_buffer_via_lsp(
|
||||
abs_path,
|
||||
language_server_id,
|
||||
language_server_name,
|
||||
cx,
|
||||
)
|
||||
lsp_store.open_local_buffer_via_lsp(abs_path, language_server_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ use settings::{
|
|||
SettingsStore, parse_json_with_comments, watch_config_file,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
|
@ -518,16 +519,15 @@ impl Default for InlineBlameSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
|
||||
pub struct BinarySettings {
|
||||
pub path: Option<String>,
|
||||
pub arguments: Option<Vec<String>>,
|
||||
// this can't be an FxHashMap because the extension APIs require the default SipHash
|
||||
pub env: Option<std::collections::HashMap<String, String>>,
|
||||
pub env: Option<BTreeMap<String, String>>,
|
||||
pub ignore_system_version: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct LspSettings {
|
||||
pub binary: Option<BinarySettings>,
|
||||
|
|
|
@ -1099,9 +1099,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
let prev_read_dir_count = fs.read_dir_call_count();
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| {
|
||||
let (id, status) = lsp_store.language_server_statuses().next().unwrap();
|
||||
(id, status.name.clone())
|
||||
let server_id = lsp_store.read_with(cx, |lsp_store, _| {
|
||||
let (id, _) = lsp_store.language_server_statuses().next().unwrap();
|
||||
id
|
||||
});
|
||||
|
||||
// Simulate jumping to a definition in a dependency outside of the worktree.
|
||||
|
@ -1110,7 +1110,6 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
project.open_local_buffer_via_lsp(
|
||||
lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(),
|
||||
server_id,
|
||||
server_name.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
|
|
@ -11,7 +11,10 @@ use collections::BTreeMap;
|
|||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList};
|
||||
use language::{
|
||||
LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain,
|
||||
ToolchainList,
|
||||
};
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
proto::{self, FromProto, ToProto},
|
||||
|
@ -104,9 +107,11 @@ impl ToolchainStore {
|
|||
cx: &App,
|
||||
) -> Task<Option<Toolchain>> {
|
||||
match &self.0 {
|
||||
ToolchainStoreInner::Local(local, _) => {
|
||||
local.read(cx).active_toolchain(path, language_name, cx)
|
||||
}
|
||||
ToolchainStoreInner::Local(local, _) => Task::ready(local.read(cx).active_toolchain(
|
||||
path.worktree_id,
|
||||
&path.path,
|
||||
language_name,
|
||||
)),
|
||||
ToolchainStoreInner::Remote(remote) => {
|
||||
remote.read(cx).active_toolchain(path, language_name, cx)
|
||||
}
|
||||
|
@ -232,9 +237,15 @@ impl ToolchainStore {
|
|||
ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
|
||||
}
|
||||
}
|
||||
pub fn as_local_store(&self) -> Option<&Entity<LocalToolchainStore>> {
|
||||
match &self.0 {
|
||||
ToolchainStoreInner::Local(local, _) => Some(local),
|
||||
ToolchainStoreInner::Remote(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalToolchainStore {
|
||||
pub struct LocalToolchainStore {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
project_environment: Entity<ProjectEnvironment>,
|
||||
|
@ -243,20 +254,19 @@ struct LocalToolchainStore {
|
|||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl language::LanguageToolchainStore for LocalStore {
|
||||
async fn active_toolchain(
|
||||
impl language::LocalLanguageToolchainStore for LocalStore {
|
||||
fn active_toolchain(
|
||||
self: Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
path: Arc<Path>,
|
||||
path: &Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Toolchain> {
|
||||
self.0
|
||||
.update(cx, |this, cx| {
|
||||
this.active_toolchain(ProjectPath { worktree_id, path }, language_name, cx)
|
||||
.update(cx, |this, _| {
|
||||
this.active_toolchain(worktree_id, path, language_name)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,19 +289,18 @@ impl language::LanguageToolchainStore for RemoteStore {
|
|||
}
|
||||
|
||||
pub struct EmptyToolchainStore;
|
||||
#[async_trait(?Send)]
|
||||
impl language::LanguageToolchainStore for EmptyToolchainStore {
|
||||
async fn active_toolchain(
|
||||
impl language::LocalLanguageToolchainStore for EmptyToolchainStore {
|
||||
fn active_toolchain(
|
||||
self: Arc<Self>,
|
||||
_: WorktreeId,
|
||||
_: Arc<Path>,
|
||||
_: &Arc<Path>,
|
||||
_: LanguageName,
|
||||
_: &mut AsyncApp,
|
||||
) -> Option<Toolchain> {
|
||||
None
|
||||
}
|
||||
}
|
||||
struct LocalStore(WeakEntity<LocalToolchainStore>);
|
||||
pub(crate) struct LocalStore(WeakEntity<LocalToolchainStore>);
|
||||
struct RemoteStore(WeakEntity<RemoteToolchainStore>);
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -349,17 +358,13 @@ impl LocalToolchainStore {
|
|||
.flatten()?;
|
||||
let worktree_id = snapshot.id();
|
||||
let worktree_root = snapshot.abs_path().to_path_buf();
|
||||
let delegate =
|
||||
Arc::from(ManifestQueryDelegate::new(snapshot)) as Arc<dyn ManifestDelegate>;
|
||||
let relative_path = manifest_tree
|
||||
.update(cx, |this, cx| {
|
||||
this.root_for_path(
|
||||
path,
|
||||
&mut std::iter::once(manifest_name.clone()),
|
||||
Arc::new(ManifestQueryDelegate::new(snapshot)),
|
||||
cx,
|
||||
)
|
||||
this.root_for_path(&path, &manifest_name, &delegate, cx)
|
||||
})
|
||||
.ok()?
|
||||
.remove(&manifest_name)
|
||||
.unwrap_or_else(|| ProjectPath {
|
||||
path: Arc::from(Path::new("")),
|
||||
worktree_id,
|
||||
|
@ -394,21 +399,20 @@ impl LocalToolchainStore {
|
|||
}
|
||||
pub(crate) fn active_toolchain(
|
||||
&self,
|
||||
path: ProjectPath,
|
||||
worktree_id: WorktreeId,
|
||||
relative_path: &Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
_: &App,
|
||||
) -> Task<Option<Toolchain>> {
|
||||
let ancestors = path.path.ancestors();
|
||||
Task::ready(
|
||||
self.active_toolchains
|
||||
.get(&(path.worktree_id, language_name))
|
||||
.and_then(|paths| {
|
||||
ancestors
|
||||
.into_iter()
|
||||
.find_map(|root_path| paths.get(root_path))
|
||||
})
|
||||
.cloned(),
|
||||
)
|
||||
) -> Option<Toolchain> {
|
||||
let ancestors = relative_path.ancestors();
|
||||
|
||||
self.active_toolchains
|
||||
.get(&(worktree_id, language_name))
|
||||
.and_then(|paths| {
|
||||
ancestors
|
||||
.into_iter()
|
||||
.find_map(|root_path| paths.get(root_path))
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
struct RemoteToolchainStore {
|
||||
|
|
|
@ -171,7 +171,11 @@ impl HeadlessProject {
|
|||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
prettier_store.clone(),
|
||||
toolchain_store.clone(),
|
||||
toolchain_store
|
||||
.read(cx)
|
||||
.as_local_store()
|
||||
.expect("Toolchain store to be local")
|
||||
.clone(),
|
||||
environment,
|
||||
manifest_tree,
|
||||
languages.clone(),
|
||||
|
|
|
@ -928,14 +928,14 @@ impl<'a> KeybindUpdateTarget<'a> {
|
|||
}
|
||||
let action_name: Value = self.action_name.into();
|
||||
let value = match self.action_arguments {
|
||||
Some(args) => {
|
||||
Some(args) if !args.is_empty() => {
|
||||
let args = serde_json::from_str::<Value>(args)
|
||||
.context("Failed to parse action arguments as JSON")?;
|
||||
serde_json::json!([action_name, args])
|
||||
}
|
||||
None => action_name,
|
||||
_ => action_name,
|
||||
};
|
||||
return Ok(value);
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn keystrokes_unparsed(&self) -> String {
|
||||
|
@ -1084,6 +1084,24 @@ mod tests {
|
|||
.unindent(),
|
||||
);
|
||||
|
||||
check_keymap_update(
|
||||
"[]",
|
||||
KeybindUpdateOperation::add(KeybindUpdateTarget {
|
||||
keystrokes: &parse_keystrokes("ctrl-a"),
|
||||
action_name: "zed::SomeAction",
|
||||
context: None,
|
||||
action_arguments: Some(""),
|
||||
}),
|
||||
r#"[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-a": "zed::SomeAction"
|
||||
}
|
||||
}
|
||||
]"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
check_keymap_update(
|
||||
r#"[
|
||||
{
|
||||
|
|
|
@ -2177,11 +2177,11 @@ impl KeybindingEditorModal {
|
|||
let action_arguments = self
|
||||
.action_arguments_editor
|
||||
.as_ref()
|
||||
.map(|editor| editor.read(cx).editor.read(cx).text(cx));
|
||||
.map(|arguments_editor| arguments_editor.read(cx).editor.read(cx).text(cx))
|
||||
.filter(|args| !args.is_empty());
|
||||
|
||||
let value = action_arguments
|
||||
.as_ref()
|
||||
.filter(|args| !args.is_empty())
|
||||
.map(|args| {
|
||||
serde_json::from_str(args).context("Failed to parse action arguments as JSON")
|
||||
})
|
||||
|
@ -2289,29 +2289,11 @@ impl KeybindingEditorModal {
|
|||
|
||||
let create = self.creating;
|
||||
|
||||
let status_toast = StatusToast::new(
|
||||
format!(
|
||||
"Saved edits to the {} action.",
|
||||
&self.editing_keybind.action().humanized_name
|
||||
),
|
||||
cx,
|
||||
move |this, _cx| {
|
||||
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
|
||||
.dismiss_button(true)
|
||||
// .action("Undo", f) todo: wire the undo functionality
|
||||
},
|
||||
);
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(status_toast, cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let action_name = existing_keybind.action().name;
|
||||
let humanized_action_name = existing_keybind.action().humanized_name.clone();
|
||||
|
||||
if let Err(err) = save_keybinding_update(
|
||||
match save_keybinding_update(
|
||||
create,
|
||||
existing_keybind,
|
||||
&action_mapping,
|
||||
|
@ -2321,25 +2303,43 @@ impl KeybindingEditorModal {
|
|||
)
|
||||
.await
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(InputError::error(err), cx);
|
||||
})
|
||||
.log_err();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.keymap_editor.update(cx, |keymap, cx| {
|
||||
keymap.previous_edit = Some(PreviousEdit::Keybinding {
|
||||
action_mapping,
|
||||
action_name,
|
||||
fallback: keymap
|
||||
.table_interaction_state
|
||||
.read(cx)
|
||||
.get_scrollbar_offset(Axis::Vertical),
|
||||
})
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.keymap_editor.update(cx, |keymap, cx| {
|
||||
keymap.previous_edit = Some(PreviousEdit::Keybinding {
|
||||
action_mapping,
|
||||
action_name,
|
||||
fallback: keymap
|
||||
.table_interaction_state
|
||||
.read(cx)
|
||||
.get_scrollbar_offset(Axis::Vertical),
|
||||
});
|
||||
let status_toast = StatusToast::new(
|
||||
format!("Saved edits to the {} action.", humanized_action_name),
|
||||
cx,
|
||||
move |this, _cx| {
|
||||
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
|
||||
.dismiss_button(true)
|
||||
// .action("Undo", f) todo: wire the undo functionality
|
||||
},
|
||||
);
|
||||
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(status_toast, cx);
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(InputError::error(err), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -3011,7 +3011,7 @@ async fn save_keybinding_update(
|
|||
|
||||
let updated_keymap_contents =
|
||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
||||
.context("Failed to update keybinding")?;
|
||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||
fs.write(
|
||||
paths::keymap_file().as_path(),
|
||||
updated_keymap_contents.as_bytes(),
|
||||
|
|
|
@ -14,7 +14,6 @@ use ui::{
|
|||
Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor,
|
||||
Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::maybe;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
|
||||
use crate::TitleBar;
|
||||
|
@ -32,52 +31,59 @@ actions!(
|
|||
);
|
||||
|
||||
fn toggle_screen_sharing(
|
||||
screen: Option<Rc<dyn ScreenCaptureSource>>,
|
||||
screen: anyhow::Result<Option<Rc<dyn ScreenCaptureSource>>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
let clicked_on_currently_shared_screen =
|
||||
room.shared_screen_id().is_some_and(|screen_id| {
|
||||
Some(screen_id)
|
||||
== screen
|
||||
.as_deref()
|
||||
.and_then(|s| s.metadata().ok().map(|meta| meta.id))
|
||||
});
|
||||
let should_unshare_current_screen = room.is_sharing_screen();
|
||||
let unshared_current_screen = should_unshare_current_screen.then(|| {
|
||||
telemetry::event!(
|
||||
"Screen Share Disabled",
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id(),
|
||||
);
|
||||
room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx)
|
||||
});
|
||||
if let Some(screen) = screen {
|
||||
if !should_unshare_current_screen {
|
||||
let toggle_screen_sharing = match screen {
|
||||
Ok(screen) => {
|
||||
let Some(room) = call.room().cloned() else {
|
||||
return;
|
||||
};
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
let clicked_on_currently_shared_screen =
|
||||
room.shared_screen_id().is_some_and(|screen_id| {
|
||||
Some(screen_id)
|
||||
== screen
|
||||
.as_deref()
|
||||
.and_then(|s| s.metadata().ok().map(|meta| meta.id))
|
||||
});
|
||||
let should_unshare_current_screen = room.is_sharing_screen();
|
||||
let unshared_current_screen = should_unshare_current_screen.then(|| {
|
||||
telemetry::event!(
|
||||
"Screen Share Enabled",
|
||||
"Screen Share Disabled",
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id(),
|
||||
);
|
||||
}
|
||||
cx.spawn(async move |room, cx| {
|
||||
unshared_current_screen.transpose()?;
|
||||
if !clicked_on_currently_shared_screen {
|
||||
room.update(cx, |room, cx| room.share_screen(screen, cx))?
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx)
|
||||
});
|
||||
if let Some(screen) = screen {
|
||||
if !should_unshare_current_screen {
|
||||
telemetry::event!(
|
||||
"Screen Share Enabled",
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id(),
|
||||
);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
|
||||
}
|
||||
cx.spawn(async move |room, cx| {
|
||||
unshared_current_screen.transpose()?;
|
||||
if !clicked_on_currently_shared_screen {
|
||||
room.update(cx, |room, cx| room.share_screen(screen, cx))?
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing
|
||||
}
|
||||
Err(e) => Task::ready(Err(e)),
|
||||
};
|
||||
toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
|
||||
}
|
||||
|
||||
fn toggle_mute(_: &ToggleMute, cx: &mut App) {
|
||||
|
@ -483,9 +489,8 @@ impl TitleBar {
|
|||
let screen = if should_share {
|
||||
cx.update(|_, cx| pick_default_screen(cx))?.await
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
};
|
||||
|
||||
cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?;
|
||||
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
|
@ -571,7 +576,7 @@ impl TitleBar {
|
|||
selectable: true,
|
||||
documentation_aside: None,
|
||||
handler: Rc::new(move |_, window, cx| {
|
||||
toggle_screen_sharing(Some(screen.clone()), window, cx);
|
||||
toggle_screen_sharing(Ok(Some(screen.clone())), window, cx);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
@ -585,11 +590,11 @@ impl TitleBar {
|
|||
}
|
||||
|
||||
/// Picks the screen to share when clicking on the main screen sharing button.
|
||||
fn pick_default_screen(cx: &App) -> Task<Option<Rc<dyn ScreenCaptureSource>>> {
|
||||
fn pick_default_screen(cx: &App) -> Task<anyhow::Result<Option<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let source = cx.screen_capture_sources();
|
||||
cx.spawn(async move |_| {
|
||||
let available_sources = maybe!(async move { source.await? }).await.ok()?;
|
||||
available_sources
|
||||
let available_sources = source.await??;
|
||||
Ok(available_sources
|
||||
.iter()
|
||||
.find(|it| {
|
||||
it.as_ref()
|
||||
|
@ -597,6 +602,6 @@ fn pick_default_screen(cx: &App) -> Task<Option<Rc<dyn ScreenCaptureSource>>> {
|
|||
.is_ok_and(|meta| meta.is_main.unwrap_or_default())
|
||||
})
|
||||
.or_else(|| available_sources.iter().next())
|
||||
.cloned()
|
||||
.cloned())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ pub enum AnimationDirection {
|
|||
FromTop,
|
||||
}
|
||||
|
||||
pub trait DefaultAnimations: Styled + Sized {
|
||||
pub trait DefaultAnimations: Styled + Sized + Element {
|
||||
fn animate_in(
|
||||
self,
|
||||
animation_type: AnimationDirection,
|
||||
|
@ -44,8 +44,13 @@ pub trait DefaultAnimations: Styled + Sized {
|
|||
AnimationDirection::FromTop => "animate_from_top",
|
||||
};
|
||||
|
||||
let animation_id = self.id().map_or_else(
|
||||
|| ElementId::from(animation_name),
|
||||
|id| (id, animation_name).into(),
|
||||
);
|
||||
|
||||
self.with_animation(
|
||||
animation_name,
|
||||
animation_id,
|
||||
gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()),
|
||||
move |mut this, delta| {
|
||||
let start_opacity = 0.4;
|
||||
|
@ -91,7 +96,7 @@ pub trait DefaultAnimations: Styled + Sized {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Styled> DefaultAnimations for E {}
|
||||
impl<E: Styled + Element> DefaultAnimations for E {}
|
||||
|
||||
// Don't use this directly, it only exists to show animation previews
|
||||
#[derive(RegisterComponent)]
|
||||
|
@ -132,7 +137,7 @@ impl Component for Animation {
|
|||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, false),
|
||||
.animate_in_from_bottom(false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -151,7 +156,7 @@ impl Component for Animation {
|
|||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, false),
|
||||
.animate_in_from_top(false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -170,7 +175,7 @@ impl Component for Animation {
|
|||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, false),
|
||||
.animate_in_from_left(false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -189,7 +194,7 @@ impl Component for Animation {
|
|||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, false),
|
||||
.animate_in_from_right(false),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -214,7 +219,7 @@ impl Component for Animation {
|
|||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, true),
|
||||
.animate_in_from_bottom(true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -233,7 +238,7 @@ impl Component for Animation {
|
|||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, true),
|
||||
.animate_in_from_top(true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -252,7 +257,7 @@ impl Component for Animation {
|
|||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, true),
|
||||
.animate_in_from_left(true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -271,7 +276,7 @@ impl Component for Animation {
|
|||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, true),
|
||||
.animate_in_from_right(true),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
|||
Vim,
|
||||
motion::{Motion, MotionKind},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
|
@ -102,8 +103,20 @@ impl Vim {
|
|||
// Emulates behavior in vim where if we expanded backwards to include a newline
|
||||
// the cursor gets set back to the start of the line
|
||||
let mut should_move_to_start: HashSet<_> = Default::default();
|
||||
|
||||
// Emulates behavior in vim where after deletion the cursor should try to move
|
||||
// to the same column it was before deletion if the line is not empty or only
|
||||
// contains whitespace
|
||||
let mut column_before_move: HashMap<_, _> = Default::default();
|
||||
let target_mode = object.target_visual_mode(vim.mode, around);
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let cursor_point = selection.head().to_point(map);
|
||||
if target_mode == Mode::VisualLine {
|
||||
column_before_move.insert(selection.id, cursor_point.column);
|
||||
}
|
||||
|
||||
object.expand_selection(map, selection, around, times);
|
||||
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
|
||||
let mut move_selection_start_to_previous_line =
|
||||
|
@ -164,6 +177,15 @@ impl Vim {
|
|||
let mut cursor = selection.head();
|
||||
if should_move_to_start.contains(&selection.id) {
|
||||
*cursor.column_mut() = 0;
|
||||
} else if let Some(column) = column_before_move.get(&selection.id)
|
||||
&& *column > 0
|
||||
{
|
||||
let mut cursor_point = cursor.to_point(map);
|
||||
cursor_point.column = *column;
|
||||
cursor = map
|
||||
.buffer_snapshot
|
||||
.clip_point(cursor_point, Bias::Left)
|
||||
.to_display_point(map);
|
||||
}
|
||||
cursor = map.clip_point(cursor, Bias::Left);
|
||||
selection.collapse_to(cursor, selection.goal)
|
||||
|
|
|
@ -1444,14 +1444,15 @@ fn paragraph(
|
|||
return None;
|
||||
}
|
||||
|
||||
let paragraph_start_row = paragraph_start.row();
|
||||
if paragraph_start_row.0 != 0 {
|
||||
let paragraph_start_buffer_point = paragraph_start.to_point(map);
|
||||
if paragraph_start_buffer_point.row != 0 {
|
||||
let previous_paragraph_last_line_start =
|
||||
Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map);
|
||||
Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
|
||||
paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
|
||||
}
|
||||
} else {
|
||||
let mut start_row = paragraph_end_row.0 + 1;
|
||||
let paragraph_end_buffer_point = paragraph_end.to_point(map);
|
||||
let mut start_row = paragraph_end_buffer_point.row + 1;
|
||||
if i > 0 {
|
||||
start_row += 1;
|
||||
}
|
||||
|
@ -1903,6 +1904,90 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
const WRAPPING_EXAMPLE: &str = indoc! {"
|
||||
ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
|
||||
|
||||
ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
|
||||
|
||||
ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
|
||||
"};
|
||||
|
||||
cx.set_shared_wrap(20).await;
|
||||
|
||||
cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
const WRAPPING_EXAMPLE: &str = indoc! {"
|
||||
ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
|
||||
|
||||
ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
|
||||
|
||||
ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
|
||||
"};
|
||||
|
||||
cx.set_shared_wrap(20).await;
|
||||
|
||||
cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
a
|
||||
ˇ•
|
||||
aaaaaaaaaaaaa
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("d i p").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
a
|
||||
aaaaaaaˇaaaaaa
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
const WRAPPING_EXAMPLE: &str = indoc! {"
|
||||
ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
|
||||
|
||||
ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
|
||||
|
||||
ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
|
||||
"};
|
||||
|
||||
cx.set_shared_wrap(20).await;
|
||||
|
||||
cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
// Test string with "`" for opening surrounders and "'" for closing surrounders
|
||||
const SURROUNDING_MARKER_STRING: &str = indoc! {"
|
||||
ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
|
||||
|
|
|
@ -1028,13 +1028,21 @@ impl Operator {
|
|||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
fn make_visible(c: &str) -> &str {
|
||||
match c {
|
||||
"\n" => "enter",
|
||||
"\t" => "tab",
|
||||
" " => "space",
|
||||
c => c,
|
||||
}
|
||||
}
|
||||
match self {
|
||||
Operator::Digraph {
|
||||
first_char: Some(first_char),
|
||||
} => format!("^K{first_char}"),
|
||||
} => format!("^K{}", make_visible(&first_char.to_string())),
|
||||
Operator::Literal {
|
||||
prefix: Some(prefix),
|
||||
} => format!("^V{prefix}"),
|
||||
} => format!("^V{}", make_visible(&prefix)),
|
||||
Operator::AutoIndent => "=".to_string(),
|
||||
Operator::ShellCommand => "=".to_string(),
|
||||
_ => self.id().to_string(),
|
||||
|
|
|
@ -414,6 +414,8 @@ impl Vim {
|
|||
);
|
||||
}
|
||||
|
||||
let original_point = selection.tail().to_point(&map);
|
||||
|
||||
if let Some(range) = object.range(map, mut_selection, around, count) {
|
||||
if !range.is_empty() {
|
||||
let expand_both_ways = object.always_expands_both_ways()
|
||||
|
@ -462,6 +464,37 @@ impl Vim {
|
|||
};
|
||||
selection.end = new_selection_end.to_display_point(map);
|
||||
}
|
||||
|
||||
// To match vim, if the range starts of the same line as it originally
|
||||
// did, we keep the tail of the selection in the same place instead of
|
||||
// snapping it to the start of the line
|
||||
if target_mode == Mode::VisualLine {
|
||||
let new_start_point = selection.start.to_point(map);
|
||||
if new_start_point.row == original_point.row {
|
||||
if selection.end.to_point(map).row > new_start_point.row {
|
||||
if original_point.column
|
||||
== map
|
||||
.buffer_snapshot
|
||||
.line_len(MultiBufferRow(original_point.row))
|
||||
{
|
||||
selection.start = movement::saturating_left(
|
||||
map,
|
||||
original_point.to_display_point(map),
|
||||
)
|
||||
} else {
|
||||
selection.start = original_point.to_display_point(map)
|
||||
}
|
||||
} else {
|
||||
selection.end = movement::saturating_right(
|
||||
map,
|
||||
original_point.to_display_point(map),
|
||||
);
|
||||
if original_point.column > 0 {
|
||||
selection.reversed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
{"SetOption":{"value":"wrap"}}
|
||||
{"SetOption":{"value":"columns=20"}}
|
||||
{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}}
|
|
@ -0,0 +1,72 @@
|
|||
{"SetOption":{"value":"wrap"}}
|
||||
{"SetOption":{"value":"columns=20"}}
|
||||
{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"Second paragraph that is also quite long and will definitely wrap under soft wrap conditions andˇ should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nThird paragraph with additional long text content that will also wrap when line length is constraˇined by the wrapping settings.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}}
|
|
@ -0,0 +1,5 @@
|
|||
{"Put":{"state":"a\n ˇ•\naaaaaaaaaaaaa\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"a\naaaaaaaˇaaaaaa\n","mode":"Normal"}}
|
|
@ -0,0 +1,72 @@
|
|||
{"SetOption":{"value":"wrap"}}
|
||||
{"SetOption":{"value":"columns=20"}}
|
||||
{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"«Fˇ»irst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"«ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is l»imited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Sˇ»econd paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and s»hould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Tˇ»hird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping s»ettings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.»\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"«First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is «limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and «should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\nˇ»","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping «settings.\nˇ»","mode":"VisualLine"}}
|
||||
{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings«.\nˇ»","mode":"VisualLine"}}
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task};
|
||||
use gpui::{AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, Subscription, Task};
|
||||
use ui::{animation::DefaultAnimations, prelude::*};
|
||||
use zed_actions::toast;
|
||||
|
||||
|
@ -76,6 +76,7 @@ impl<V: ToastView> ToastViewHandle for Entity<V> {
|
|||
}
|
||||
|
||||
pub struct ActiveToast {
|
||||
id: EntityId,
|
||||
toast: Box<dyn ToastViewHandle>,
|
||||
action: Option<ToastAction>,
|
||||
_subscriptions: [Subscription; 1],
|
||||
|
@ -113,9 +114,9 @@ impl ToastLayer {
|
|||
V: ToastView,
|
||||
{
|
||||
if let Some(active_toast) = &self.active_toast {
|
||||
let is_close = active_toast.toast.view().downcast::<V>().is_ok();
|
||||
let did_close = self.hide_toast(cx);
|
||||
if is_close || !did_close {
|
||||
let show_new = active_toast.id != new_toast.entity_id();
|
||||
self.hide_toast(cx);
|
||||
if !show_new {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -130,11 +131,12 @@ impl ToastLayer {
|
|||
let focus_handle = cx.focus_handle();
|
||||
|
||||
self.active_toast = Some(ActiveToast {
|
||||
toast: Box::new(new_toast.clone()),
|
||||
action,
|
||||
_subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| {
|
||||
this.hide_toast(cx);
|
||||
})],
|
||||
id: new_toast.entity_id(),
|
||||
toast: Box::new(new_toast),
|
||||
action,
|
||||
focus_handle,
|
||||
});
|
||||
|
||||
|
@ -143,11 +145,9 @@ impl ToastLayer {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn hide_toast(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
pub fn hide_toast(&mut self, cx: &mut Context<Self>) {
|
||||
self.active_toast.take();
|
||||
cx.notify();
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn active_toast<V>(&self) -> Option<Entity<V>>
|
||||
|
@ -218,11 +218,10 @@ impl Render for ToastLayer {
|
|||
let Some(active_toast) = &self.active_toast else {
|
||||
return div();
|
||||
};
|
||||
let handle = cx.weak_entity();
|
||||
|
||||
div().absolute().size_full().bottom_0().left_0().child(
|
||||
v_flex()
|
||||
.id("toast-layer-container")
|
||||
.id(("toast-layer-container", active_toast.id))
|
||||
.absolute()
|
||||
.w_full()
|
||||
.bottom(px(0.))
|
||||
|
@ -234,17 +233,14 @@ impl Render for ToastLayer {
|
|||
h_flex()
|
||||
.id("active-toast-container")
|
||||
.occlude()
|
||||
.on_hover(move |hover_start, _window, cx| {
|
||||
let Some(this) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
.on_hover(cx.listener(|this, hover_start, _window, cx| {
|
||||
if *hover_start {
|
||||
this.update(cx, |this, _| this.pause_dismiss_timer());
|
||||
this.pause_dismiss_timer();
|
||||
} else {
|
||||
this.update(cx, |this, cx| this.restart_dismiss_timer(cx));
|
||||
this.restart_dismiss_timer(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
})
|
||||
}))
|
||||
.on_click(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
|
|
|
@ -561,6 +561,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
@ -578,6 +579,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
files: true,
|
||||
directories,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
@ -2655,6 +2657,7 @@ impl Workspace {
|
|||
files: false,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
},
|
||||
DirectoryLister::Project(self.project.clone()),
|
||||
window,
|
||||
|
|
|
@ -550,7 +550,8 @@ async fn upload_previous_panics(
|
|||
|
||||
pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow::Result<()> {
|
||||
let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else {
|
||||
return Err(anyhow::anyhow!("Minidump endpoint not set"));
|
||||
log::warn!("Minidump endpoint not set");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
|
||||
|
|
|
@ -645,6 +645,7 @@ fn register_actions(
|
|||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
},
|
||||
DirectoryLister::Local(
|
||||
workspace.project().clone(),
|
||||
|
@ -685,6 +686,7 @@ fn register_actions(
|
|||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
},
|
||||
DirectoryLister::Project(workspace.project().clone()),
|
||||
window,
|
||||
|
|
|
@ -427,7 +427,7 @@ Custom models will be listed in the model dropdown in the Agent Panel.
|
|||
Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider.
|
||||
This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models.
|
||||
|
||||
You can add a custom, OpenAI-compatible model via either via the UI or by editing your `settings.json`.
|
||||
You can add a custom, OpenAI-compatible model either via the UI or by editing your `settings.json`.
|
||||
|
||||
To do it via the UI, go to the Agent Panel settings (`agent: open settings`) and look for the "Add Provider" button to the right of the "LLM Providers" section title.
|
||||
Then, fill up the input fields available in the modal.
|
||||
|
@ -443,7 +443,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod
|
|||
{
|
||||
"name": "mistralai/Mixtral-8x7B-Instruct-v0.1",
|
||||
"display_name": "Together Mixtral 8x7B",
|
||||
"max_tokens": 32768
|
||||
"max_tokens": 32768,
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"images": false,
|
||||
"parallel_tool_calls": false,
|
||||
"prompt_cache_key": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -451,6 +457,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod
|
|||
}
|
||||
```
|
||||
|
||||
By default, OpenAI-compatible models inherit the following capabilities:
|
||||
|
||||
- `tools`: true (supports tool/function calling)
|
||||
- `images`: false (does not support image inputs)
|
||||
- `parallel_tool_calls`: false (does not support `parallel_tool_calls` parameter)
|
||||
- `prompt_cache_key`: false (does not support `prompt_cache_key` parameter)
|
||||
|
||||
Note that LLM API keys aren't stored in your settings file.
|
||||
So, ensure you have it set in your environment variables (`OPENAI_API_KEY=<your api key>`) so your settings can pick it up.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue