Compare commits

..

5 Commits

Author SHA1 Message Date
Conrad Irwin
2aa1255ddb Merge branch 'main' into message-editor 2025-08-13 15:37:54 -06:00
Conrad Irwin
d36304963e History, is history 2025-08-13 13:44:39 -06:00
Conrad Irwin
0d71351b02 Merge branch 'main' into message-editor 2025-08-13 13:07:25 -06:00
Conrad Irwin
b06fe288f3 Clip Clop 2025-08-13 13:05:28 -06:00
Conrad Irwin
fd0ffb737f Create a new MessageEditor 2025-08-13 12:01:50 -06:00
75 changed files with 1252 additions and 4387 deletions

11
Cargo.lock generated
View File

@@ -172,9 +172,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.0.24"
version = "0.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd68bbbef8e424fb8a605c5f0b00c360f682c4528b0a5feb5ec928aaf5ce28e"
checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8"
dependencies = [
"anyhow",
"futures 0.3.31",
@@ -395,7 +395,6 @@ dependencies = [
"ui",
"ui_input",
"unindent",
"url",
"urlencoding",
"util",
"uuid",
@@ -1304,9 +1303,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
@@ -15054,10 +15053,8 @@ dependencies = [
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]

View File

@@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" }
#
agentic-coding-protocol = "0.0.10"
agent-client-protocol = "0.0.24"
agent-client-protocol = "0.0.23"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 607 B

View File

@@ -58,8 +58,6 @@
"[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown",
"[ f": "workspace::FollowNextCollaborator",
"] f": "workspace::FollowNextCollaborator",
// Word motions
"w": "vim::NextWordStart",
@@ -392,7 +390,7 @@
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
"l": "vim::WrappingRight",
"y": "vim::HelixYank",
"y": "editor::Copy",
"alt-;": "vim::OtherEnd",
"ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
@@ -409,7 +407,6 @@
"g w": "vim::PushRewrap",
"insert": "vim::InsertBefore",
"alt-.": "vim::RepeatFind",
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",

View File

@@ -13,7 +13,7 @@ path = "src/acp_thread.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
action_log.workspace = true
@@ -29,7 +29,6 @@ gpui.workspace = true
itertools.workspace = true
language.workspace = true
markdown.workspace = true
parking_lot = { workspace = true, optional = true }
project.workspace = true
prompt_store.workspace = true
serde.workspace = true

View File

@@ -400,7 +400,7 @@ impl ContentBlock {
}
}
let new_content = self.block_string_contents(block);
let new_content = self.extract_content_from_block(block);
match self {
ContentBlock::Empty => {
@@ -410,7 +410,7 @@ impl ContentBlock {
markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
}
ContentBlock::ResourceLink { resource_link } => {
let existing_content = Self::resource_link_md(&resource_link.uri);
let existing_content = Self::resource_link_to_content(&resource_link.uri);
let combined = format!("{}\n{}", existing_content, new_content);
*self = Self::create_markdown_block(combined, language_registry, cx);
@@ -418,6 +418,14 @@ impl ContentBlock {
}
}
fn resource_link_to_content(uri: &str) -> String {
if let Some(uri) = MentionUri::parse(&uri).log_err() {
uri.as_link().to_string()
} else {
uri.to_string()
}
}
fn create_markdown_block(
content: String,
language_registry: &Arc<LanguageRegistry>,
@@ -429,11 +437,11 @@ impl ContentBlock {
}
}
fn block_string_contents(&self, block: acp::ContentBlock) -> String {
fn extract_content_from_block(&self, block: acp::ContentBlock) -> String {
match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
acp::ContentBlock::ResourceLink(resource_link) => {
Self::resource_link_md(&resource_link.uri)
Self::resource_link_to_content(&resource_link.uri)
}
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource:
@@ -442,24 +450,13 @@ impl ContentBlock {
..
}),
..
}) => Self::resource_link_md(&uri),
acp::ContentBlock::Image(image) => Self::image_md(&image),
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
}) => Self::resource_link_to_content(&uri),
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => String::new(),
}
}
fn resource_link_md(uri: &str) -> String {
if let Some(uri) = MentionUri::parse(&uri).log_err() {
uri.as_link().to_string()
} else {
uri.to_string()
}
}
fn image_md(_image: &acp::ImageContent) -> String {
"`Image`".into()
}
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self {
ContentBlock::Empty => "",
@@ -1578,7 +1575,11 @@ mod tests {
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.spawn(async move |mut cx| {
connection
.new_thread(project, Path::new(path!("/test")), &mut cx)
.await
})
.await
.unwrap();
@@ -1698,7 +1699,11 @@ mod tests {
));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.spawn(async move |mut cx| {
connection
.new_thread(project, Path::new(path!("/test")), &mut cx)
.await
})
.await
.unwrap();
@@ -1781,7 +1786,7 @@ mod tests {
.unwrap();
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
.spawn(|mut cx| connection.new_thread(project, Path::new(path!("/tmp")), &mut cx))
.await
.unwrap();
@@ -1844,7 +1849,11 @@ mod tests {
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.spawn(async move |mut cx| {
connection
.new_thread(project, Path::new(path!("/test")), &mut cx)
.await
})
.await
.unwrap();
@@ -1952,11 +1961,10 @@ mod tests {
}
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
let thread = connection
.new_thread(project, Path::new(path!("/test")), &mut cx.to_async())
.await
.unwrap();
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx)))
.await
.unwrap();
@@ -2013,8 +2021,8 @@ mod tests {
.boxed_local()
}
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
let thread = connection
.new_thread(project, Path::new(path!("/test")), &mut cx.to_async())
.await
.unwrap();
@@ -2219,7 +2227,7 @@ mod tests {
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::App,
cx: &mut gpui::AsyncApp,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(
rand::thread_rng()
@@ -2229,8 +2237,9 @@ mod tests {
.collect::<String>()
.into(),
);
let thread =
cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx));
let thread = cx
.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx))
.unwrap();
self.sessions.lock().insert(session_id, thread.downgrade());
Task::ready(Ok(thread))
}

View File

@@ -2,7 +2,7 @@ use crate::AcpThread;
use agent_client_protocol::{self as acp};
use anyhow::Result;
use collections::IndexMap;
use gpui::{Entity, SharedString, Task};
use gpui::{AsyncApp, Entity, SharedString, Task};
use project::Project;
use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc};
use ui::{App, IconName};
@@ -22,7 +22,7 @@ pub trait AgentConnection {
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>>;
fn auth_methods(&self) -> &[acp::AuthMethod];
@@ -160,155 +160,3 @@ impl AgentModelList {
}
}
}
#[cfg(feature = "test-support")]
mod test_support {
use std::sync::Arc;
use collections::HashMap;
use futures::future::try_join_all;
use gpui::{AppContext as _, WeakEntity};
use parking_lot::Mutex;
use super::*;
#[derive(Clone, Default)]
pub struct StubAgentConnection {
sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
}
impl StubAgentConnection {
pub fn new() -> Self {
Self {
next_prompt_updates: Default::default(),
permission_requests: HashMap::default(),
sessions: Arc::default(),
}
}
pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
*self.next_prompt_updates.lock() = updates;
}
pub fn with_permission_requests(
mut self,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
) -> Self {
self.permission_requests = permission_requests;
self
}
pub fn send_update(
&self,
session_id: acp::SessionId,
update: acp::SessionUpdate,
cx: &mut App,
) {
self.sessions
.lock()
.get(&session_id)
.unwrap()
.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
})
.unwrap();
}
}
impl AgentConnection for StubAgentConnection {
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
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());
Task::ready(Ok(thread))
}
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
_cx: &mut App,
) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(
&self,
_id: Option<UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<gpui::Result<acp::PromptResponse>> {
let sessions = self.sessions.lock();
let thread = sessions.get(&params.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(),
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 session_editor(
&self,
_session_id: &agent_client_protocol::SessionId,
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
Some(Rc::new(StubAgentSessionEditor))
}
}
struct StubAgentSessionEditor;
impl AgentSessionEditor for StubAgentSessionEditor {
fn truncate(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
}
}
#[cfg(feature = "test-support")]
pub use test_support::*;

View File

@@ -6,7 +6,6 @@ use std::{
fmt,
ops::Range,
path::{Path, PathBuf},
str::FromStr,
};
use ui::{App, IconName, SharedString};
use url::Url;
@@ -225,14 +224,6 @@ impl MentionUri {
}
}
impl FromStr for MentionUri {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Self::parse(s)
}
}
pub struct MentionLink<'a>(&'a MentionUri);
impl fmt::Display for MentionLink<'_> {
@@ -295,7 +286,7 @@ mod tests {
abs_path,
is_directory,
} => {
assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir");
assert!(is_directory);
}
_ => panic!("Expected File variant"),

View File

@@ -205,22 +205,6 @@ impl ThreadStore {
(this, ready_rx)
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(project: Entity<Project>, cx: &mut App) -> Self {
Self {
project,
tools: cx.new(|_| ToolWorkingSet::default()),
prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
prompt_store: None,
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
project_context: SharedProjectContext::default(),
reload_system_prompt_tx: mpsc::channel(0).0,
_reload_system_prompt_task: Task::ready(()),
_subscriptions: vec![],
}
}
fn handle_project_event(
&mut self,
_project: Entity<Project>,

View File

@@ -1,8 +1,8 @@
use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool,
EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool,
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent,
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool,
ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent,
WebSearchTool,
};
use acp_thread::AgentModelSelector;
@@ -522,7 +522,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
cx: &mut AsyncApp,
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone();
log::info!("Creating new thread for project at: {:?}", cwd);
@@ -583,22 +583,22 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
default_model,
cx,
);
thread.add_tool(CopyPathTool::new(project.clone()));
thread.add_tool(CreateDirectoryTool::new(project.clone()));
thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone()));
thread.add_tool(CopyPathTool::new(project.clone()));
thread.add_tool(DiagnosticsTool::new(project.clone()));
thread.add_tool(EditFileTool::new(cx.entity()));
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(GrepTool::new(project.clone()));
thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(MovePathTool::new(project.clone()));
thread.add_tool(NowTool);
thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(OpenTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(TerminalTool::new(project.clone(), cx));
thread.add_tool(ThinkingTool);
thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model.
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
thread.add_tool(GrepTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(EditFileTool::new(cx.entity()));
thread.add_tool(NowTool);
thread.add_tool(TerminalTool::new(project.clone(), cx));
// TODO: Needs to be conditional based on zed model or not
thread.add_tool(WebSearchTool);
thread
});
@@ -940,7 +940,11 @@ mod tests {
// Create a thread/session
let acp_thread = cx
.update(|cx| {
Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx)
Rc::new(connection.clone()).new_thread(
project.clone(),
Path::new("/a"),
&mut cx.to_async(),
)
})
.await
.unwrap();

View File

@@ -841,7 +841,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
// Create a thread using new_thread
let connection_rc = Rc::new(connection.clone());
let acp_thread = cx
.update(|cx| connection_rc.new_thread(project, cwd, cx))
.update(|cx| connection_rc.new_thread(project, cwd, &mut cx.to_async()))
.await
.expect("new_thread should succeed");

View File

