Compare commits
5 Commits
taffy-zero
...
merge-conf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa1255ddb | ||
|
|
d36304963e | ||
|
|
0d71351b02 | ||
|
|
b06fe288f3 | ||
|
|
fd0ffb737f |
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(¶ms.session_id).unwrap();
|
||||
let mut tasks = vec![];
|
||||
for update in self.next_prompt_updates.lock().drain(..) {
|
||||
let thread = thread.clone();
|
||||
let update = update.clone();
|
||||
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
|
||||
&& let Some(options) = self.permission_requests.get(&tool_call.id)
|
||||
{
|
||||
Some((tool_call.clone(), options.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let task = cx.spawn(async move |cx| {
|
||||
if let Some((tool_call, options)) = permission_request {
|
||||
let permission = thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(
|
||||
tool_call.clone(),
|
||||
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::*;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)),
|
||||
),
|
||||
|
||||
@@ -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>)> {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -11,9 +11,6 @@ workspace = true
|
||||
[lib]
|
||||
path = "src/assistant_context.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1036,10 +1036,6 @@ impl Item for Editor {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::BreadcrumbsChanged => {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
f(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -140,7 +140,6 @@ pub enum IconName {
|
||||
Image,
|
||||
Indicator,
|
||||
Info,
|
||||
Json,
|
||||
Keyboard,
|
||||
Library,
|
||||
LineHeight,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>]) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user