@@ -25,8 +25,8 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::fmt::Write;
use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
use std::{fmt::Write, ops::Range};
use util::{ResultExt, markdown::MarkdownCodeBlock};
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -79,9 +79,9 @@ impl UserMessage {
}
UserMessageContent::Mention { uri, content } => {
if !content.is_empty() {
let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content);
markdown.push_str(&format!("{}\n\n{}\n", uri.to_link(), content));
} else {
let _ = write!(&mut markdown, "{}\n", uri.as_link());
markdown.push_str(&format!("{}\n", uri.to_link()));
}
}
}
@@ -104,14 +104,12 @@ impl UserMessage {
const OPEN_FILES_TAG: &str = "<files>";
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
const OPEN_THREADS_TAG: &str = "<threads>";
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
const OPEN_RULES_TAG: &str =
"<rules>\nThe user has specified the following rules that should be applied:\n";
let mut file_context = OPEN_FILES_TAG.to_string();
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
let mut thread_context = OPEN_THREADS_TAG.to_string();
let mut fetch_context = OPEN_FETCH_TAG.to_string();
let mut rules_context = OPEN_RULES_TAG.to_string();
for chunk in &self.content {
@@ -124,40 +122,21 @@ impl UserMessage {
}
UserMessageContent::Mention { uri, content } => {
match uri {
MentionUri::File { abs_path, .. } => {
MentionUri::File(path) | MentionUri::Symbol(path, _) => {
write!(
&mut symbol_context,
"\n{}",
MarkdownCodeBlock {
tag: &codeblock_tag(&abs_path, None),
tag: &codeblock_tag(&path),
text: &content.to_string(),
}
)
.ok();
}
MentionUri::Symbol {
path, line_range, ..
}
| MentionUri::Selection {
path, line_range, ..
} => {
write!(
&mut rules_context,
"\n{}",
MarkdownCodeBlock {
tag: &codeblock_tag(&path, Some(line_range)),
text: &content
}
)
.ok();
}
MentionUri::Thread { .. } => {
MentionUri::Thread(_session_id) => {
write!(&mut thread_context, "\n{}\n", content).ok();
}
MentionUri::TextThread { .. } => {
write!(&mut thread_context, "\n{}\n", content).ok();
}
MentionUri::Rule { .. } => {
MentionUri::Rule(_user_prompt_id) => {
write!(
&mut rules_context,
"\n{}",
@@ -168,12 +147,9 @@ impl UserMessage {
)
.ok();
}
MentionUri::Fetch { url } => {
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
}
}
language_model::MessageContent::Text(uri.as_link().to_string())
language_model::MessageContent::Text(uri.to_link())
}
};
@@ -203,13 +179,6 @@ impl UserMessage {
.push(language_model::MessageContent::Text(thread_context));
}
if fetch_context.len() > OPEN_FETCH_TAG.len() {
fetch_context.push_str("</fetched_urls>\n");
message
.content
.push(language_model::MessageContent::Text(fetch_context));
}
if rules_context.len() > OPEN_RULES_TAG.len() {
rules_context.push_str("</user_rules>\n");
message
@@ -231,26 +200,6 @@ impl UserMessage {
}
}
fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
let mut result = String::new();
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
let _ = write!(result, "{} ", extension);
}
let _ = write!(result, "{}", full_path.display());
if let Some(range) = line_range {
if range.start == range.end {
let _ = write!(result, ":{}", range.start + 1);
} else {
let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1);
}
}
result
}
impl AgentMessage {
pub fn to_markdown(&self) -> String {
let mut markdown = String::from("## Assistant\n\n");
@@ -411,7 +360,7 @@ pub struct Thread {
/// Survives across multiple requests as the model performs tool calls and
/// we run tools, report their results.
running_turn: Option<Task<()>>,
pending_message: Option<AgentMessage>,
pending_agent_message: Option<AgentMessage>,
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
context_server_registry: Entity<ContextServerRegistry>,
profile_id: AgentProfileId,
@@ -437,7 +386,7 @@ impl Thread {
messages: Vec::new(),
completion_mode: CompletionMode::Normal,
running_turn: None,
pending_message: None,
pending_agent_message: None,
tools: BTreeMap::default(),
context_server_registry,
profile_id,
@@ -463,7 +412,7 @@ impl Thread {
#[cfg(any(test, feature = "test-support"))]
pub fn last_message(&self) -> Option<Message> {
if let Some(message) = self.pending_message.clone() {
if let Some(message) = self.pending_agent_message.clone() {
Some(Message::Agent(message))
} else {
self.messages.last().cloned()
@@ -485,7 +434,7 @@ impl Thread {
pub fn cancel(&mut self) {
// TODO: do we need to emit a stop::cancel for ACP?
self.running_turn.take();
self.flush_pending_message();
self.flush_pending_agent_message();
}
pub fn truncate(&mut self, message_id: UserMessageId) -> Result<()> {
@@ -521,58 +470,74 @@ impl Thread {
mpsc::unbounded::<Result<AgentResponseEvent, LanguageModelCompletionError>>();
let event_stream = AgentResponseEventStream(events_tx);
let user_message_ix = self.messages.len();
self.messages.push(Message::User(UserMessage {
id: message_id.clone(),
id: message_id,
content,
}));
log::info!("Total messages in thread: {}", self.messages.len());
self.running_turn = Some(cx.spawn(async move |this, cx| {
self.running_turn = Some(cx.spawn(async move |thread, cx| {
log::info!("Starting agent turn execution");
let turn_result = async {
// Perform one request, then keep looping if the model makes tool calls.
let mut completion_intent = CompletionIntent::UserPrompt;
loop {
'outer: loop {
log::debug!(
"Building completion request with intent: {:?}",
completion_intent
);
let request = this.update(cx, |this, cx| {
this.build_completion_request(completion_intent, cx)
let request = thread.update(cx, |thread, cx| {
thread.build_completion_request(completion_intent, cx)
})?;
// Stream events, appending to messages and collecting up tool uses.
log::info!("Calling model.stream_completion");
let mut events = model.stream_completion(request, cx).await?;
log::debug!("Stream completion started successfully");
let mut tool_uses = FuturesUnordered::new();
while let Some(event) = events.next().await {
match event? {
LanguageModelCompletionEvent::Stop(reason) => {
match event {
Ok(LanguageModelCompletionEvent::Stop(reason)) => {
event_stream.send_stop(reason);
if reason == StopReason::Refusal {
this.update(cx, |this, _cx| this.truncate(message_id))??;
return Ok(());
thread.update(cx, |thread, _cx| {
thread.pending_agent_message = None;
thread.messages.truncate(user_message_ix);
})?;
break 'outer;
}
}
event => {
Ok(event) => {
log::trace!("Received completion event: {:?}", event);
this.update(cx, |this, cx| {
tool_uses.extend(this.handle_streamed_completion_event(
event,
&event_stream,
cx,
));
})
.ok();
thread
.update(cx, |thread, cx| {
tool_uses.extend(thread.handle_streamed_completion_event(
event,
&event_stream,
cx,
));
})
.ok();
}
Err(error) => {
log::error!("Error in completion stream: {:?}", error);
event_stream.send_error(error);
break;
}
}
}
// If there are no tool uses, the turn is done.
if tool_uses.is_empty() {
log::info!("No tool uses found, completing turn");
return Ok(());
break;
}
log::info!("Found {} tool uses to execute", tool_uses.len());
// As tool results trickle in, insert them in the last user
// message so that they can be sent on the next tick of the
// agentic loop.
while let Some(tool_result) = tool_uses.next().await {
log::info!("Tool finished {:?}", tool_result);
@@ -588,21 +553,29 @@ impl Thread {
..Default::default()
},
);
this.update(cx, |this, _cx| {
this.pending_message()
.tool_results
.insert(tool_result.tool_use_id.clone(), tool_result);
})
.ok();
thread
.update(cx, |thread, _cx| {
thread
.pending_agent_message()
.tool_results
.insert(tool_result.tool_use_id.clone(), tool_result);
})
.ok();
}
this.update(cx, |this, _| this.flush_pending_message())?;
thread.update(cx, |thread, _cx| thread.flush_pending_agent_message())?;
completion_intent = CompletionIntent::ToolResults;
}
Ok(())
}
.await;
this.update(cx, |this, _| this.flush_pending_message()).ok();
thread
.update(cx, |thread, _cx| thread.flush_pending_agent_message())
.ok();
if let Err(error) = turn_result {
log::error!("Turn execution failed: {:?}", error);
event_stream.send_error(error);
@@ -644,8 +617,7 @@ impl Thread {
match event {
StartMessage { .. } => {
self.flush_pending_message();
self.pending_message = Some(AgentMessage::default());
self.messages.push(Message::Agent(AgentMessage::default()));
}
Text(new_text) => self.handle_text_event(new_text, event_stream, cx),
Thinking { text, signature } => {
@@ -683,7 +655,7 @@ impl Thread {
) {
events_stream.send_text(&new_text);
let last_message = self.pending_message();
let last_message = self.pending_agent_message();
if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() {
text.push_str(&new_text);
} else {
@@ -704,7 +676,7 @@ impl Thread {
) {
event_stream.send_thinking(&new_text);
let last_message = self.pending_message();
let last_message = self.pending_agent_message();
if let Some(AgentMessageContent::Thinking { text, signature }) =
last_message.content.last_mut()
{
@@ -721,7 +693,7 @@ impl Thread {
}
fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context<Self>) {
let last_message = self.pending_message();
let last_message = self.pending_agent_message();
last_message
.content
.push(AgentMessageContent::RedactedThinking(data));
@@ -745,7 +717,7 @@ impl Thread {
}
// Ensure the last message ends in the current tool use
let last_message = self.pending_message();
let last_message = self.pending_agent_message();
let push_new_tool_use = last_message.content.last_mut().map_or(true, |content| {
if let AgentMessageContent::ToolUse(last_tool_use) = content {
if last_tool_use.id == tool_use.id {
@@ -848,12 +820,12 @@ impl Thread {
}
}
fn pending_message(&mut self) -> &mut AgentMessage {
self.pending_message.get_or_insert_default()
fn pending_agent_message(&mut self) -> &mut AgentMessage {
self.pending_agent_message.get_or_insert_default()
}
fn flush_pending_message(&mut self) {
let Some(mut message) = self.pending_message.take() else {
fn flush_pending_agent_message(&mut self) {
let Some(mut message) = self.pending_agent_message.take() else {
return;
};
@@ -974,7 +946,7 @@ impl Thread {
}
}
if let Some(message) = self.pending_message.as_ref() {
if let Some(message) = self.pending_agent_message.as_ref() {
messages.extend(message.to_request());
}
@@ -990,7 +962,7 @@ impl Thread {
markdown.push_str(&message.to_markdown());
}
if let Some(message) = self.pending_message.as_ref() {
if let Some(message) = self.pending_agent_message.as_ref() {
markdown.push('\n');
markdown.push_str(&message.to_markdown());
}
@@ -1395,6 +1367,18 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
}
}
fn codeblock_tag(full_path: &Path) -> String {
let mut result = String::new();
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
let _ = write!(result, "{} ", extension);
}
let _ = write!(result, "{}", full_path.display());
result
}
impl From<&str> for UserMessageContent {
fn from(text: &str) -> Self {
Self::Text(text.into())

View File

@@ -423,7 +423,7 @@ impl AgentConnection for AcpConnection {
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut App,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
let task = self.connection.request_any(
acp_old::InitializeParams {

View File

@@ -111,7 +111,7 @@ impl AgentConnection for AcpConnection {
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();

View File

@@ -74,7 +74,7 @@ impl AgentConnection for ClaudeAgentConnection {
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
let cwd = cwd.to_owned();
cx.spawn(async move |cx| {

View File

@@ -422,8 +422,8 @@ pub async fn new_test_thread(
.await
.unwrap();
let thread = cx
.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx))
let thread = connection
.new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
.await
.unwrap();

View File

@@ -93,7 +93,6 @@ time.workspace = true
time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
@@ -103,9 +102,6 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
assistant_context = { workspace = true, features = ["test-support"] }
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }

View File

@@ -1,5 +1,4 @@
mod completion_provider;
mod entry_view_state;
mod message_editor;
mod model_selector;
mod model_selector_popover;

File diff suppressed because it is too large Load Diff

View File

@@ -1,351 +0,0 @@
use std::{collections::HashMap, ops::Range};
use acp_thread::AcpThread;
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use settings::Settings as _;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::TextSize;
use workspace::Workspace;
#[derive(Default)]
pub struct EntryViewState {
entries: Vec<Entry>,
}
impl EntryViewState {
pub fn entry(&self, index: usize) -> Option<&Entry> {
self.entries.get(index)
}
pub fn sync_entry(
&mut self,
workspace: WeakEntity<Workspace>,
thread: Entity<AcpThread>,
index: usize,
window: &mut Window,
cx: &mut App,
) {
debug_assert!(index <= self.entries.len());
let entry = if let Some(entry) = self.entries.get_mut(index) {
entry
} else {
self.entries.push(Entry::default());
self.entries.last_mut().unwrap()
};
entry.sync_diff_multibuffers(&thread, index, window, cx);
entry.sync_terminals(&workspace, &thread, index, window, cx);
}
pub fn remove(&mut self, range: Range<usize>) {
self.entries.drain(range);
}
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
for view in entry.views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
diff_editor.update(cx, |diff_editor, cx| {
diff_editor
.set_text_style_refinement(diff_editor_text_style_refinement(cx));
cx.notify();
})
}
}
}
}
}
pub struct Entry {
views: HashMap<EntityId, AnyEntity>,
}
impl Entry {
pub fn editor_for_diff(&self, diff: &Entity<MultiBuffer>) -> Option<Entity<Editor>> {
self.views
.get(&diff.entity_id())
.cloned()
.map(|entity| entity.downcast::<Editor>().unwrap())
}
pub fn terminal(
&self,
terminal: &Entity<acp_thread::Terminal>,
) -> Option<Entity<TerminalView>> {
self.views
.get(&terminal.entity_id())
.cloned()
.map(|entity| entity.downcast::<TerminalView>().unwrap())
}
fn sync_diff_multibuffers(
&mut self,
thread: &Entity<AcpThread>,
index: usize,
window: &mut Window,
cx: &mut App,
) {
let Some(entry) = thread.read(cx).entries().get(index) else {
return;
};
let multibuffers = entry
.diffs()
.map(|diff| diff.read(cx).multibuffer().clone());
let multibuffers = multibuffers.collect::<Vec<_>>();
for multibuffer in multibuffers {
if self.views.contains_key(&multibuffer.entity_id()) {
return;
}
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: true,
},
multibuffer.clone(),
None,
window,
cx,
);
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_expand_excerpt_buttons(cx);
editor.set_show_vertical_scrollbar(false, cx);
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
editor.set_soft_wrap_mode(SoftWrap::None, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
editor
});
let entity_id = multibuffer.entity_id();
self.views.insert(entity_id, editor.into_any());
}
}
fn sync_terminals(
&mut self,
workspace: &WeakEntity<Workspace>,
thread: &Entity<AcpThread>,
index: usize,
window: &mut Window,
cx: &mut App,
) {
let Some(entry) = thread.read(cx).entries().get(index) else {
return;
};
let terminals = entry
.terminals()
.map(|terminal| terminal.clone())
.collect::<Vec<_>>();
for terminal in terminals {
if self.views.contains_key(&terminal.entity_id()) {
return;
}
let Some(strong_workspace) = workspace.upgrade() else {
return;
};
let terminal_view = cx.new(|cx| {
let mut view = TerminalView::new(
terminal.read(cx).inner().clone(),
workspace.clone(),
None,
strong_workspace.read(cx).project().downgrade(),
window,
cx,
);
view.set_embedded_mode(Some(1000), cx);
view
});
let entity_id = terminal.entity_id();
self.views.insert(entity_id, terminal_view.into_any());
}
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.views.len()
}
}
fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
TextStyleRefinement {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.into(),
),
..Default::default()
}
}
impl Default for Entry {
fn default() -> Self {
Self {
// Avoid allocating in the heap by default
views: HashMap::with_capacity(0),
}
}
}
#[cfg(test)]
mod tests {
use std::{path::Path, rc::Rc};
use acp_thread::{AgentConnection, StubAgentConnection};
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::{EditorSettings, RowInfo};
use fs::FakeFs;
use gpui::{SemanticVersion, TestAppContext};
use multi_buffer::MultiBufferRow;
use pretty_assertions::assert_matches;
use project::Project;
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use theme::ThemeSettings;
use util::path;
use workspace::Workspace;
use crate::acp::entry_view_state::EntryViewState;
#[gpui::test]
async fn test_diff_sync(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
"hello.txt": "hi world"
}),
)
.await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let tool_call = acp::ToolCall {
id: acp::ToolCallId("tool".into()),
title: "Tool call".into(),
kind: acp::ToolKind::Other,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/hello.txt".into(),
old_text: Some("hi world".into()),
new_text: "hello world".into(),
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
};
let connection = Rc::new(StubAgentConnection::new());
let thread = cx
.update(|_, cx| {
connection
.clone()
.new_thread(project, Path::new(path!("/project")), cx)
})
.await
.unwrap();
let session_id = thread.update(cx, |thread, _| thread.session_id().clone());
cx.update(|_, cx| {
connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
});
let mut view_state = EntryViewState::default();
cx.update(|window, cx| {
view_state.sync_entry(workspace.downgrade(), thread.clone(), 0, window, cx);
});
let multibuffer = thread.read_with(cx, |thread, cx| {
thread
.entries()
.get(0)
.unwrap()
.diffs()
.next()
.unwrap()
.read(cx)
.multibuffer()
.clone()
});
cx.run_until_parked();
let entry = view_state.entry(0).unwrap();
let diff_editor = entry.editor_for_diff(&multibuffer).unwrap();
assert_eq!(
diff_editor.read_with(cx, |editor, cx| editor.text(cx)),
"hi world\nhello world"
);
let row_infos = diff_editor.read_with(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx);
multibuffer
.snapshot(cx)
.row_infos(MultiBufferRow(0))
.collect::<Vec<_>>()
});
assert_matches!(
row_infos.as_slice(),
[
RowInfo {
multibuffer_row: Some(MultiBufferRow(0)),
diff_status: Some(DiffHunkStatus {
kind: DiffHunkStatusKind::Deleted,
..
}),
..
},
RowInfo {
multibuffer_row: Some(MultiBufferRow(1)),
diff_status: Some(DiffHunkStatus {
kind: DiffHunkStatusKind::Added,
..
}),
..
}
]
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});
}
}

View File

@@ -1,55 +1,37 @@
use crate::acp::completion_provider::ContextPickerCompletionProvider;
use crate::acp::completion_provider::MentionImage;
use crate::acp::completion_provider::MentionSet;
use acp_thread::MentionUri;
use agent::TextThreadStore;
use agent::ThreadStore;
use agent_client_protocol as acp;
use anyhow::Result;
use collections::HashSet;
use editor::ExcerptId;
use editor::actions::Paste;
use editor::display_map::CreaseId;
use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
EditorStyle, MultiBuffer,
};
use futures::FutureExt as _;
use gpui::ClipboardEntry;
use gpui::Image;
use gpui::ImageFormat;
use file_icons::FileIcons;
use gpui::{
AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity,
};
use language::Buffer;
use language::Language;
use language_model::LanguageModelImage;
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
use settings::Settings;
use std::fmt::Write;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::IconName;
use ui::SharedString;
use ui::{
ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize,
Window, div,
ActiveTheme, App, IconName, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, Styled, TextSize, Window, div,
};
use util::ResultExt;
use workspace::Workspace;
use workspace::notifications::NotifyResultExt as _;
use zed_actions::agent::Chat;
use super::completion_provider::Mention;
pub struct MessageEditor {
editor: Entity<Editor>,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
mention_set: Arc<Mutex<MentionSet>>,
}
@@ -64,8 +46,6 @@ impl MessageEditor {
pub fn new(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
mode: EditorMode,
window: &mut Window,
cx: &mut Context<Self>,
@@ -91,8 +71,6 @@ impl MessageEditor {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
mention_set.clone(),
workspace,
thread_store.downgrade(),
text_thread_store.downgrade(),
cx.weak_entity(),
))));
editor.set_context_menu_options(ContextMenuOptions {
@@ -107,8 +85,6 @@ impl MessageEditor {
editor,
project,
mention_set,
thread_store,
text_thread_store,
}
}
@@ -116,18 +92,8 @@ impl MessageEditor {
self.editor.read(cx).is_empty(cx)
}
pub fn contents(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Vec<acp::ContentBlock>>> {
let contents = self.mention_set.lock().contents(
self.project.clone(),
self.thread_store.clone(),
self.text_thread_store.clone(),
window,
cx,
);
pub fn contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> {
let contents = self.mention_set.lock().contents(self.project.clone(), cx);
let editor = self.editor.clone();
cx.spawn(async move |_, cx| {
@@ -145,41 +111,23 @@ impl MessageEditor {
continue;
}
let Some(mention) = contents.get(&crease_id) else {
continue;
};
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
chunks.push(text[ix..crease_range.start].into());
if let Some(mention) = contents.get(&crease_id) {
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
chunks.push(text[ix..crease_range.start].into());
}
chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: mention.content.clone(),
uri: mention.uri.to_uri(),
},
),
}));
ix = crease_range.end;
}
let chunk = match mention {
Mention::Text { uri, content } => {
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
},
),
})
}
Mention::Image(mention_image) => {
acp::ContentBlock::Image(acp::ImageContent {
annotations: None,
data: mention_image.data.to_string(),
mime_type: mention_image.format.mime_type().into(),
uri: mention_image
.abs_path
.as_ref()
.map(|path| format!("file://{}", path.display())),
})
}
};
chunks.push(chunk);
ix = crease_range.end;
}
if ix < text.len() {
@@ -210,56 +158,6 @@ impl MessageEditor {
cx.emit(MessageEditorEvent::Cancel)
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
let images = cx
.read_from_clipboard()
.map(|item| {
item.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
Some(image)
} else {
None
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if images.is_empty() {
return;
}
cx.stop_propagation();
let replacement_text = "image";
for image in images {
let (excerpt_id, anchor) = self.editor.update(cx, |message_editor, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, snapshot) = snapshot.buffer_snapshot.as_singleton().unwrap();
let anchor = snapshot.anchor_before(snapshot.len());
message_editor.edit(
[(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
format!("{replacement_text} "),
)],
cx,
);
(*excerpt_id, anchor)
});
self.insert_image(
excerpt_id,
anchor,
replacement_text.len(),
Arc::new(image),
None,
window,
cx,
);
}
}
pub fn insert_dragged_files(
&self,
paths: Vec<project::ProjectPath>,
@@ -287,7 +185,7 @@ impl MessageEditor {
.unwrap_or(path.path.as_os_str())
.display()
.to_string();
let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
let completion = ContextPickerCompletionProvider::completion_for_path(
path,
&path_prefix,
false,
@@ -298,9 +196,7 @@ impl MessageEditor {
self.mention_set.clone(),
self.project.clone(),
cx,
) else {
continue;
};
);
self.editor.update(cx, |message_editor, cx| {
message_editor.edit(
@@ -317,68 +213,6 @@ impl MessageEditor {
}
}
fn insert_image(
&mut self,
excerpt_id: ExcerptId,
crease_start: text::Anchor,
content_len: usize,
image: Arc<Image>,
abs_path: Option<Arc<Path>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(crease_id) = insert_crease_for_image(
excerpt_id,
crease_start,
content_len,
self.editor.clone(),
window,
cx,
) else {
return;
};
self.editor.update(cx, |_editor, cx| {
let format = image.format;
let convert = LanguageModelImage::from_image(image, cx);
let task = cx
.spawn_in(window, async move |editor, cx| {
if let Some(image) = convert.await {
Ok(MentionImage {
abs_path,
data: image.source,
format,
})
} else {
editor
.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let Some(anchor) =
snapshot.anchor_in_excerpt(excerpt_id, crease_start)
else {
return;
};
editor.display_map.update(cx, |display_map, cx| {
display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
});
editor.remove_creases([crease_id], cx);
})
.ok();
Err("Failed to convert image".to_string())
}
})
.shared();
cx.spawn_in(window, {
let task = task.clone();
async move |_, cx| task.clone().await.notify_async_err(cx)
})
.detach();
self.mention_set.lock().insert_image(crease_id, task);
});
}
pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.set_mode(mode);
@@ -388,13 +222,12 @@ impl MessageEditor {
pub fn set_message(
&mut self,
message: Vec<acp::ContentBlock>,
message: &[acp::ContentBlock],
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut text = String::new();
let mut mentions = Vec::new();
let mut images = Vec::new();
for chunk in message {
match chunk {
@@ -405,20 +238,19 @@ impl MessageEditor {
resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
..
}) => {
if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
if let Some(mention) = MentionUri::parse(&resource.uri).log_err() {
let project_path = self
.project
.read(cx)
.project_path_for_absolute_path(&abs_path, cx);
let start = text.len();
write!(&mut text, "{}", mention_uri.as_link()).ok();
write!(text, "{}", mention.as_link());
let end = text.len();
mentions.push((start..end, mention_uri));
mentions.push((start..end, project_path, filename));
}
}
acp::ContentBlock::Image(content) => {
let start = text.len();
text.push_str("image");
let end = text.len();
images.push((start..end, content));
}
acp::ContentBlock::Audio(_)
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_)
| acp::ContentBlock::ResourceLink(_) => {}
}
@@ -430,64 +262,31 @@ impl MessageEditor {
});
self.mention_set.lock().clear();
for (range, mention_uri) in mentions {
let anchor = snapshot.anchor_before(range.start);
let crease_id = crate::context_picker::insert_crease_for_mention(
anchor.excerpt_id,
anchor.text_anchor,
range.end - range.start,
mention_uri.name().into(),
mention_uri.icon_path(cx),
self.editor.clone(),
window,
cx,
);
if let Some(crease_id) = crease_id {
self.mention_set.lock().insert_uri(crease_id, mention_uri);
}
}
for (range, content) in images {
let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else {
continue;
for (range, project_path, filename) in mentions {
let crease_icon_path = if project_path.path.is_dir() {
FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
.unwrap_or_else(|| IconName::File.path().into())
};
let anchor = snapshot.anchor_before(range.start);
let abs_path = content
.uri
.as_ref()
.and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into()));
let name = content
.uri
.as_ref()
.and_then(|uri| {
uri.strip_prefix("file://")
.and_then(|path| Path::new(path).file_name())
})
.map(|name| name.to_string_lossy().to_string())
.unwrap_or("Image".to_owned());
let crease_id = crate::context_picker::insert_crease_for_mention(
anchor.excerpt_id,
anchor.text_anchor,
range.end - range.start,
name.into(),
IconName::Image.path().into(),
self.editor.clone(),
window,
cx,
);
let data: SharedString = content.data.to_string().into();
if let Some(crease_id) = crease_id {
self.mention_set.lock().insert_image(
crease_id,
Task::ready(Ok(MentionImage {
abs_path,
data,
format,
}))
.shared(),
if let Some(project_path) = self.project.read(cx).absolute_path(&project_path, cx) {
let crease_id = crate::context_picker::insert_crease_for_mention(
anchor.excerpt_id,
anchor.text_anchor,
range.end - range.start,
filename,
crease_icon_path,
self.editor.clone(),
window,
cx,
);
if let Some(crease_id) = crease_id {
self.mention_set.lock().insert(crease_id, project_path);
}
}
}
cx.notify();
@@ -513,7 +312,6 @@ impl Render for MessageEditor {
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::cancel))
.capture_action(cx.listener(Self::paste))
.flex_1()
.child({
let settings = ThemeSettings::get_global(cx);
@@ -546,31 +344,10 @@ impl Render for MessageEditor {
}
}
pub(crate) fn insert_crease_for_image(
excerpt_id: ExcerptId,
anchor: text::Anchor,
content_len: usize,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> Option<CreaseId> {
crate::context_picker::insert_crease_for_mention(
excerpt_id,
anchor,
content_len,
"Image".into(),
IconName::Image.path().into(),
editor,
window,
cx,
)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol as acp;
use editor::EditorMode;
use fs::FakeFs;
@@ -594,16 +371,11 @@ mod tests {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
MessageEditor::new(
workspace.downgrade(),
project.clone(),
thread_store.clone(),
text_thread_store.clone(),
EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
@@ -627,7 +399,7 @@ mod tests {
.unwrap()
});
let completions = editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello @file ", window, cx);
editor.set_text("Hello @", window, cx);
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
let completion_provider = editor.completion_provider().unwrap();
completion_provider.completions(
@@ -672,8 +444,8 @@ mod tests {
});
let content = message_editor
.update_in(cx, |message_editor, window, cx| {
message_editor.contents(window, cx)
.update_in(cx, |message_editor, _window, cx| {
message_editor.contents(cx)
})
.await
.unwrap();

File diff suppressed because it is too large Load Diff

View File

@@ -65,8 +65,8 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
Banner, ButtonLike, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding,
PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{
@@ -969,9 +969,6 @@ impl AgentPanel {
agent: crate::ExternalAgent,
}
let thread_store = self.thread_store.clone();
let text_thread_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let server: Rc<dyn AgentServer> = match agent_choice {
Some(agent) => {
@@ -1006,15 +1003,7 @@ impl AgentPanel {
this.update_in(cx, |this, window, cx| {
let thread_view = cx.new(|cx| {
crate::acp::AcpThreadView::new(
server,
workspace.clone(),
project,
thread_store.clone(),
text_thread_store.clone(),
window,
cx,
)
crate::acp::AcpThreadView::new(server, workspace.clone(), project, window, cx)
});
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
@@ -1987,7 +1976,9 @@ impl AgentPanel {
PopoverMenu::new("agent-nav-menu")
.trigger_with_tooltip(
IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
IconButton::new("agent-nav-menu", icon)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Subtle),
{
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2124,10 +2115,9 @@ impl AgentPanel {
.pl_1()
.gap_1()
.child(match &self.active_view {
ActiveView::History | ActiveView::Configuration => div()
.pl(DynamicSpacing::Base04.rems(cx))
.child(self.render_toolbar_back_button(cx))
.into_any_element(),
ActiveView::History | ActiveView::Configuration => {
self.render_toolbar_back_button(cx).into_any_element()
}
_ => self
.render_recent_entries_menu(IconName::MenuAlt, cx)
.into_any_element(),
@@ -2165,7 +2155,33 @@ impl AgentPanel {
let new_thread_menu = PopoverMenu::new("new_thread_menu")
.trigger_with_tooltip(
IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
ButtonLike::new("new_thread_menu_btn").child(
h_flex()
.group("agent-selector")
.gap_1p5()
.child(
h_flex()
.relative()
.size_4()
.justify_center()
.child(
h_flex()
.group_hover("agent-selector", |s| s.invisible())
.child(
Icon::new(self.selected_agent.icon())
.color(Color::Muted),
),
)
.child(
h_flex()
.absolute()
.invisible()
.group_hover("agent-selector", |s| s.visible())
.child(Icon::new(IconName::Plus)),
),
)
.child(Label::new(self.selected_agent.label())),
),
{
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2383,24 +2399,15 @@ impl AgentPanel {
.size_full()
.gap(DynamicSpacing::Base08.rems(cx))
.child(match &self.active_view {
ActiveView::History | ActiveView::Configuration => div()
.pl(DynamicSpacing::Base04.rems(cx))
.child(self.render_toolbar_back_button(cx))
.into_any_element(),
ActiveView::History | ActiveView::Configuration => {
self.render_toolbar_back_button(cx).into_any_element()
}
_ => h_flex()
.h_full()
.px(DynamicSpacing::Base04.rems(cx))
.border_r_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.px_0p5()
.gap_1p5()
.child(
Icon::new(self.selected_agent.icon()).color(Color::Muted),
)
.child(Label::new(self.selected_agent.label())),
)
.child(new_thread_menu)
.into_any_element(),
})
.child(self.render_title_view(window, cx)),
@@ -2418,7 +2425,6 @@ impl AgentPanel {
.pr(DynamicSpacing::Base06.rems(cx))
.border_l_1()
.border_color(cx.theme().colors().border)
.child(new_thread_menu)
.child(self.render_recent_entries_menu(IconName::HistoryRerun, cx))
.child(self.render_panel_options_menu(window, cx)),
),

View File

@@ -1,16 +1,15 @@
mod completion_provider;
pub(crate) mod fetch_context_picker;
mod fetch_context_picker;
pub(crate) mod file_context_picker;
pub(crate) mod rules_context_picker;
pub(crate) mod symbol_context_picker;
pub(crate) mod thread_context_picker;
mod rules_context_picker;
mod symbol_context_picker;
mod thread_context_picker;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Result, anyhow};
use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
@@ -46,7 +45,7 @@ use agent::{
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerEntry {
enum ContextPickerEntry {
Mode(ContextPickerMode),
Action(ContextPickerAction),
}
@@ -75,7 +74,7 @@ impl ContextPickerEntry {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerMode {
enum ContextPickerMode {
File,
Symbol,
Fetch,
@@ -84,7 +83,7 @@ pub(crate) enum ContextPickerMode {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerAction {
enum ContextPickerAction {
AddSelections,
}
@@ -532,7 +531,7 @@ impl ContextPicker {
return vec![];
};
recent_context_picker_entries_with_store(
recent_context_picker_entries(
context_store,
self.thread_store.clone(),
self.text_thread_store.clone(),
@@ -586,8 +585,7 @@ impl Render for ContextPicker {
})
}
}
pub(crate) enum RecentEntry {
enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<str>,
@@ -595,7 +593,7 @@ pub(crate) enum RecentEntry {
Thread(ThreadContextEntry),
}
pub(crate) fn available_context_picker_entries(
fn available_context_picker_entries(
prompt_store: &Option<Entity<PromptStore>>,
thread_store: &Option<WeakEntity<ThreadStore>>,
workspace: &Entity<Workspace>,
@@ -632,56 +630,24 @@ pub(crate) fn available_context_picker_entries(
entries
}
fn recent_context_picker_entries_with_store(
fn recent_context_picker_entries(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>,
exclude_path: Option<ProjectPath>,
cx: &App,
) -> Vec<RecentEntry> {
let project = workspace.read(cx).project();
let mut exclude_paths = context_store.read(cx).file_paths(cx);
exclude_paths.extend(exclude_path);
let exclude_paths = exclude_paths
.into_iter()
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
.collect();
let exclude_threads = context_store.read(cx).thread_ids();
recent_context_picker_entries(
thread_store,
text_thread_store,
workspace,
&exclude_paths,
exclude_threads,
cx,
)
}
pub(crate) fn recent_context_picker_entries(
thread_store: Option<WeakEntity<ThreadStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>,
exclude_paths: &HashSet<PathBuf>,
exclude_threads: &HashSet<ThreadId>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.read(cx).file_paths(cx);
current_files.extend(exclude_path);
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(_, abs_path)| {
abs_path
.as_ref()
.map_or(true, |path| !exclude_paths.contains(path.as_path()))
})
.filter(|(path, _)| !current_files.contains(path))
.take(4)
.filter_map(|(project_path, _)| {
project
@@ -693,6 +659,8 @@ pub(crate) fn recent_context_picker_entries(
}),
);
let current_threads = context_store.read(cx).thread_ids();
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
@@ -704,7 +672,7 @@ pub(crate) fn recent_context_picker_entries(
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
.filter(|(_, thread)| match thread {
ThreadContextEntry::Thread { id, .. } => {
Some(id) != active_thread_id && !exclude_threads.contains(id)
Some(id) != active_thread_id && !current_threads.contains(id)
}
ThreadContextEntry::Context { .. } => true,
})
@@ -742,7 +710,7 @@ fn add_selections_as_context(
})
}
pub(crate) fn selection_ranges(
fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {

View File

@@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
};
use crate::message_editor::ContextCreasesAddon;
@@ -787,7 +787,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
.and_then(|b| b.read(cx).file())
.map(|file| ProjectPath::from_file(file.as_ref(), cx));
let recent_entries = recent_context_picker_entries_with_store(
let recent_entries = recent_context_picker_entries(
context_store.clone(),
thread_store.clone(),
text_thread_store.clone(),

View File

@@ -11,9 +11,6 @@ workspace = true
[lib]
path = "src/assistant_context.rs"
[features]
test-support = []
[dependencies]
agent_settings.workspace = true
anyhow.workspace = true

View File

@@ -138,27 +138,6 @@ impl ContextStore {
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
Self {
contexts: Default::default(),
contexts_metadata: Default::default(),
context_server_slash_command_ids: Default::default(),
host_contexts: Default::default(),
fs: project.read(cx).fs().clone(),
languages: project.read(cx).languages().clone(),
slash_commands: Arc::default(),
telemetry: project.read(cx).client().telemetry().clone(),
_watch_updates: Task::ready(None),
client: project.read(cx).client(),
project,
project_is_shared: false,
client_subscription: None,
_project_subscriptions: Default::default(),
prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
}
}
async fn handle_advertise_contexts(
this: Entity<Self>,
envelope: TypedEnvelope<proto::AdvertiseContexts>,

View File

@@ -18,7 +18,7 @@ fn main() {}
#[cfg(target_os = "windows")]
mod windows_impl {
use std::{borrow::Cow, path::Path};
use std::path::Path;
use super::dialog::create_dialog_window;
use super::updater::perform_update;
@@ -37,9 +37,9 @@ mod windows_impl {
pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1;
pub(crate) const WM_TERMINATE: u32 = WM_USER + 2;
#[derive(Debug, Default)]
#[derive(Debug)]
struct Args {
launch: bool,
launch: Option<bool>,
}
pub(crate) fn run() -> Result<()> {
@@ -56,9 +56,9 @@ mod windows_impl {
log::info!("======= Starting Zed update =======");
let (tx, rx) = std::sync::mpsc::channel();
let hwnd = create_dialog_window(rx)?.0 as isize;
let args = parse_args(std::env::args().skip(1));
let args = parse_args();
std::thread::spawn(move || {
let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch);
let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch.unwrap_or(true));
tx.send(result).ok();
unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok();
});
@@ -83,27 +83,39 @@ mod windows_impl {
Ok(())
}
fn parse_args(input: impl IntoIterator<Item = String>) -> Args {
let mut args: Args = Args { launch: true };
let mut input = input.into_iter();
if let Some(arg) = input.next() {
let launch_arg;
if arg == "--launch" {
launch_arg = input.next().map(Cow::Owned);
} else if let Some(rest) = arg.strip_prefix("--launch=") {
launch_arg = Some(Cow::Borrowed(rest));
} else {
launch_arg = None;
}
if launch_arg.as_deref() == Some("false") {
args.launch = false;
}
fn parse_args() -> Args {
let mut result = Args { launch: None };
if let Some(candidate) = std::env::args().nth(1) {
parse_single_arg(&candidate, &mut result);
}
args
result
}
fn parse_single_arg(arg: &str, result: &mut Args) {
let Some((key, value)) = arg.strip_prefix("--").and_then(|arg| arg.split_once('=')) else {
log::error!(
"Invalid argument format: '{}'. Expected format: --key=value",
arg
);
return;
};
match key {
"launch" => parse_launch_arg(value, &mut result.launch),
_ => log::error!("Unknown argument: --{}", key),
}
}
fn parse_launch_arg(value: &str, arg: &mut Option<bool>) {
match value {
"true" => *arg = Some(true),
"false" => *arg = Some(false),
_ => log::error!(
"Invalid value for --launch: '{}'. Expected 'true' or 'false'",
value
),
}
}
pub(crate) fn show_error(mut content: String) {
@@ -123,28 +135,44 @@ mod windows_impl {
#[cfg(test)]
mod tests {
use crate::windows_impl::parse_args;
use crate::windows_impl::{Args, parse_launch_arg, parse_single_arg};
#[test]
fn test_parse_args() {
// launch can be specified via two separate arguments
assert_eq!(parse_args(["--launch".into(), "true".into()]).launch, true);
assert_eq!(
parse_args(["--launch".into(), "false".into()]).launch,
false
);
fn test_parse_launch_arg() {
let mut arg = None;
parse_launch_arg("true", &mut arg);
assert_eq!(arg, Some(true));
// launch can be specified via one single argument
assert_eq!(parse_args(["--launch=true".into()]).launch, true);
assert_eq!(parse_args(["--launch=false".into()]).launch, false);
let mut arg = None;
parse_launch_arg("false", &mut arg);
assert_eq!(arg, Some(false));
// launch defaults to true on no arguments
assert_eq!(parse_args([]).launch, true);
let mut arg = None;
parse_launch_arg("invalid", &mut arg);
assert_eq!(arg, None);
}
// launch defaults to true on invalid arguments
assert_eq!(parse_args(["--launch".into()]).launch, true);
assert_eq!(parse_args(["--launch=".into()]).launch, true);
assert_eq!(parse_args(["--launch=invalid".into()]).launch, true);
#[test]
fn test_parse_single_arg() {
let mut args = Args { launch: None };
parse_single_arg("--launch=true", &mut args);
assert_eq!(args.launch, Some(true));
let mut args = Args { launch: None };
parse_single_arg("--launch=false", &mut args);
assert_eq!(args.launch, Some(false));
let mut args = Args { launch: None };
parse_single_arg("--launch=invalid", &mut args);
assert_eq!(args.launch, None);
let mut args = Args { launch: None };
parse_single_arg("--launch", &mut args);
assert_eq!(args.launch, None);
let mut args = Args { launch: None };
parse_single_arg("--unknown", &mut args);
assert_eq!(args.launch, None);
}
}
}

View File

@@ -10,10 +10,10 @@ use client::{
};
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::StreamExt;
use futures::{FutureExt, StreamExt};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FutureExt as _,
ScreenCaptureSource, ScreenCaptureStream, Task, Timeout, WeakEntity,
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource,
ScreenCaptureStream, Task, WeakEntity,
};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
@@ -370,53 +370,57 @@ impl Room {
})?;
// Wait for client to re-establish a connection to the server.
let executor = cx.background_executor().clone();
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
if client_status.borrow().is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade() else { break };
match this.update(cx, |this, cx| this.rejoin(cx)) {
Ok(task) => {
if task.await.log_err().is_some() {
return true;
} else {
remaining_attempts -= 1;
}
}
Err(_app_dropped) => return false,
}
} else if client_status.borrow().is_signed_out() {
return false;
}
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
client_status.next().await;
}
false
};
match client_reconnection
.with_timeout(RECONNECT_TIMEOUT, &executor)
.await
{
Ok(true) => {
log::info!("successfully reconnected to room");
// If we successfully joined the room, go back around the loop
// waiting for future connection status changes.
continue;
let mut reconnection_timeout =
cx.background_executor().timer(RECONNECT_TIMEOUT).fuse();
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
if client_status.borrow().is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade() else { break };
match this.update(cx, |this, cx| this.rejoin(cx)) {
Ok(task) => {
if task.await.log_err().is_some() {
return true;
} else {
remaining_attempts -= 1;
}
}
Err(_app_dropped) => return false,
}
} else if client_status.borrow().is_signed_out() {
return false;
}
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
client_status.next().await;
}
false
}
Ok(false) => break,
Err(Timeout) => {
log::info!("room reconnection timeout expired");
break;
.fuse();
futures::pin_mut!(client_reconnection);
futures::select_biased! {
reconnected = client_reconnection => {
if reconnected {
log::info!("successfully reconnected to room");
// If we successfully joined the room, go back around the loop
// waiting for future connection status changes.
continue;
}
}
_ = reconnection_timeout => {
log::info!("room reconnection timeout expired");
}
}
}
break;
}
}

View File

@@ -21,7 +21,7 @@ use language::{
point_from_lsp, point_to_lsp,
};
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::{NodeRuntime, VersionCheck};
use parking_lot::Mutex;
use project::DisableAiSettings;
use request::StatusNotification;
@@ -349,11 +349,7 @@ impl Copilot {
this.start_copilot(true, false, cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
this.start_copilot(true, false, cx);
if let Ok(server) = this.server.as_running() {
notify_did_change_config_to_server(&server.lsp, cx)
.context("copilot setting change: did change configuration")
.log_err();
}
this.send_configuration_update(cx);
})
.detach();
this
@@ -442,6 +438,43 @@ impl Copilot {
if env.is_empty() { None } else { Some(env) }
}
fn send_configuration_update(&mut self, cx: &mut Context<Self>) {
let copilot_settings = all_language_settings(None, cx)
.edit_predictions
.copilot
.clone();
let settings = json!({
"http": {
"proxy": copilot_settings.proxy,
"proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
},
"github-enterprise": {
"uri": copilot_settings.enterprise_uri
}
});
if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
copilot_chat.update(cx, |chat, cx| {
chat.set_configuration(
copilot_chat::CopilotChatConfiguration {
enterprise_uri: copilot_settings.enterprise_uri.clone(),
},
cx,
);
});
}
if let Ok(server) = self.server.as_running() {
server
.lsp
.notify::<lsp::notification::DidChangeConfiguration>(
&lsp::DidChangeConfigurationParams { settings },
)
.log_err();
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
use fs::FakeFs;
@@ -540,9 +573,6 @@ impl Copilot {
})?
.await?;
this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))?
.context("copilot: did change configuration")?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {
local_checks_only: false,
@@ -568,6 +598,8 @@ impl Copilot {
});
cx.emit(Event::CopilotLanguageServerStarted);
this.update_sign_in_status(status, cx);
// Send configuration now that the LSP is fully started
this.send_configuration_update(cx);
}
Err(error) => {
this.server = CopilotServer::Error(error.to_string().into());
@@ -1124,41 +1156,6 @@ fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
}
}
fn notify_did_change_config_to_server(
server: &Arc<LanguageServer>,
cx: &mut Context<Copilot>,
) -> std::result::Result<(), anyhow::Error> {
let copilot_settings = all_language_settings(None, cx)
.edit_predictions
.copilot
.clone();
if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
copilot_chat.update(cx, |chat, cx| {
chat.set_configuration(
copilot_chat::CopilotChatConfiguration {
enterprise_uri: copilot_settings.enterprise_uri.clone(),
},
cx,
);
});
}
let settings = json!({
"http": {
"proxy": copilot_settings.proxy,
"proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
},
"github-enterprise": {
"uri": copilot_settings.enterprise_uri
}
});
server.notify::<lsp::notification::DidChangeConfiguration>(&lsp::DidChangeConfigurationParams {
settings,
})
}
async fn clear_copilot_dir() {
remove_matching(paths::copilot_dir(), |_| true).await
}
@@ -1172,9 +1169,8 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
const SERVER_PATH: &str =
"node_modules/@github/copilot-language-server/dist/language-server.js";
let latest_version = node_runtime
.npm_package_latest_version(PACKAGE_NAME)
.await?;
// pinning it: https://github.com/zed-industries/zed/issues/36093
const PINNED_VERSION: &str = "1.354";
let server_path = paths::copilot_dir().join(SERVER_PATH);
fs.create_dir(paths::copilot_dir()).await?;
@@ -1184,12 +1180,13 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
PACKAGE_NAME,
&server_path,
paths::copilot_dir(),
VersionStrategy::Latest(&latest_version),
&PINNED_VERSION,
VersionCheck::VersionMismatch,
)
.await;
if should_install {
node_runtime
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)])
.await?;
}

View File

@@ -273,16 +273,6 @@ pub enum UuidVersion {
V7,
}
/// Splits selection into individual lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct SplitSelectionIntoLines {
/// Keep the text selected after splitting instead of collapsing to cursors.
#[serde(default)]
pub keep_selections: bool,
}
/// Goes to the next diagnostic in the file.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = editor)]
@@ -682,6 +672,8 @@ actions!(
SortLinesCaseInsensitive,
/// Sorts selected lines case-sensitively.
SortLinesCaseSensitive,
/// Splits selection into individual lines.
SplitSelectionIntoLines,
/// Stops the language server for the current file.
StopLanguageServer,
/// Switches between source and header files.

View File

@@ -12176,8 +12176,6 @@ impl Editor {
let clipboard_text = Cow::Borrowed(text);
self.transact(window, cx, |this, window, cx| {
let had_active_edit_prediction = this.has_active_edit_prediction();
if let Some(mut clipboard_selections) = clipboard_selections {
let old_selections = this.selections.all::<usize>(cx);
let all_selections_were_entire_line =
@@ -12250,11 +12248,6 @@ impl Editor {
} else {
this.insert(&clipboard_text, window, cx);
}
let trigger_in_words =
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
});
}
@@ -13612,7 +13605,7 @@ impl Editor {
pub fn split_selection_into_lines(
&mut self,
action: &SplitSelectionIntoLines,
_: &SplitSelectionIntoLines,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -13629,21 +13622,8 @@ impl Editor {
let buffer = self.buffer.read(cx).read(cx);
for selection in selections {
for row in selection.start.row..selection.end.row {
let line_start = Point::new(row, 0);
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
if action.keep_selections {
// Keep the selection range for each line
let selection_start = if row == selection.start.row {
selection.start
} else {
line_start
};
new_selection_ranges.push(selection_start..line_end);
} else {
// Collapse to cursor at end of line
new_selection_ranges.push(line_end..line_end);
}
let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row)));
new_selection_ranges.push(cursor..cursor);
}
let is_multiline_selection = selection.start.row != selection.end.row;
@@ -13651,16 +13631,7 @@ impl Editor {
// so this action feels more ergonomic when paired with other selection operations
let should_skip_last = is_multiline_selection && selection.end.column == 0;
if !should_skip_last {
if action.keep_selections {
if is_multiline_selection {
let line_start = Point::new(selection.end.row, 0);
new_selection_ranges.push(line_start..selection.end);
} else {
new_selection_ranges.push(selection.start..selection.end);
}
} else {
new_selection_ranges.push(selection.end..selection.end);
}
new_selection_ranges.push(selection.end..selection.end);
}
}
}
@@ -15864,25 +15835,19 @@ impl Editor {
let tab_kind = match kind {
Some(GotoDefinitionKind::Implementation) => "Implementations",
Some(GotoDefinitionKind::Symbol) | None => "Definitions",
Some(GotoDefinitionKind::Declaration) => "Declarations",
Some(GotoDefinitionKind::Type) => "Types",
_ => "Definitions",
};
let title = editor
.update_in(acx, |_, _, cx| {
let target = locations
.iter()
.map(|location| {
location
.buffer
.read(cx)
.text_for_range(location.range.clone())
.collect::<String>()
})
.unique()
.take(3)
.join(", ");
format!("{tab_kind} for {target}")
let origin = locations.first().unwrap();
let buffer = origin.buffer.read(cx);
format!(
"{} for {}",
tab_kind,
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
})
.context("buffer title")?;
@@ -16078,19 +16043,19 @@ impl Editor {
}
workspace.update_in(cx, |workspace, window, cx| {
let target = locations
.iter()
let title = locations
.first()
.as_ref()
.map(|location| {
location
.buffer
.read(cx)
.text_for_range(location.range.clone())
.collect::<String>()
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unique()
.take(3)
.join(", ");
let title = format!("References to {target}");
.unwrap();
Self::open_locations_in_multibuffer(
workspace,
locations,
@@ -20235,7 +20200,6 @@ impl Editor {
);
let old_cursor_shape = self.cursor_shape;
let old_show_breadcrumbs = self.show_breadcrumbs;
{
let editor_settings = EditorSettings::get_global(cx);
@@ -20249,10 +20213,6 @@ impl Editor {
cx.emit(EditorEvent::CursorShapeChanged);
}
if old_show_breadcrumbs != self.show_breadcrumbs {
cx.emit(EditorEvent::BreadcrumbsChanged);
}
let project_settings = ProjectSettings::get_global(cx);
self.serialize_dirty_buffers =
!self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers;
@@ -22874,7 +22834,6 @@ pub enum EditorEvent {
},
Reloaded,
CursorShapeChanged,
BreadcrumbsChanged,
PushedToNavHistory {
anchor: Anchor,
is_deactivate: bool,

View File

@@ -1901,51 +1901,6 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let move_to_beg = MoveToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
};
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
// test cursor between line_start and indent_start
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
]);
});
// cursor should move to line_start
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
);
// cursor should move to indent_start
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
);
// cursor should move to back to line_start
editor.move_to_beginning_of_line(&move_to_beg, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
);
});
}
#[gpui::test]
fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -6401,7 +6356,7 @@ async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
cx.set_state(initial_state);
cx.update_editor(|e, window, cx| {
e.split_selection_into_lines(&Default::default(), window, cx)
e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
});
cx.assert_editor_state(expected_state);
}
@@ -6489,7 +6444,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA
DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
])
});
editor.split_selection_into_lines(&Default::default(), window, cx);
editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
assert_eq!(
editor.display_text(cx),
"aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
@@ -6505,7 +6460,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA
DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
])
});
editor.split_selection_into_lines(&Default::default(), window, cx);
editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
assert_eq!(
editor.display_text(cx),
"aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"

View File

@@ -1036,10 +1036,6 @@ impl Item for Editor {
f(ItemEvent::UpdateBreadcrumbs);
}
EditorEvent::BreadcrumbsChanged => {
f(ItemEvent::UpdateBreadcrumbs);
}
EditorEvent::DirtyChanged => {
f(ItemEvent::UpdateTab);
}

View File

@@ -230,7 +230,7 @@ pub fn indented_line_beginning(
if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
{
soft_line_start
} else if stop_at_indent && (display_point > indent_start || display_point == line_start) {
} else if stop_at_indent && display_point != indent_start {
indent_start
} else {
line_start

View File

@@ -121,7 +121,7 @@ smallvec.workspace = true
smol.workspace = true
strum.workspace = true
sum_tree.workspace = true
taffy = { version = "=0.9.0", features = ["debug"] }
taffy = "=0.9.0"
thiserror.workspace = true
util.workspace = true
uuid.workspace = true
@@ -305,7 +305,3 @@ path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "grid_layout"
path = "examples/grid_layout.rs"

View File

@@ -1,80 +0,0 @@
use gpui::{
App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*,
px, rgb, size,
};
// https://en.wikipedia.org/wiki/Holy_grail_(web_design)
struct HolyGrailExample {}
impl Render for HolyGrailExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let block = |color: Hsla| {
div()
.size_full()
.bg(color)
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white())
.items_center()
};
div()
.gap_1()
.grid()
.bg(rgb(0x505050))
.size(px(500.0))
.shadow_lg()
.border_1()
.size_full()
.grid_cols(5)
.grid_rows(5)
.child(
block(gpui::white())
.row_span(1)
.col_span_full()
.child("Header"),
)
.child(
block(gpui::red())
.col_span(1)
.h_56()
.child("Table of contents"),
)
.child(
block(gpui::green())
.col_span(3)
.row_span(3)
.child("Content"),
)
.child(
block(gpui::blue())
.col_span(1)
.row_span(3)
.child("AD :(")
.text_color(gpui::white()),
)
.child(
block(gpui::black())
.row_span(1)
.col_span_full()
.text_color(gpui::white())
.child("Footer"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HolyGrailExample {}),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -585,7 +585,7 @@ impl<V: 'static> Entity<V> {
cx.executor().advance_clock(advance_clock_by);
async move {
let notification = crate::util::smol_timeout(duration, rx.recv())
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
@@ -629,7 +629,7 @@ impl<V> Entity<V> {
let handle = self.downgrade();
async move {
crate::util::smol_timeout(Duration::from_secs(1), async move {
crate::util::timeout(Duration::from_secs(1), async move {
loop {
{
let cx = cx.borrow();

View File

@@ -23,7 +23,6 @@ use crate::{
MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels,
Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task,
TooltipId, Visibility, Window, WindowControlArea, point, px, size,
taffy::{CONTAINER_LAYOUT_ID_TO_DEBUG, LAYOUT_ID_TO_DEBUG},
};
use collections::HashMap;
use refineable::Refineable;
@@ -1299,23 +1298,7 @@ impl Element for Div {
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
let layout_id =
window.request_layout(style, child_layout_ids.iter().copied(), cx);
if let Some(global_id) = global_id.as_ref()
&& global_id.0.ends_with(&["api-key-editor".into()])
{
LAYOUT_ID_TO_DEBUG.with_borrow_mut(|layout_id_to_debug| {
*layout_id_to_debug = Some(layout_id)
});
}
if let Some(global_id) = global_id.as_ref()
&& global_id.0.ends_with(&["open-router-container".into()])
{
CONTAINER_LAYOUT_ID_TO_DEBUG.with_borrow_mut(|layout_id_to_debug| {
*layout_id_to_debug = Some(layout_id)
});
}
layout_id
window.request_layout(style, child_layout_ids.iter().copied(), cx)
})
},
)

View File

@@ -9,14 +9,12 @@ use refineable::Refineable;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::borrow::Cow;
use std::ops::Range;
use std::{
cmp::{self, PartialOrd},
fmt::{self, Display},
hash::Hash,
ops::{Add, Div, Mul, MulAssign, Neg, Sub},
};
use taffy::prelude::{TaffyGridLine, TaffyGridSpan};
use crate::{App, DisplayId};
@@ -3610,37 +3608,6 @@ impl From<()> for Length {
}
}
/// A location in a grid layout.
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)]
pub struct GridLocation {
/// The rows this item uses within the grid.
pub row: Range<GridPlacement>,
/// The columns this item uses within the grid.
pub column: Range<GridPlacement>,
}
/// The placement of an item within a grid layout's column or row.
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)]
pub enum GridPlacement {
/// The grid line index to place this item.
Line(i16),
/// The number of grid lines to span.
Span(u16),
/// Automatically determine the placement, equivalent to Span(1)
#[default]
Auto,
}
impl From<GridPlacement> for taffy::GridPlacement {
fn from(placement: GridPlacement) -> Self {
match placement {
GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index),
GridPlacement::Span(span) => taffy::GridPlacement::from_span(span),
GridPlacement::Auto => taffy::GridPlacement::Auto,
}
}
}
/// Provides a trait for types that can calculate half of their value.
///
/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type

View File

@@ -157,7 +157,7 @@ pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use text_system::*;
pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
pub use util::arc_cow::ArcCow;
pub use view::*;
pub use window::*;

View File

@@ -99,7 +99,7 @@ impl VSyncProvider {
// operation for the first call after the vsync thread becomes non-idle,
// but it shouldn't happen often.
if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
log::trace!("VSyncProvider::wait_for_vsync() took less time than expected");
log::warn!("VSyncProvider::wait_for_vsync() took shorter than expected");
std::thread::sleep(self.interval);
}
}

View File

@@ -7,7 +7,7 @@ use std::{
use crate::{
AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners,
CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point,
FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi,
point, quad, rems, size,
};
@@ -260,17 +260,6 @@ pub struct Style {
/// The opacity of this element
pub opacity: Option<f32>,
/// The grid columns of this element
/// Equivalent to the Tailwind `grid-cols-<number>`
pub grid_cols: Option<u16>,
/// The row span of this element
/// Equivalent to the Tailwind `grid-rows-<number>`
pub grid_rows: Option<u16>,
/// The grid location of this element
pub grid_location: Option<GridLocation>,
/// Whether to draw a red debugging outline around this element
#[cfg(debug_assertions)]
pub debug: bool,
@@ -286,13 +275,6 @@ impl Styled for StyleRefinement {
}
}
impl StyleRefinement {
/// The grid location of this element
pub fn grid_location_mut(&mut self) -> &mut GridLocation {
self.grid_location.get_or_insert_default()
}
}
/// The value of the visibility property, similar to the CSS property `visibility`
#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum Visibility {
@@ -775,9 +757,6 @@ impl Default for Style {
text: TextStyleRefinement::default(),
mouse_cursor: None,
opacity: None,
grid_rows: None,
grid_cols: None,
grid_location: None,
#[cfg(debug_assertions)]
debug: false,

View File

@@ -1,8 +1,8 @@
use crate::{
self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight,
GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement,
TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextAlign,
TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
};
pub use gpui_macros::{
border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
@@ -46,13 +46,6 @@ pub trait Styled: Sized {
self
}
/// Sets the display type of the element to `grid`.
/// [Docs](https://tailwindcss.com/docs/display)
fn grid(mut self) -> Self {
self.style().display = Some(Display::Grid);
self
}
/// Sets the whitespace of the element to `normal`.
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
fn whitespace_normal(mut self) -> Self {
@@ -647,102 +640,6 @@ pub trait Styled: Sized {
self
}
/// Sets the grid columns of this element.
fn grid_cols(mut self, cols: u16) -> Self {
self.style().grid_cols = Some(cols);
self
}
/// Sets the grid rows of this element.
fn grid_rows(mut self, rows: u16) -> Self {
self.style().grid_rows = Some(rows);
self
}
/// Sets the column start of this element.
fn col_start(mut self, start: i16) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.column.start = GridPlacement::Line(start);
self
}
/// Sets the column start of this element to auto.
fn col_start_auto(mut self) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.column.start = GridPlacement::Auto;
self
}
/// Sets the column end of this element.
fn col_end(mut self, end: i16) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.column.end = GridPlacement::Line(end);
self
}
/// Sets the column end of this element to auto.
fn col_end_auto(mut self) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.column.end = GridPlacement::Auto;
self
}
/// Sets the column span of this element.
fn col_span(mut self, span: u16) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span);
self
}
/// Sets the row span of this element.
fn col_span_full(mut self) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1);
self
}
/// Sets the row start of this element.
fn row_start(mut self, start: i16) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.row.start = GridPlacement::Line(start);
self
}
/// Sets the row start of this element to "auto"
fn row_start_auto(mut self) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.row.start = GridPlacement::Auto;
self
}
/// Sets the row end of this element.
fn row_end(mut self, end: i16) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.row.end = GridPlacement::Line(end);
self
}
/// Sets the row end of this element to "auto"
fn row_end_auto(mut self) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.row.end = GridPlacement::Auto;
self
}
/// Sets the row span of this element.
fn row_span(mut self, span: u16) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span);
self
}
/// Sets the row span of this element.
fn row_span_full(mut self) -> Self {
let grid_location = self.style().grid_location_mut();
grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1);
self
}
/// Draws a debug border around this element.
#[cfg(debug_assertions)]
fn debug(mut self) -> Self {

View File

@@ -3,7 +3,7 @@ use crate::{
};
use collections::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use std::{cell::RefCell, fmt::Debug, ops::Range};
use std::fmt::Debug;
use taffy::{
TaffyTree, TraversePartialTree as _,
geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
@@ -11,14 +11,6 @@ use taffy::{
tree::NodeId,
};
thread_local! {
pub static LAYOUT_ID_TO_DEBUG: RefCell<Option<LayoutId>> = const { RefCell::new(None) };
}
thread_local! {
pub static CONTAINER_LAYOUT_ID_TO_DEBUG: RefCell<Option<LayoutId>> = const { RefCell::new(None) };
}
type NodeMeasureFn = Box<
dyn FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>,
>;
@@ -206,14 +198,6 @@ impl TaffyLayoutEngine {
)
.expect(EXPECT_MESSAGE);
LAYOUT_ID_TO_DEBUG.with_borrow(|layout_id_to_debug| {
println!("Layout ID Debug: {:?}", layout_id_to_debug);
});
CONTAINER_LAYOUT_ID_TO_DEBUG.with_borrow(|layout_id| {
println!("Container Layout ID Debug: {:?}\n", layout_id);
});
// println!("compute_layout took {:?}", started_at.elapsed());
}
@@ -267,25 +251,6 @@ trait ToTaffy<Output> {
impl ToTaffy<taffy::style::Style> for Style {
fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
use taffy::style_helpers::{fr, length, minmax, repeat};
fn to_grid_line(
placement: &Range<crate::GridPlacement>,
) -> taffy::Line<taffy::GridPlacement> {
taffy::Line {
start: placement.start.into(),
end: placement.end.into(),
}
}
fn to_grid_repeat<T: taffy::style::CheapCloneStr>(
unit: &Option<u16>,
) -> Vec<taffy::GridTemplateComponent<T>> {
// grid-template-columns: repeat(<number>, minmax(0, 1fr));
unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])])
.unwrap_or_default()
}
taffy::style::Style {
display: self.display.into(),
overflow: self.overflow.into(),
@@ -309,19 +274,7 @@ impl ToTaffy<taffy::style::Style> for Style {
flex_basis: self.flex_basis.to_taffy(rem_size),
flex_grow: self.flex_grow,
flex_shrink: self.flex_shrink,
grid_template_rows: to_grid_repeat(&self.grid_rows),
grid_template_columns: to_grid_repeat(&self.grid_cols),
grid_row: self
.grid_location
.as_ref()
.map(|location| to_grid_line(&location.row))
.unwrap_or_default(),
grid_column: self
.grid_location
.as_ref()
.map(|location| to_grid_line(&location.column))
.unwrap_or_default(),
..Default::default()
..Default::default() // Ignore grid properties for now
}
}
}

View File

@@ -1,11 +1,13 @@
use crate::{BackgroundExecutor, Task};
use std::{
future::Future,
pin::Pin,
sync::atomic::{AtomicUsize, Ordering::SeqCst},
task,
time::Duration,
};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::SeqCst;
#[cfg(any(test, feature = "test-support"))]
use std::time::Duration;
#[cfg(any(test, feature = "test-support"))]
use futures::Future;
#[cfg(any(test, feature = "test-support"))]
use smol::future::FutureExt;
pub use util::*;
@@ -68,59 +70,8 @@ pub trait FluentBuilder {
}
}
/// Extensions for Future types that provide additional combinators and utilities.
pub trait FutureExt {
/// Requires a Future to complete before the specified duration has elapsed.
/// Similar to tokio::timeout.
fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
where
Self: Sized;
}
impl<T: Future> FutureExt for T {
fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
where
Self: Sized,
{
WithTimeout {
future: self,
timer: executor.timer(timeout),
}
}
}
pub struct WithTimeout<T> {
future: T,
timer: Task<()>,
}
#[derive(Debug, thiserror::Error)]
#[error("Timed out before future resolved")]
/// Error returned by with_timeout when the timeout duration elapsed before the future resolved
pub struct Timeout;
impl<T: Future> Future for WithTimeout<T> {
type Output = Result<T::Output, Timeout>;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll<Self::Output> {
// SAFETY: the fields of Timeout are private and we never move the future ourselves
// And its already pinned since we are being polled (all futures need to be pinned to be polled)
let this = unsafe { self.get_unchecked_mut() };
let future = unsafe { Pin::new_unchecked(&mut this.future) };
let timer = unsafe { Pin::new_unchecked(&mut this.timer) };
if let task::Poll::Ready(output) = future.poll(cx) {
task::Poll::Ready(Ok(output))
} else if timer.poll(cx).is_ready() {
task::Poll::Ready(Err(Timeout))
} else {
task::Poll::Pending
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub async fn smol_timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
where
F: Future<Output = T>,
{
@@ -129,7 +80,7 @@ where
Err(())
};
let future = async move { Ok(f.await) };
smol::future::FutureExt::race(timer, future).await
timer.race(future).await
}
/// Increment the given atomic counter if it is not zero.

View File

@@ -140,7 +140,6 @@ pub enum IconName {
Image,
Indicator,
Info,
Json,
Keyboard,
Library,
LineHeight,

View File

@@ -853,7 +853,6 @@ impl Render for ConfigurationView {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
v_flex()
.id("open-router-container")
.size_full()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:"))
@@ -873,7 +872,6 @@ impl Render for ConfigurationView {
)
.child(
h_flex()
.id("api-key-editor")
.w_full()
.my_2()
.px_2()

View File

@@ -4,7 +4,7 @@ use futures::StreamExt;
use gpui::AsyncApp;
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::json;
use smol::fs;
@@ -107,7 +107,8 @@ impl LspAdapter for CssLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
VersionStrategy::Latest(version),
&version,
Default::default(),
)
.await;

View File

@@ -12,7 +12,7 @@ use language::{
LspAdapter, LspAdapterDelegate,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -344,7 +344,8 @@ impl LspAdapter for JsonLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
VersionStrategy::Latest(version),
&version,
Default::default(),
)
.await;

View File

@@ -13,7 +13,7 @@ use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use language::{Toolchain, WorkspaceFoldersContent};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use pet_core::Configuration;
use pet_core::os_environment::Environment;
use pet_core::python_environment::PythonEnvironmentKind;
@@ -205,7 +205,8 @@ impl LspAdapter for PythonLspAdapter {
Self::SERVER_NAME.as_ref(),
&server_path,
&container_dir,
VersionStrategy::Latest(version),
&version,
Default::default(),
)
.await;

View File

@@ -5,7 +5,7 @@ use futures::StreamExt;
use gpui::AsyncApp;
use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::fs;
@@ -112,7 +112,8 @@ impl LspAdapter for TailwindLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
VersionStrategy::Latest(version),
&version,
Default::default(),
)
.await;

View File

@@ -10,7 +10,7 @@ use language::{
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, lock::RwLock, stream::StreamExt};
@@ -588,7 +588,8 @@ impl LspAdapter for TypeScriptLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
VersionStrategy::Latest(version.typescript_version.as_str()),
version.typescript_version.as_str(),
Default::default(),
)
.await;

View File

@@ -4,7 +4,7 @@ use collections::HashMap;
use gpui::AsyncApp;
use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::Value;
use std::{
@@ -115,7 +115,8 @@ impl LspAdapter for VtslsLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
VersionStrategy::Latest(&latest_version.server_version),
&latest_version.server_version,
Default::default(),
)
.await
{
@@ -128,7 +129,8 @@ impl LspAdapter for VtslsLspAdapter {
Self::TYPESCRIPT_PACKAGE_NAME,
&container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
&container_dir,
VersionStrategy::Latest(&latest_version.typescript_version),
&latest_version.typescript_version,
Default::default(),
)
.await
{

View File

@@ -6,7 +6,7 @@ use language::{
LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::Value;
use settings::{Settings, SettingsLocation};
@@ -108,7 +108,8 @@ impl LspAdapter for YamlLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
VersionStrategy::Latest(version),
&version,
Default::default(),
)
.await;

View File

@@ -318,8 +318,6 @@ impl LanguageServer {
} else {
root_path.parent().unwrap_or_else(|| Path::new("/"))
};
let root_uri = Url::from_file_path(&working_dir)
.map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?;
log::info!(
"starting language server process. binary path: {:?}, working directory: {:?}, args: {:?}",
@@ -347,6 +345,8 @@ impl LanguageServer {
let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap();
let stderr = server.stderr.take().unwrap();
let root_uri = Url::from_file_path(&working_dir)
.map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?;
let server = Self::new_internal(
server_id,
server_name,

View File

@@ -29,11 +29,13 @@ pub struct NodeBinaryOptions {
pub use_paths: Option<(PathBuf, PathBuf)>,
}
pub enum VersionStrategy<'a> {
/// Install if current version doesn't match pinned version
Pin(&'a str),
/// Install if current version is older than latest version
Latest(&'a str),
#[derive(Default)]
pub enum VersionCheck {
/// Check whether the installed and requested version have a mismatch
VersionMismatch,
/// Only check whether the currently installed version is older than the newest one
#[default]
OlderVersion,
}
#[derive(Clone)]
@@ -293,7 +295,8 @@ impl NodeRuntime {
package_name: &str,
local_executable_path: &Path,
local_package_directory: &Path,
version_strategy: VersionStrategy<'_>,
latest_version: &str,
version_check: VersionCheck,
) -> bool {
// In the case of the local system not having the package installed,
// or in the instances where we fail to parse package.json data,
@@ -314,20 +317,13 @@ impl NodeRuntime {
let Some(installed_version) = Version::parse(&installed_version).log_err() else {
return true;
};
let Some(latest_version) = Version::parse(latest_version).log_err() else {
return true;
};
match version_strategy {
VersionStrategy::Pin(pinned_version) => {
let Some(pinned_version) = Version::parse(pinned_version).log_err() else {
return true;
};
installed_version != pinned_version
}
VersionStrategy::Latest(latest_version) => {
let Some(latest_version) = Version::parse(latest_version).log_err() else {
return true;
};
installed_version < latest_version
}
match version_check {
VersionCheck::VersionMismatch => installed_version != latest_version,
VersionCheck::OlderVersion => installed_version < latest_version,
}
}
}

View File

@@ -721,7 +721,7 @@ fn render_popular_settings_section(
.items_start()
.justify_between()
.child(
v_flex().child(Label::new("Minimap")).child(
v_flex().child(Label::new("Mini Map")).child(
Label::new("See a high-level overview of your source code.")
.color(Color::Muted),
),

View File

@@ -414,7 +414,6 @@ impl GitStore {
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_get_remotes);
client.add_entity_request_handler(Self::handle_get_branches);
client.add_entity_request_handler(Self::handle_get_default_branch);
client.add_entity_request_handler(Self::handle_change_branch);
client.add_entity_request_handler(Self::handle_create_branch);
client.add_entity_request_handler(Self::handle_git_init);
@@ -1895,23 +1894,6 @@ impl GitStore {
.collect::<Vec<_>>(),
})
}
async fn handle_get_default_branch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GetDefaultBranch>,
mut cx: AsyncApp,
) -> Result<proto::GetDefaultBranchResponse> {
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
let branch = repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.default_branch()
})?
.await??
.map(Into::into);
Ok(proto::GetDefaultBranchResponse { branch })
}
async fn handle_create_branch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitCreateBranch>,
@@ -2349,7 +2331,7 @@ impl GitStore {
return None;
};
let mut paths = Vec::new();
let mut paths = vec![];
// All paths prefixed by a given repo will constitute a continuous range.
while let Some(path) = entries.get(ix)
&& let Some(repo_path) =
@@ -2358,11 +2340,7 @@ impl GitStore {
paths.push((repo_path, ix));
ix += 1;
}
if paths.is_empty() {
None
} else {
Some((repo, paths))
}
Some((repo, paths))
});
tasks.push_back(task);
}
@@ -4342,8 +4320,7 @@ impl Repository {
bail!("not a local repository")
};
let (snapshot, events) = this
.update(&mut cx, |this, _| {
this.paths_needing_status_update.clear();
.read_with(&mut cx, |this, _| {
compute_snapshot(
this.id,
this.work_directory_abs_path.clone(),
@@ -4573,9 +4550,6 @@ impl Repository {
};
let paths = changed_paths.iter().cloned().collect::<Vec<_>>();
if paths.is_empty() {
return Ok(());
}
let statuses = backend.status(&paths).await?;
let changed_path_statuses = cx

View File

@@ -390,17 +390,13 @@ impl LocalLspStore {
delegate.update_status(
adapter.name(),
BinaryStatus::Failed {
error: if log.is_empty() {
format!("{err:#}")
} else {
format!("{err:#}\n-- stderr --\n{log}")
},
error: format!("{err}\n-- stderr--\n{log}"),
},
);
log::error!("Failed to start language server {server_name:?}: {err:?}");
if !log.is_empty() {
log::error!("server stderr: {log}");
}
let message =
format!("Failed to start language server {server_name:?}: {err:#?}");
log::error!("{message}");
log::error!("server stderr: {log}");
None
}
}

View File

@@ -740,9 +740,9 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" > [EDITOR: ''] <== selected",
" > 3",
" > 4",
" > [EDITOR: ''] <== selected",
" a-different-filename.tar.gz",
" > C",
" .dockerignore",
@@ -765,10 +765,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" > [PROCESSING: 'new-dir']",
" > 3 <== selected",
" > 3",
" > 4",
" a-different-filename.tar.gz",
" > [PROCESSING: 'new-dir']",
" a-different-filename.tar.gz <== selected",
" > C",
" .dockerignore",
]
@@ -782,10 +782,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" > 3 <== selected",
" > 3",
" > 4",
" > new-dir",
" a-different-filename.tar.gz",
" a-different-filename.tar.gz <== selected",
" > C",
" .dockerignore",
]
@@ -801,10 +801,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" > [EDITOR: '3'] <== selected",
" > 3",
" > 4",
" > new-dir",
" a-different-filename.tar.gz",
" [EDITOR: 'a-different-filename.tar.gz'] <== selected",
" > C",
" .dockerignore",
]
@@ -819,10 +819,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" > 3 <== selected",
" > 3",
" > 4",
" > new-dir",
" a-different-filename.tar.gz",
" a-different-filename.tar.gz <== selected",
" > C",
" .dockerignore",
]
@@ -837,12 +837,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" v 3",
" [EDITOR: ''] <== selected",
" Q",
" > 3",
" > 4",
" > new-dir",
" [EDITOR: ''] <== selected",
" a-different-filename.tar.gz",
" > C",
]
);
panel.update_in(cx, |panel, window, cx| {
@@ -863,12 +863,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
" > .git",
" > a",
" v b",
" v 3 <== selected",
" Q",
" > 3",
" > 4",
" > new-dir",
" a-different-filename.tar.gz",
" a-different-filename.tar.gz <== selected",
" > C",
" .dockerignore",
]
);
}

View File

@@ -58,6 +58,15 @@ pub enum PromptId {
EditWorkflow,
}
impl std::fmt::Display for PromptId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptId::User { uuid } => write!(f, "{}", uuid.0),
PromptId::EditWorkflow => write!(f, "Edit workflow"),
}
}
}
impl PromptId {
pub fn new() -> PromptId {
UserPromptId::new().into()
@@ -90,15 +99,6 @@ impl From<Uuid> for UserPromptId {
}
}
impl std::fmt::Display for PromptId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptId::User { uuid } => write!(f, "{}", uuid.0),
PromptId::EditWorkflow => write!(f, "Edit workflow"),
}
}
}
pub struct PromptStore {
env: heed::Env,
metadata_cache: RwLock<MetadataCache>,

View File

@@ -42,10 +42,8 @@ tree-sitter-rust.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
vim.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
db = {"workspace"= true, "features" = ["test-support"]}

View File

@@ -23,7 +23,7 @@ use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAss
use ui::{
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,
Styled as _, Tooltip, Window, prelude::*, right_click_menu,
Styled as _, Tooltip, Window, prelude::*,
};
use ui_input::SingleLineInput;
use util::ResultExt;
@@ -1536,33 +1536,6 @@ impl Render for KeymapEditor {
.child(
h_flex()
.gap_2()
.child(
right_click_menu("open-keymap-menu")
.menu(|window, cx| {
ContextMenu::build(window, cx, |menu, _, _| {
menu.header("Open Keymap JSON")
.action("User", zed_actions::OpenKeymap.boxed_clone())
.action("Zed Default", zed_actions::OpenDefaultKeymap.boxed_clone())
.action("Vim Default", vim::OpenDefaultKeymap.boxed_clone())
})
})
.anchor(gpui::Corner::TopLeft)
.trigger(|open, _, _|
IconButton::new(
"OpenKeymapJsonButton",
IconName::Json
)
.shape(ui::IconButtonShape::Square)
.when(!open, |this|
this.tooltip(move |window, cx| {
Tooltip::with_meta("Open Keymap JSON", Some(&zed_actions::OpenKeymap),"Right click to view more options", window, cx)
})
)
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
})
)
)
.child(
div()
.key_context({

View File

@@ -167,7 +167,6 @@ enum InternalEvent {
// Vi mode events
ToggleViMode,
ViMotion(ViMotion),
MoveViCursorToAlacPoint(AlacPoint),
}
///A translation struct for Alacritty to communicate with us from their event loop
@@ -973,10 +972,6 @@ impl Terminal {
term.scroll_to_point(*point);
self.refresh_hovered_word(window);
}
InternalEvent::MoveViCursorToAlacPoint(point) => {
term.vi_goto_point(*point);
self.refresh_hovered_word(window);
}
InternalEvent::ToggleViMode => {
self.vi_mode_enabled = !self.vi_mode_enabled;
term.toggle_vi_mode();
@@ -1105,19 +1100,10 @@ impl Terminal {
pub fn activate_match(&mut self, index: usize) {
if let Some(search_match) = self.matches.get(index).cloned() {
self.set_selection(Some((make_selection(&search_match), *search_match.end())));
if self.vi_mode_enabled {
self.events
.push_back(InternalEvent::MoveViCursorToAlacPoint(*search_match.end()));
} else {
self.events
.push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
}
}
}
pub fn clear_matches(&mut self) {
self.matches.clear();
self.set_selection(None);
self.events
.push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
}
}
pub fn select_matches(&mut self, matches: &[RangeInclusive<AlacPoint>]) {

View File

@@ -1869,7 +1869,7 @@ impl SearchableItem for TerminalView {
/// Clear stored matches
fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.terminal().update(cx, |term, _| term.clear_matches())
self.terminal().update(cx, |term, _| term.matches.clear())
}
/// Store matches returned from find_matches somewhere for rendering

View File

@@ -104,19 +104,6 @@ impl<T: Copy + Ord> Selection<T> {
self.goal = new_goal;
}
pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) {
if head < tail {
self.reversed = true;
self.start = head;
self.end = tail;
} else {
self.reversed = false;
self.start = tail;
self.end = head;
}
self.goal = new_goal;
}
pub fn swap_head_tail(&mut self) {
if self.reversed {
self.reversed = false;

View File

@@ -1,7 +1,4 @@
use globset::{Glob, GlobSet, GlobSetBuilder};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::cmp;
use std::path::StripPrefixError;
use std::sync::{Arc, OnceLock};
use std::{
@@ -10,6 +7,12 @@ use std::{
sync::LazyLock,
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::NumericPrefixWithSuffix;
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -542,172 +545,17 @@ impl PathMatcher {
}
}
/// Custom character comparison that prioritizes lowercase for same letters
fn compare_chars(a: char, b: char) -> Ordering {
// First compare case-insensitive
match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) {
Ordering::Equal => {
// If same letter, prioritize lowercase (lowercase < uppercase)
match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) {
(true, false) => Ordering::Less, // lowercase comes first
(false, true) => Ordering::Greater, // uppercase comes after
_ => Ordering::Equal, // both same case or both non-ascii
}
}
other => other,
}
}
/// Compares two sequences of consecutive digits for natural sorting.
///
/// This function is a core component of natural sorting that handles numeric comparison
/// in a way that feels natural to humans. It extracts and compares consecutive digit
/// sequences from two iterators, handling various cases like leading zeros and very large numbers.
///
/// # Behavior
///
/// The function implements the following comparison rules:
/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10")
/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2")
/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128
///
/// # Examples
///
/// ```text
/// "1" vs "2" -> Less (different values)
/// "2" vs "10" -> Less (numeric comparison)
/// "002" vs "2" -> Greater (leading zeros)
/// "10" vs "010" -> Less (leading zeros)
/// "999..." vs "1000..." -> Less (large number comparison)
/// ```
///
/// # Implementation Details
///
/// 1. Extracts consecutive digits into strings
/// 2. Compares sequence lengths for leading zero handling
/// 3. For equal lengths, compares digit by digit
/// 4. For different lengths:
/// - Attempts numeric comparison first (for numbers up to 2^128 - 1)
/// - Falls back to string comparison if numbers would overflow
///
/// The function advances both iterators past their respective numeric sequences,
/// regardless of the comparison result.
fn compare_numeric_segments<I>(
a_iter: &mut std::iter::Peekable<I>,
b_iter: &mut std::iter::Peekable<I>,
) -> Ordering
where
I: Iterator<Item = char>,
{
// Collect all consecutive digits into strings
let mut a_num_str = String::new();
let mut b_num_str = String::new();
while let Some(&c) = a_iter.peek() {
if !c.is_ascii_digit() {
break;
}
a_num_str.push(c);
a_iter.next();
}
while let Some(&c) = b_iter.peek() {
if !c.is_ascii_digit() {
break;
}
b_num_str.push(c);
b_iter.next();
}
// First compare lengths (handle leading zeros)
match a_num_str.len().cmp(&b_num_str.len()) {
Ordering::Equal => {
// Same length, compare digit by digit
match a_num_str.cmp(&b_num_str) {
Ordering::Equal => Ordering::Equal,
ordering => ordering,
}
}
// Different lengths but same value means leading zeros
ordering => {
// Try parsing as numbers first
if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
match a_val.cmp(&b_val) {
Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros)
ord => ord,
}
} else {
// If parsing fails (overflow), compare as strings
a_num_str.cmp(&b_num_str)
}
}
}
}
/// Performs natural sorting comparison between two strings.
///
/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations.
/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting).
///
/// # Characteristics
///
/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase
/// * Numbers are compared by numeric value, not character by character
/// * Leading zeros affect ordering when numeric values are equal
/// * Can handle numbers larger than u128::MAX (falls back to string comparison)
///
/// # Algorithm
///
/// The function works by:
/// 1. Processing strings character by character
/// 2. When encountering digits, treating consecutive digits as a single number
/// 3. Comparing numbers by their numeric value rather than lexicographically
/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
fn natural_sort(a: &str, b: &str) -> Ordering {
let mut a_iter = a.chars().peekable();
let mut b_iter = b.chars().peekable();
loop {
match (a_iter.peek(), b_iter.peek()) {
(None, None) => return Ordering::Equal,
(None, _) => return Ordering::Less,
(_, None) => return Ordering::Greater,
(Some(&a_char), Some(&b_char)) => {
if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
match compare_numeric_segments(&mut a_iter, &mut b_iter) {
Ordering::Equal => continue,
ordering => return ordering,
}
} else {
match compare_chars(a_char, b_char) {
Ordering::Equal => {
a_iter.next();
b_iter.next();
}
ordering => return ordering,
}
}
}
}
}
}
pub fn compare_paths(
(path_a, a_is_file): (&Path, bool),
(path_b, b_is_file): (&Path, bool),
) -> Ordering {
) -> cmp::Ordering {
let mut components_a = path_a.components().peekable();
let mut components_b = path_b.components().peekable();
loop {
match (components_a.next(), components_b.next()) {
(Some(component_a), Some(component_b)) => {
let a_is_file = components_a.peek().is_none() && a_is_file;
let b_is_file = components_b.peek().is_none() && b_is_file;
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
let path_a = Path::new(component_a.as_os_str());
let path_string_a = if a_is_file {
@@ -716,6 +564,9 @@ pub fn compare_paths(
path_a.file_name()
}
.map(|s| s.to_string_lossy());
let num_and_remainder_a = path_string_a
.as_deref()
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
let path_b = Path::new(component_b.as_os_str());
let path_string_b = if b_is_file {
@@ -724,32 +575,27 @@ pub fn compare_paths(
path_b.file_name()
}
.map(|s| s.to_string_lossy());
let num_and_remainder_b = path_string_b
.as_deref()
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
let compare_components = match (path_string_a, path_string_b) {
(Some(a), Some(b)) => natural_sort(&a, &b),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
};
compare_components.then_with(|| {
num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| {
if a_is_file && b_is_file {
let ext_a = path_a.extension().unwrap_or_default();
let ext_b = path_b.extension().unwrap_or_default();
ext_a.cmp(ext_b)
} else {
Ordering::Equal
cmp::Ordering::Equal
}
})
});
if !ordering.is_eq() {
return ordering;
}
}
(Some(_), None) => break Ordering::Greater,
(None, Some(_)) => break Ordering::Less,
(None, None) => break Ordering::Equal,
(Some(_), None) => break cmp::Ordering::Greater,
(None, Some(_)) => break cmp::Ordering::Less,
(None, None) => break cmp::Ordering::Equal,
}
}
}
@@ -1203,335 +1049,4 @@ mod tests {
"C:\\Users\\someone\\test_file.rs"
);
}
#[test]
fn test_compare_numeric_segments() {
// Helper function to create peekable iterators and test
fn compare(a: &str, b: &str) -> Ordering {
let mut a_iter = a.chars().peekable();
let mut b_iter = b.chars().peekable();
let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
// Verify iterators advanced correctly
assert!(
!a_iter.next().map_or(false, |c| c.is_ascii_digit()),
"Iterator a should have consumed all digits"
);
assert!(
!b_iter.next().map_or(false, |c| c.is_ascii_digit()),
"Iterator b should have consumed all digits"
);
result
}
// Basic numeric comparisons
assert_eq!(compare("0", "0"), Ordering::Equal);
assert_eq!(compare("1", "2"), Ordering::Less);
assert_eq!(compare("9", "10"), Ordering::Less);
assert_eq!(compare("10", "9"), Ordering::Greater);
assert_eq!(compare("99", "100"), Ordering::Less);
// Leading zeros
assert_eq!(compare("0", "00"), Ordering::Less);
assert_eq!(compare("00", "0"), Ordering::Greater);
assert_eq!(compare("01", "1"), Ordering::Greater);
assert_eq!(compare("001", "1"), Ordering::Greater);
assert_eq!(compare("001", "01"), Ordering::Greater);
// Same value different representation
assert_eq!(compare("000100", "100"), Ordering::Greater);
assert_eq!(compare("100", "0100"), Ordering::Less);
assert_eq!(compare("0100", "00100"), Ordering::Less);
// Large numbers
assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
assert_eq!(
compare(
"340282366920938463463374607431768211455", // u128::MAX
"340282366920938463463374607431768211456"
),
Ordering::Less
);
assert_eq!(
compare(
"340282366920938463463374607431768211456", // > u128::MAX
"340282366920938463463374607431768211455"
),
Ordering::Greater
);
// Iterator advancement verification
let mut a_iter = "123abc".chars().peekable();
let mut b_iter = "456def".chars().peekable();
compare_numeric_segments(&mut a_iter, &mut b_iter);
assert_eq!(a_iter.collect::<String>(), "abc");
assert_eq!(b_iter.collect::<String>(), "def");
}
#[test]
fn test_natural_sort() {
// Basic alphanumeric
assert_eq!(natural_sort("a", "b"), Ordering::Less);
assert_eq!(natural_sort("b", "a"), Ordering::Greater);
assert_eq!(natural_sort("a", "a"), Ordering::Equal);
// Case sensitivity
assert_eq!(natural_sort("a", "A"), Ordering::Less);
assert_eq!(natural_sort("A", "a"), Ordering::Greater);
assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
// Numbers
assert_eq!(natural_sort("1", "2"), Ordering::Less);
assert_eq!(natural_sort("2", "10"), Ordering::Less);
assert_eq!(natural_sort("02", "10"), Ordering::Less);
assert_eq!(natural_sort("02", "2"), Ordering::Greater);
// Mixed alphanumeric
assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
// Multiple numeric segments
assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
// Special characters
assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
// Unicode
assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
// Empty and special cases
assert_eq!(natural_sort("", ""), Ordering::Equal);
assert_eq!(natural_sort("", "a"), Ordering::Less);
assert_eq!(natural_sort("a", ""), Ordering::Greater);
assert_eq!(natural_sort(" ", " "), Ordering::Less);
// Mixed everything
assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
}
#[test]
fn test_compare_paths() {
// Helper function for cleaner tests
fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
}
// Basic path comparison
assert_eq!(compare("a", true, "b", true), Ordering::Less);
assert_eq!(compare("b", true, "a", true), Ordering::Greater);
assert_eq!(compare("a", true, "a", true), Ordering::Equal);
// Files vs Directories
assert_eq!(compare("a", true, "a", false), Ordering::Greater);
assert_eq!(compare("a", false, "a", true), Ordering::Less);
assert_eq!(compare("b", false, "a", true), Ordering::Less);
// Extensions
assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
// Nested paths
assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
// Case sensitivity in paths
assert_eq!(
compare("Dir/file", true, "dir/file", true),
Ordering::Greater
);
assert_eq!(
compare("dir/File", true, "dir/file", true),
Ordering::Greater
);
assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
// Hidden files and special names
assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
// Mixed numeric paths
assert_eq!(
compare("dir1/file", true, "dir2/file", true),
Ordering::Less
);
assert_eq!(
compare("dir2/file", true, "dir10/file", true),
Ordering::Less
);
assert_eq!(
compare("dir02/file", true, "dir2/file", true),
Ordering::Greater
);
// Root paths
assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
assert_eq!(compare("/", false, "/a", true), Ordering::Less);
// Complex real-world examples
assert_eq!(
compare("project/src/main.rs", true, "project/src/lib.rs", true),
Ordering::Greater
);
assert_eq!(
compare(
"project/tests/test_1.rs",
true,
"project/tests/test_2.rs",
true
),
Ordering::Less
);
assert_eq!(
compare(
"project/v1.0.0/README.md",
true,
"project/v1.10.0/README.md",
true
),
Ordering::Less
);
}
#[test]
fn test_natural_sort_case_sensitivity() {
// Same letter different case - lowercase should come first
assert_eq!(natural_sort("a", "A"), Ordering::Less);
assert_eq!(natural_sort("A", "a"), Ordering::Greater);
assert_eq!(natural_sort("a", "a"), Ordering::Equal);
assert_eq!(natural_sort("A", "A"), Ordering::Equal);
// Mixed case strings
assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
// Different letters
assert_eq!(natural_sort("a", "b"), Ordering::Less);
assert_eq!(natural_sort("A", "b"), Ordering::Less);
assert_eq!(natural_sort("a", "B"), Ordering::Less);
}
#[test]
fn test_natural_sort_with_numbers() {
// Basic number ordering
assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
// Numbers in different positions
assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
// Multiple numbers in string
assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
// Leading zeros
assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
// Very large numbers
assert_eq!(
natural_sort("file999999999999999999999", "file999999999999999999998"),
Ordering::Greater
);
// u128 edge cases
// Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455)
assert_eq!(
natural_sort(
"file340282366920938463463374607431768211454",
"file340282366920938463463374607431768211455"
),
Ordering::Less
);
// Equal length numbers that overflow u128
assert_eq!(
natural_sort(
"file340282366920938463463374607431768211456",
"file340282366920938463463374607431768211455"
),
Ordering::Greater
);
// Different length numbers that overflow u128
assert_eq!(
natural_sort(
"file3402823669209384634633746074317682114560",
"file340282366920938463463374607431768211455"
),
Ordering::Greater
);
// Leading zeros with numbers near u128::MAX
assert_eq!(
natural_sort(
"file0340282366920938463463374607431768211455",
"file340282366920938463463374607431768211455"
),
Ordering::Greater
);
// Very large numbers with different lengths (both overflow u128)
assert_eq!(
natural_sort(
"file999999999999999999999999999999999999999999999999",
"file9999999999999999999999999999999999999999999999999"
),
Ordering::Less
);
// Mixed case with numbers
assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
}
#[test]
fn test_natural_sort_edge_cases() {
// Empty strings
assert_eq!(natural_sort("", ""), Ordering::Equal);
assert_eq!(natural_sort("", "a"), Ordering::Less);
assert_eq!(natural_sort("a", ""), Ordering::Greater);
// Special characters
assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
// Unicode characters
// 9312 vs 9313
assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
// 9321 vs 9313
assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
// 28450 vs 23383
assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
// Mixed alphanumeric with special chars
assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
}
}

View File

@@ -1,11 +1,9 @@
use editor::display_map::DisplaySnapshot;
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
use gpui::{Action, actions};
use gpui::{Context, Window};
use language::{CharClassifier, CharKind};
use text::{Bias, SelectionGoal};
use crate::motion;
use crate::{
Vim,
motion::{Motion, right},
@@ -17,8 +15,6 @@ actions!(
[
/// Switches to normal mode after the cursor (Helix-style).
HelixNormalAfter,
/// Yanks the current selection or character if no selection.
HelixYank,
/// Inserts at the beginning of the selection.
HelixInsert,
/// Appends at the end of the selection.
@@ -30,7 +26,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_normal_after);
Vim::action(editor, cx, Vim::helix_insert);
Vim::action(editor, cx, Vim::helix_append);
Vim::action(editor, cx, Vim::helix_yank);
}
impl Vim {
@@ -60,35 +55,6 @@ impl Vim {
self.helix_move_cursor(motion, times, window, cx);
}
/// Updates all selections based on where the cursors are.
fn helix_new_selections(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
mut change: impl FnMut(
// the start of the cursor
DisplayPoint,
&DisplaySnapshot,
) -> Option<(DisplayPoint, DisplayPoint)>,
) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor_start = if selection.reversed || selection.is_empty() {
selection.head()
} else {
movement::left(map, selection.head())
};
let Some((head, tail)) = change(cursor_start, map) else {
return;
};
selection.set_head_tail(head, tail, SelectionGoal::None);
});
});
});
}
fn helix_find_range_forward(
&mut self,
times: Option<usize>,
@@ -96,30 +62,49 @@ impl Vim {
cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
let times = times.unwrap_or(1);
self.helix_new_selections(window, cx, |cursor, map| {
let mut head = movement::right(map, cursor);
let mut tail = cursor;
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
if head == map.max_point() {
return None;
}
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
let new_goal = SelectionGoal::None;
let mut head = selection.head();
let mut tail = selection.tail();
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
if head == map.max_point() {
return;
}
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
Some((head, tail))
// collapse to block cursor
if tail < head {
tail = movement::left(map, head);
} else {
tail = head;
head = movement::right(map, head);
}
// create a classifier
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
selection.set_tail(tail, new_goal);
selection.set_head(head, new_goal);
});
});
});
}
@@ -130,33 +115,56 @@ impl Vim {
cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
let times = times.unwrap_or(1);
self.helix_new_selections(window, cx, |cursor, map| {
let mut head = cursor;
// The original cursor was one character wide,
// but the search starts from the left side of it,
// so to include that space the selection must end one character to the right.
let mut tail = movement::right(map, cursor);
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
if head == DisplayPoint::zero() {
return None;
}
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_preceding_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
let new_goal = SelectionGoal::None;
let mut head = selection.head();
let mut tail = selection.tail();
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
if head == DisplayPoint::zero() {
return;
}
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
Some((head, tail))
// collapse to block cursor
if tail < head {
tail = movement::left(map, head);
} else {
tail = head;
head = movement::right(map, head);
}
selection.set_head(head, new_goal);
selection.set_tail(tail, new_goal);
// flip the selection
selection.swap_head_tail();
head = selection.head();
tail = selection.tail();
// create a classifier
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_preceding_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
selection.set_tail(tail, new_goal);
selection.set_head(head, new_goal);
});
})
});
}
@@ -244,100 +252,64 @@ impl Vim {
found
})
}
Motion::FindForward {
before,
char,
mode,
smartcase,
} => {
self.helix_new_selections(window, cx, |cursor, map| {
let start = cursor;
let mut last_boundary = start;
for _ in 0..times.unwrap_or(1) {
last_boundary = movement::find_boundary(
map,
movement::right(map, last_boundary),
mode,
|left, right| {
let current_char = if before { right } else { left };
motion::is_character_match(char, current_char, smartcase)
},
);
}
Some((last_boundary, start))
Motion::FindForward { .. } => {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
selection.head()
} else {
movement::left(map, selection.head())
};
let (point, goal) = motion
.move_point(
map,
cursor,
selection.goal,
times,
&text_layout_details,
)
.unwrap_or((cursor, goal));
selection.set_tail(selection.head(), goal);
selection.set_head(movement::right(map, point), goal);
})
});
});
}
Motion::FindBackward {
after,
char,
mode,
smartcase,
} => {
self.helix_new_selections(window, cx, |cursor, map| {
let start = cursor;
let mut last_boundary = start;
for _ in 0..times.unwrap_or(1) {
last_boundary = movement::find_preceding_boundary_display_point(
map,
last_boundary,
mode,
|left, right| {
let current_char = if after { left } else { right };
motion::is_character_match(char, current_char, smartcase)
},
);
}
// The original cursor was one character wide,
// but the search started from the left side of it,
// so to include that space the selection must end one character to the right.
Some((last_boundary, movement::right(map, start)))
Motion::FindBackward { .. } => {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
selection.head()
} else {
movement::left(map, selection.head())
};
let (point, goal) = motion
.move_point(
map,
cursor,
selection.goal,
times,
&text_layout_details,
)
.unwrap_or((cursor, goal));
selection.set_tail(selection.head(), goal);
selection.set_head(point, goal);
})
});
});
}
_ => self.helix_move_and_collapse(motion, times, window, cx),
}
}
pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(cx, |vim, editor, cx| {
let has_selection = editor
.selections
.all_adjusted(cx)
.iter()
.any(|selection| !selection.is_empty());
if !has_selection {
// If no selection, expand to current character (like 'v' does)
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let head = selection.head();
let new_head = movement::saturating_right(map, head);
selection.set_tail(head, SelectionGoal::None);
selection.set_head(new_head, SelectionGoal::None);
});
});
vim.yank_selections_content(
editor,
crate::motion::MotionKind::Exclusive,
window,
cx,
);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_map, selection| {
selection.collapse_to(selection.start, SelectionGoal::None);
});
});
} else {
// Yank the selection(s)
vim.yank_selections_content(
editor,
crate::motion::MotionKind::Exclusive,
window,
cx,
);
}
});
}
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.update_editor(cx, |_, editor, cx| {
@@ -614,33 +586,13 @@ mod test {
Mode::HelixNormal,
);
cx.simulate_keystrokes("F e F e");
cx.simulate_keystrokes("2 T r");
cx.assert_state(
indoc! {"
The quick brown
fox jumps ov«ˇer
the» lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("e 2 F e");
cx.assert_state(
indoc! {"
Th«ˇe quick brown
fox jumps over»
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("t r t r");
cx.assert_state(
indoc! {"
The quick «brown
fox jumps oveˇ»r
the lazy dog."},
The quick br«ˇown
fox jumps over
the laz»y dog."},
Mode::HelixNormal,
);
}
@@ -751,29 +703,4 @@ mod test {
cx.assert_state("«xxˇ»", Mode::HelixNormal);
}
#[gpui::test]
async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// Test yanking current character with no selection
cx.set_state("hello ˇworld", Mode::HelixNormal);
cx.simulate_keystrokes("y");
// Test cursor remains at the same position after yanking single character
cx.assert_state("hello ˇworld", Mode::HelixNormal);
cx.shared_clipboard().assert_eq("w");
// Move cursor and yank another character
cx.simulate_keystrokes("l");
cx.simulate_keystrokes("y");
cx.shared_clipboard().assert_eq("o");
// Test yanking with existing selection
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
cx.simulate_keystrokes("y");
cx.shared_clipboard().assert_eq("worl");
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
}
}

View File

@@ -2639,8 +2639,7 @@ fn find_backward(
}
}
/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
if smartcase {
if target.is_uppercase() {
target == other

View File

@@ -143,16 +143,6 @@ impl VimTestContext {
})
}
pub fn enable_helix(&mut self) {
self.cx.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<vim_mode_setting::HelixModeSetting>(cx, |s| {
*s = Some(true)
});
});
})
}
pub fn mode(&mut self) -> Mode {
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
}
@@ -220,26 +210,6 @@ impl VimTestContext {
assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
}
pub fn shared_clipboard(&mut self) -> VimClipboard {
VimClipboard {
editor: self
.read_from_clipboard()
.map(|item| item.text().unwrap().to_string())
.unwrap_or_default(),
}
}
}
pub struct VimClipboard {
editor: String,
}
impl VimClipboard {
#[track_caller]
pub fn assert_eq(&self, expected: &str) {
assert_eq!(self.editor, expected);
}
}
impl Deref for VimTestContext {

View File

@@ -6664,15 +6664,25 @@ impl Render for Workspace {
}
})
.children(self.zoomed.as_ref().and_then(|view| {
Some(div()
let zoomed_view = view.upgrade()?;
let div = div()
.occlude()
.absolute()
.overflow_hidden()
.border_color(colors.border)
.bg(colors.background)
.child(view.upgrade()?)
.child(zoomed_view)
.inset_0()
.shadow_lg())
.shadow_lg();
Some(match self.zoomed_position {
Some(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => {
div.top_2().bottom_2().left_2().right_2().border_1()
}
})
}))
.children(self.render_notifications(window, cx)),
)

View File

@@ -35,7 +35,6 @@ pub fn app_menus() -> Vec<Menu> {
],
}),
MenuItem::separator(),
#[cfg(target_os = "macos")]
MenuItem::os_submenu("Services", gpui::SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action("Extensions", zed_actions::Extensions::default()),

View File

@@ -392,26 +392,26 @@ Zed will also use the `OPENAI_API_KEY` environment variable if it's defined.
#### Custom Models {#openai-custom-models}
The Zed agent comes pre-configured to use the latest version for common models (GPT-5, GPT-5 mini, o4-mini, GPT-4.1, and others).
To use alternate models, perhaps a preview release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`:
To use alternate models, perhaps a preview release or a dated model release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`:
```json
{
"language_models": {
"openai": {
"available_models": [
{
"name": "gpt-5",
"display_name": "gpt-5 high",
"reasoning_effort": "high",
"max_tokens": 272000,
"max_completion_tokens": 20000
},
{
"name": "gpt-4o-2024-08-06",
"display_name": "GPT 4o Summer 2024",
"max_tokens": 128000
},
{
"name": "o1-mini",
"display_name": "o1-mini",
"max_tokens": 128000,
"max_completion_tokens": 20000
}
]
],
"version": "1"
}
}
}

View File

@@ -114,19 +114,19 @@ cargo test --workspace
## Installing from msys2
[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). The package is available for UCRT64, CLANG64 and CLANGARM64 repositories. To download it, run
[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). The package is available for UCRT64, MINGW64 and CLANG64 repositories. To download it, run
```sh
pacman -Syu
pacman -S $MINGW_PACKAGE_PREFIX-zed
```
then you can run `zeditor` CLI. Editor executable is installed under `$MINGW_PREFIX/lib/zed` directory
You can see the [build script](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-zed/PKGBUILD) for more details on build process.
> Please, report any issue in [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed) first.
See also MSYS2 [documentation page](https://www.msys2.org/docs/ides-editors).
Note that `collab` is not supported for MSYS2.
## Troubleshooting