Compare commits
28 Commits
vim-slow
...
thread-edi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
257d10f324 | ||
|
|
eccdfed32b | ||
|
|
2664596a34 | ||
|
|
23f2fb6089 | ||
|
|
fb2c2c55dc | ||
|
|
8315fde1ff | ||
|
|
fc87440682 | ||
|
|
c996eadaf5 | ||
|
|
e8c6c1ba04 | ||
|
|
b8364d7c33 | ||
|
|
7c23ef89ec | ||
|
|
2f463370cc | ||
|
|
feed34cafe | ||
|
|
4724aa5cb8 | ||
|
|
366a5db2c0 | ||
|
|
81e87c4cd6 | ||
|
|
b8ba663c20 | ||
|
|
27fb1098fa | ||
|
|
0f5a63a9b0 | ||
|
|
c8ada5b1ae | ||
|
|
27a18843d4 | ||
|
|
2bc1d60c52 | ||
|
|
17933f1222 | ||
|
|
cd87307289 | ||
|
|
11b29d693f | ||
|
|
c061698229 | ||
|
|
b4f7af066e | ||
|
|
c83621fa1f |
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -211,8 +211,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -228,9 +226,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol-schema"
|
name = "agent-client-protocol-schema"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
@@ -18618,6 +18614,7 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"mach2 0.5.0",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
@@ -21135,7 +21132,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.212.0"
|
version = "0.213.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acp_tools",
|
"acp_tools",
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
|||||||
# External crates
|
# External crates
|
||||||
#
|
#
|
||||||
|
|
||||||
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
|
agent-client-protocol = { path = "../agent-client-protocol", features = ["unstable"] }
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = "0.25.1-rc1"
|
alacritty_terminal = "0.25.1-rc1"
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|||||||
@@ -735,14 +735,6 @@
|
|||||||
"tab": "editor::ComposeCompletion"
|
"tab": "editor::ComposeCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Editor && in_snippet",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"alt-right": "editor::NextSnippetTabstop",
|
|
||||||
"alt-left": "editor::PreviousSnippetTabstop"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Bindings for accepting edit predictions
|
// Bindings for accepting edit predictions
|
||||||
//
|
//
|
||||||
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
|
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
|
||||||
|
|||||||
@@ -805,14 +805,6 @@
|
|||||||
"tab": "editor::ComposeCompletion"
|
"tab": "editor::ComposeCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Editor && in_snippet",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"alt-right": "editor::NextSnippetTabstop",
|
|
||||||
"alt-left": "editor::PreviousSnippetTabstop"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "Editor && edit_prediction",
|
"context": "Editor && edit_prediction",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|||||||
@@ -739,14 +739,6 @@
|
|||||||
"tab": "editor::ComposeCompletion"
|
"tab": "editor::ComposeCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Editor && in_snippet",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"alt-right": "editor::NextSnippetTabstop",
|
|
||||||
"alt-left": "editor::PreviousSnippetTabstop"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Bindings for accepting edit predictions
|
// Bindings for accepting edit predictions
|
||||||
//
|
//
|
||||||
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
|
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UserMessage {
|
pub struct UserMessage<T> {
|
||||||
pub id: Option<UserMessageId>,
|
pub id: Option<UserMessageId>,
|
||||||
pub content: ContentBlock,
|
pub content: ContentBlock,
|
||||||
pub chunks: Vec<acp::ContentBlock>,
|
pub chunks: Vec<acp::ContentBlock<T>>,
|
||||||
pub checkpoint: Option<Checkpoint>,
|
pub checkpoint: Option<Checkpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ pub struct Checkpoint {
|
|||||||
pub show: bool,
|
pub show: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserMessage {
|
impl<T> UserMessage<T> {
|
||||||
fn to_markdown(&self, cx: &App) -> String {
|
fn to_markdown(&self, cx: &App) -> String {
|
||||||
let mut markdown = String::new();
|
let mut markdown = String::new();
|
||||||
if self
|
if self
|
||||||
@@ -116,13 +116,13 @@ impl AssistantMessageChunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AgentThreadEntry {
|
pub enum AgentThreadEntry<T> {
|
||||||
UserMessage(UserMessage),
|
UserMessage(UserMessage<T>),
|
||||||
AssistantMessage(AssistantMessage),
|
AssistantMessage(AssistantMessage),
|
||||||
ToolCall(ToolCall),
|
ToolCall(ToolCall),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentThreadEntry {
|
impl<T> AgentThreadEntry<T> {
|
||||||
pub fn to_markdown(&self, cx: &App) -> String {
|
pub fn to_markdown(&self, cx: &App) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::UserMessage(message) => message.to_markdown(cx),
|
Self::UserMessage(message) => message.to_markdown(cx),
|
||||||
@@ -131,7 +131,7 @@ impl AgentThreadEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_message(&self) -> Option<&UserMessage> {
|
pub fn user_message(&self) -> Option<&UserMessage<T>> {
|
||||||
if let AgentThreadEntry::UserMessage(message) = self {
|
if let AgentThreadEntry::UserMessage(message) = self {
|
||||||
Some(message)
|
Some(message)
|
||||||
} else {
|
} else {
|
||||||
@@ -802,9 +802,11 @@ pub struct RetryStatus {
|
|||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AcpThread {
|
pub struct AnchoredText;
|
||||||
title: SharedString,
|
|
||||||
entries: Vec<AgentThreadEntry>,
|
pub struct AcpThread<T = SharedString> {
|
||||||
|
title: T,
|
||||||
|
entries: Vec<AgentThreadEntry<AnchoredText>>,
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
@@ -1002,7 +1004,7 @@ impl Display for LoadError {
|
|||||||
|
|
||||||
impl Error for LoadError {}
|
impl Error for LoadError {}
|
||||||
|
|
||||||
impl AcpThread {
|
impl<T> AcpThread<T> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
title: impl Into<SharedString>,
|
title: impl Into<SharedString>,
|
||||||
connection: Rc<dyn AgentConnection>,
|
connection: Rc<dyn AgentConnection>,
|
||||||
@@ -1152,7 +1154,7 @@ impl AcpThread {
|
|||||||
pub fn push_user_content_block(
|
pub fn push_user_content_block(
|
||||||
&mut self,
|
&mut self,
|
||||||
message_id: Option<UserMessageId>,
|
message_id: Option<UserMessageId>,
|
||||||
chunk: acp::ContentBlock,
|
chunk: acp::ContentBlock<T>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let language_registry = self.project.read(cx).languages().clone();
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
@@ -1231,7 +1233,7 @@ impl AcpThread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
fn push_entry(&mut self, entry: AgentThreadEntry<T>, cx: &mut Context<Self>) {
|
||||||
self.entries.push(entry);
|
self.entries.push(entry);
|
||||||
cx.emit(AcpThreadEvent::NewEntry);
|
cx.emit(AcpThreadEvent::NewEntry);
|
||||||
}
|
}
|
||||||
@@ -1924,7 +1926,7 @@ impl AcpThread {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> {
|
fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage<T>)> {
|
||||||
self.entries
|
self.entries
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod message_editor;
|
|||||||
mod mode_selector;
|
mod mode_selector;
|
||||||
mod model_selector;
|
mod model_selector;
|
||||||
mod model_selector_popover;
|
mod model_selector_popover;
|
||||||
|
mod thread_editor;
|
||||||
mod thread_history;
|
mod thread_history;
|
||||||
mod thread_view;
|
mod thread_view;
|
||||||
|
|
||||||
|
|||||||
6
crates/agent_ui/src/acp/thread_editor.rs
Normal file
6
crates/agent_ui/src/acp/thread_editor.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use acp_thread::AcpThread;
|
||||||
|
use gpui::Entity;
|
||||||
|
|
||||||
|
pub struct ThreadEditor {
|
||||||
|
thread: Entity<AcpThread>,
|
||||||
|
}
|
||||||
@@ -1506,6 +1506,12 @@ impl AcpThreadView {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Run SpawnInTerminal in the same dir as the ACP server
|
||||||
|
let cwd = connection
|
||||||
|
.clone()
|
||||||
|
.downcast::<agent_servers::AcpConnection>()
|
||||||
|
.map(|acp_conn| acp_conn.root_dir().to_path_buf());
|
||||||
|
|
||||||
// Build SpawnInTerminal from _meta
|
// Build SpawnInTerminal from _meta
|
||||||
let login = task::SpawnInTerminal {
|
let login = task::SpawnInTerminal {
|
||||||
id: task::TaskId(format!("external-agent-{}-login", label)),
|
id: task::TaskId(format!("external-agent-{}-login", label)),
|
||||||
@@ -1514,6 +1520,7 @@ impl AcpThreadView {
|
|||||||
command: Some(command.to_string()),
|
command: Some(command.to_string()),
|
||||||
args,
|
args,
|
||||||
command_label: label.to_string(),
|
command_label: label.to_string(),
|
||||||
|
cwd,
|
||||||
env,
|
env,
|
||||||
use_new_terminal: true,
|
use_new_terminal: true,
|
||||||
allow_concurrent_runs: true,
|
allow_concurrent_runs: true,
|
||||||
@@ -1526,8 +1533,9 @@ impl AcpThreadView {
|
|||||||
pending_auth_method.replace(method.clone());
|
pending_auth_method.replace(method.clone());
|
||||||
|
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
|
let project = self.project.clone();
|
||||||
let authenticate = Self::spawn_external_agent_login(
|
let authenticate = Self::spawn_external_agent_login(
|
||||||
login, workspace, false, window, cx,
|
login, workspace, project, false, true, window, cx,
|
||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.auth_task = Some(cx.spawn_in(window, {
|
self.auth_task = Some(cx.spawn_in(window, {
|
||||||
@@ -1671,7 +1679,10 @@ impl AcpThreadView {
|
|||||||
&& let Some(login) = self.login.clone()
|
&& let Some(login) = self.login.clone()
|
||||||
{
|
{
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
Self::spawn_external_agent_login(login, workspace, false, window, cx)
|
let project = self.project.clone();
|
||||||
|
Self::spawn_external_agent_login(
|
||||||
|
login, workspace, project, false, false, window, cx,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
@@ -1721,17 +1732,40 @@ impl AcpThreadView {
|
|||||||
fn spawn_external_agent_login(
|
fn spawn_external_agent_login(
|
||||||
login: task::SpawnInTerminal,
|
login: task::SpawnInTerminal,
|
||||||
workspace: Entity<Workspace>,
|
workspace: Entity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
previous_attempt: bool,
|
previous_attempt: bool,
|
||||||
|
check_exit_code: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||||
return Task::ready(Ok(()));
|
return Task::ready(Ok(()));
|
||||||
};
|
};
|
||||||
let project = workspace.read(cx).project().clone();
|
|
||||||
|
|
||||||
window.spawn(cx, async move |cx| {
|
window.spawn(cx, async move |cx| {
|
||||||
let mut task = login.clone();
|
let mut task = login.clone();
|
||||||
|
if let Some(cmd) = &task.command {
|
||||||
|
// Have "node" command use Zed's managed Node runtime by default
|
||||||
|
if cmd == "node" {
|
||||||
|
let resolved_node_runtime = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let agent_server_store = project.agent_server_store().clone();
|
||||||
|
agent_server_store.update(cx, |store, cx| {
|
||||||
|
store.node_runtime().map(|node_runtime| {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
node_runtime.binary_path().await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Ok(Some(resolve_task)) = resolved_node_runtime {
|
||||||
|
if let Ok(node_path) = resolve_task.await {
|
||||||
|
task.command = Some(node_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
task.shell = task::Shell::WithArguments {
|
task.shell = task::Shell::WithArguments {
|
||||||
program: task.command.take().expect("login command should be set"),
|
program: task.command.take().expect("login command should be set"),
|
||||||
args: std::mem::take(&mut task.args),
|
args: std::mem::take(&mut task.args),
|
||||||
@@ -1749,44 +1783,65 @@ impl AcpThreadView {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let terminal = terminal.await?;
|
let terminal = terminal.await?;
|
||||||
let mut exit_status = terminal
|
|
||||||
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
|
||||||
.fuse();
|
|
||||||
|
|
||||||
let logged_in = cx
|
if check_exit_code {
|
||||||
.spawn({
|
// For extension-based auth, wait for the process to exit and check exit code
|
||||||
let terminal = terminal.clone();
|
let exit_status = terminal
|
||||||
async move |cx| {
|
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||||
loop {
|
.await;
|
||||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
|
||||||
let content =
|
match exit_status {
|
||||||
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
|
Some(status) if status.success() => {
|
||||||
if content.contains("Login successful")
|
Ok(())
|
||||||
|| content.contains("Type your message")
|
}
|
||||||
{
|
Some(status) => {
|
||||||
return anyhow::Ok(());
|
Err(anyhow!("Login command failed with exit code: {:?}", status.code()))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err(anyhow!("Login command terminated without exit status"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For hardcoded agents (claude-login, gemini-cli): look for specific output
|
||||||
|
let mut exit_status = terminal
|
||||||
|
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||||
|
.fuse();
|
||||||
|
|
||||||
|
let logged_in = cx
|
||||||
|
.spawn({
|
||||||
|
let terminal = terminal.clone();
|
||||||
|
async move |cx| {
|
||||||
|
loop {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||||
|
let content =
|
||||||
|
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
|
||||||
|
if content.contains("Login successful")
|
||||||
|
|| content.contains("Type your message")
|
||||||
|
{
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.fuse();
|
||||||
|
futures::pin_mut!(logged_in);
|
||||||
|
futures::select_biased! {
|
||||||
|
result = logged_in => {
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("{e}");
|
||||||
|
return Err(anyhow!("exited before logging in"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
_ = exit_status => {
|
||||||
.fuse();
|
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
||||||
futures::pin_mut!(logged_in);
|
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await
|
||||||
futures::select_biased! {
|
}
|
||||||
result = logged_in => {
|
|
||||||
if let Err(e) = result {
|
|
||||||
log::error!("{e}");
|
|
||||||
return Err(anyhow!("exited before logging in"));
|
return Err(anyhow!("exited before logging in"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = exit_status => {
|
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
|
||||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
Ok(())
|
||||||
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
|
|
||||||
}
|
|
||||||
return Err(anyhow!("exited before logging in"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
|
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2051,6 +2106,15 @@ impl AcpThreadView {
|
|||||||
.into_any(),
|
.into_any(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
||||||
|
matches!(
|
||||||
|
tool_call.status,
|
||||||
|
ToolCallStatus::WaitingForConfirmation { .. }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
let Some(thread) = self.thread() else {
|
let Some(thread) = self.thread() else {
|
||||||
return primary;
|
return primary;
|
||||||
};
|
};
|
||||||
@@ -2059,7 +2123,13 @@ impl AcpThreadView {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(primary)
|
.child(primary)
|
||||||
.child(self.render_thread_controls(&thread, cx))
|
.map(|this| {
|
||||||
|
if needs_confirmation {
|
||||||
|
this.child(self.render_generating(true))
|
||||||
|
} else {
|
||||||
|
this.child(self.render_thread_controls(&thread, cx))
|
||||||
|
}
|
||||||
|
})
|
||||||
.when_some(
|
.when_some(
|
||||||
self.thread_feedback.comments_editor.clone(),
|
self.thread_feedback.comments_editor.clone(),
|
||||||
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
|
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
|
||||||
@@ -4829,6 +4899,31 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_generating(&self, confirmation: bool) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.id("generating-spinner")
|
||||||
|
.py_2()
|
||||||
|
.px(rems_from_px(22.))
|
||||||
|
.map(|this| {
|
||||||
|
if confirmation {
|
||||||
|
this.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_2()
|
||||||
|
.child(SpinnerLabel::sand().size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
LoadingLabel::new("Waiting Confirmation")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(SpinnerLabel::new().size(LabelSize::Small))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_thread_controls(
|
fn render_thread_controls(
|
||||||
&self,
|
&self,
|
||||||
thread: &Entity<AcpThread>,
|
thread: &Entity<AcpThread>,
|
||||||
@@ -4836,12 +4931,7 @@ impl AcpThreadView {
|
|||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||||
if is_generating {
|
if is_generating {
|
||||||
return h_flex().id("thread-controls-container").child(
|
return self.render_generating(false).into_any_element();
|
||||||
div()
|
|
||||||
.py_2()
|
|
||||||
.px(rems_from_px(22.))
|
|
||||||
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||||
@@ -4929,7 +5019,10 @@ impl AcpThreadView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.child(open_as_markdown).child(scroll_to_top)
|
container
|
||||||
|
.child(open_as_markdown)
|
||||||
|
.child(scroll_to_top)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
|
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
|
||||||
|
|||||||
@@ -1013,7 +1013,7 @@ impl AgentConfiguration {
|
|||||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||||
.child(self.render_agent_server(
|
.child(self.render_agent_server(
|
||||||
AgentIcon::Name(IconName::AiOpenAi),
|
AgentIcon::Name(IconName::AiOpenAi),
|
||||||
"Codex",
|
"Codex CLI",
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||||
|
|||||||
@@ -1880,7 +1880,12 @@ impl AgentPanel {
|
|||||||
{
|
{
|
||||||
let focus_handle = focus_handle.clone();
|
let focus_handle = focus_handle.clone();
|
||||||
move |_window, cx| {
|
move |_window, cx| {
|
||||||
Tooltip::for_action_in("New…", &ToggleNewThreadMenu, &focus_handle, cx)
|
Tooltip::for_action_in(
|
||||||
|
"New Thread…",
|
||||||
|
&ToggleNewThreadMenu,
|
||||||
|
&focus_handle,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1978,7 +1983,7 @@ impl AgentPanel {
|
|||||||
.separator()
|
.separator()
|
||||||
.header("External Agents")
|
.header("External Agents")
|
||||||
.item(
|
.item(
|
||||||
ContextMenuEntry::new("New Claude Code Thread")
|
ContextMenuEntry::new("New Claude Code")
|
||||||
.icon(IconName::AiClaude)
|
.icon(IconName::AiClaude)
|
||||||
.disabled(is_via_collab)
|
.disabled(is_via_collab)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
@@ -2004,7 +2009,7 @@ impl AgentPanel {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.item(
|
.item(
|
||||||
ContextMenuEntry::new("New Codex Thread")
|
ContextMenuEntry::new("New Codex CLI")
|
||||||
.icon(IconName::AiOpenAi)
|
.icon(IconName::AiOpenAi)
|
||||||
.disabled(is_via_collab)
|
.disabled(is_via_collab)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
@@ -2030,7 +2035,7 @@ impl AgentPanel {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.item(
|
.item(
|
||||||
ContextMenuEntry::new("New Gemini CLI Thread")
|
ContextMenuEntry::new("New Gemini CLI")
|
||||||
.icon(IconName::AiGemini)
|
.icon(IconName::AiGemini)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.disabled(is_via_collab)
|
.disabled(is_via_collab)
|
||||||
@@ -2074,9 +2079,9 @@ impl AgentPanel {
|
|||||||
for agent_name in agent_names {
|
for agent_name in agent_names {
|
||||||
let icon_path = agent_server_store_read.agent_icon(&agent_name);
|
let icon_path = agent_server_store_read.agent_icon(&agent_name);
|
||||||
let mut entry =
|
let mut entry =
|
||||||
ContextMenuEntry::new(format!("New {} Thread", agent_name));
|
ContextMenuEntry::new(format!("New {}", agent_name));
|
||||||
if let Some(icon_path) = icon_path {
|
if let Some(icon_path) = icon_path {
|
||||||
entry = entry.custom_icon_path(icon_path);
|
entry = entry.custom_icon_svg(icon_path);
|
||||||
} else {
|
} else {
|
||||||
entry = entry.icon(IconName::Terminal);
|
entry = entry.icon(IconName::Terminal);
|
||||||
}
|
}
|
||||||
@@ -2145,7 +2150,7 @@ impl AgentPanel {
|
|||||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||||
let label = selected_agent_label.clone();
|
let label = selected_agent_label.clone();
|
||||||
this.px(DynamicSpacing::Base02.rems(cx))
|
this.px(DynamicSpacing::Base02.rems(cx))
|
||||||
.child(Icon::from_path(icon_path).color(Color::Muted))
|
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
|
||||||
.tooltip(move |_window, cx| {
|
.tooltip(move |_window, cx| {
|
||||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ impl TextThreadEditor {
|
|||||||
editor.insert(&format!("/{name}"), window, cx);
|
editor.insert(&format!("/{name}"), window, cx);
|
||||||
if command.accepts_arguments() {
|
if command.accepts_arguments() {
|
||||||
editor.insert(" ", window, cx);
|
editor.insert(" ", window, cx);
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions::default(), window, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ use anyhow::Context as _;
|
|||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{Channel, ChannelEvent, ChannelStore};
|
use channel::{Channel, ChannelEvent, ChannelStore};
|
||||||
use client::{ChannelId, Client, Contact, User, UserStore};
|
use client::{ChannelId, Client, Contact, User, UserStore};
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
use contact_finder::ContactFinder;
|
use contact_finder::ContactFinder;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
use fuzzy::{StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
|
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
|
||||||
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
|
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
|
||||||
@@ -30,9 +31,9 @@ use smallvec::SmallVec;
|
|||||||
use std::{mem, sync::Arc};
|
use std::{mem, sync::Arc};
|
||||||
use theme::{ActiveTheme, ThemeSettings};
|
use theme::{ActiveTheme, ThemeSettings};
|
||||||
use ui::{
|
use ui::{
|
||||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, Icon, IconButton,
|
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
|
||||||
IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip, prelude::*,
|
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
|
||||||
tooltip_container,
|
prelude::*, tooltip_container,
|
||||||
};
|
};
|
||||||
use util::{ResultExt, TryFutureExt, maybe};
|
use util::{ResultExt, TryFutureExt, maybe};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
@@ -261,6 +262,8 @@ enum ListEntry {
|
|||||||
channel: Arc<Channel>,
|
channel: Arc<Channel>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
has_children: bool,
|
has_children: bool,
|
||||||
|
// `None` when the channel is a parent of a matched channel.
|
||||||
|
string_match: Option<StringMatch>,
|
||||||
},
|
},
|
||||||
ChannelNotes {
|
ChannelNotes {
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
@@ -630,6 +633,10 @@ impl CollabPanel {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
|
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
|
||||||
);
|
);
|
||||||
|
let mut channels = channel_store
|
||||||
|
.ordered_channels()
|
||||||
|
.map(|(_, chan)| chan)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let matches = executor.block(match_strings(
|
let matches = executor.block(match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
&query,
|
&query,
|
||||||
@@ -639,14 +646,34 @@ impl CollabPanel {
|
|||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let matches_by_id: HashMap<_, _> = matches
|
||||||
|
.iter()
|
||||||
|
.map(|mat| (channels[mat.candidate_id].id, mat.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let channel_ids_of_matches_or_parents: HashSet<_> = matches
|
||||||
|
.iter()
|
||||||
|
.flat_map(|mat| {
|
||||||
|
let match_channel = channels[mat.candidate_id];
|
||||||
|
|
||||||
|
match_channel
|
||||||
|
.parent_path
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.chain(Some(match_channel.id))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
channels.retain(|chan| channel_ids_of_matches_or_parents.contains(&chan.id));
|
||||||
|
|
||||||
if let Some(state) = &self.channel_editing_state
|
if let Some(state) = &self.channel_editing_state
|
||||||
&& matches!(state, ChannelEditingState::Create { location: None, .. })
|
&& matches!(state, ChannelEditingState::Create { location: None, .. })
|
||||||
{
|
{
|
||||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||||
}
|
}
|
||||||
let mut collapse_depth = None;
|
let mut collapse_depth = None;
|
||||||
for mat in matches {
|
for (idx, channel) in channels.into_iter().enumerate() {
|
||||||
let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
|
|
||||||
let depth = channel.parent_path.len();
|
let depth = channel.parent_path.len();
|
||||||
|
|
||||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||||
@@ -663,7 +690,7 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let has_children = channel_store
|
let has_children = channel_store
|
||||||
.channel_at_index(mat.candidate_id + 1)
|
.channel_at_index(idx + 1)
|
||||||
.is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
|
.is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
|
||||||
|
|
||||||
match &self.channel_editing_state {
|
match &self.channel_editing_state {
|
||||||
@@ -675,6 +702,7 @@ impl CollabPanel {
|
|||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
depth,
|
depth,
|
||||||
has_children: false,
|
has_children: false,
|
||||||
|
string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
|
||||||
});
|
});
|
||||||
self.entries
|
self.entries
|
||||||
.push(ListEntry::ChannelEditor { depth: depth + 1 });
|
.push(ListEntry::ChannelEditor { depth: depth + 1 });
|
||||||
@@ -690,6 +718,7 @@ impl CollabPanel {
|
|||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
depth,
|
depth,
|
||||||
has_children,
|
has_children,
|
||||||
|
string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2321,8 +2350,17 @@ impl CollabPanel {
|
|||||||
channel,
|
channel,
|
||||||
depth,
|
depth,
|
||||||
has_children,
|
has_children,
|
||||||
|
string_match,
|
||||||
} => self
|
} => self
|
||||||
.render_channel(channel, *depth, *has_children, is_selected, ix, cx)
|
.render_channel(
|
||||||
|
channel,
|
||||||
|
*depth,
|
||||||
|
*has_children,
|
||||||
|
is_selected,
|
||||||
|
ix,
|
||||||
|
string_match.as_ref(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ListEntry::ChannelEditor { depth } => self
|
ListEntry::ChannelEditor { depth } => self
|
||||||
.render_channel_editor(*depth, window, cx)
|
.render_channel_editor(*depth, window, cx)
|
||||||
@@ -2719,6 +2757,7 @@ impl CollabPanel {
|
|||||||
has_children: bool,
|
has_children: bool,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
|
string_match: Option<&StringMatch>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
@@ -2855,7 +2894,14 @@ impl CollabPanel {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(channel_id.0 as usize)
|
.id(channel_id.0 as usize)
|
||||||
.child(Label::new(channel.name.clone()))
|
.child(match string_match {
|
||||||
|
None => Label::new(channel.name.clone()).into_any_element(),
|
||||||
|
Some(string_match) => HighlightedLabel::new(
|
||||||
|
channel.name.clone(),
|
||||||
|
string_match.positions.clone(),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
})
|
||||||
.children(face_pile.map(|face_pile| face_pile.p_1())),
|
.children(face_pile.map(|face_pile| face_pile.p_1())),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -265,9 +265,10 @@ impl minidumper::ServerHandler for CrashServer {
|
|||||||
3 => {
|
3 => {
|
||||||
let gpu_specs: system_specs::GpuSpecs =
|
let gpu_specs: system_specs::GpuSpecs =
|
||||||
bincode::deserialize(&buffer).expect("gpu specs");
|
bincode::deserialize(&buffer).expect("gpu specs");
|
||||||
self.active_gpu
|
// we ignore the case where it was already set because this message is sent
|
||||||
.set(gpu_specs)
|
// on each new window. in theory all zed windows should be using the same
|
||||||
.expect("already set active gpu");
|
// GPU so this is fine.
|
||||||
|
self.active_gpu.set(gpu_specs).ok();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("invalid message kind");
|
panic!("invalid message kind");
|
||||||
|
|||||||
@@ -213,6 +213,15 @@ pub struct ExpandExcerptsDown {
|
|||||||
pub(super) lines: u32,
|
pub(super) lines: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows code completion suggestions at the cursor position.
|
||||||
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
||||||
|
#[action(namespace = editor)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ShowCompletions {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) trigger: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles text input in the editor.
|
/// Handles text input in the editor.
|
||||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
||||||
#[action(namespace = editor)]
|
#[action(namespace = editor)]
|
||||||
@@ -612,8 +621,6 @@ actions!(
|
|||||||
NextEditPrediction,
|
NextEditPrediction,
|
||||||
/// Scrolls to the next screen.
|
/// Scrolls to the next screen.
|
||||||
NextScreen,
|
NextScreen,
|
||||||
/// Goes to the next snippet tabstop if one exists.
|
|
||||||
NextSnippetTabstop,
|
|
||||||
/// Opens the context menu at cursor position.
|
/// Opens the context menu at cursor position.
|
||||||
OpenContextMenu,
|
OpenContextMenu,
|
||||||
/// Opens excerpts from the current file.
|
/// Opens excerpts from the current file.
|
||||||
@@ -647,8 +654,6 @@ actions!(
|
|||||||
Paste,
|
Paste,
|
||||||
/// Navigates to the previous edit prediction.
|
/// Navigates to the previous edit prediction.
|
||||||
PreviousEditPrediction,
|
PreviousEditPrediction,
|
||||||
/// Goes to the previous snippet tabstop if one exists.
|
|
||||||
PreviousSnippetTabstop,
|
|
||||||
/// Redoes the last undone edit.
|
/// Redoes the last undone edit.
|
||||||
Redo,
|
Redo,
|
||||||
/// Redoes the last selection change.
|
/// Redoes the last selection change.
|
||||||
@@ -727,8 +732,6 @@ actions!(
|
|||||||
SelectToStartOfParagraph,
|
SelectToStartOfParagraph,
|
||||||
/// Extends selection up.
|
/// Extends selection up.
|
||||||
SelectUp,
|
SelectUp,
|
||||||
/// Shows code completion suggestions at the cursor position.
|
|
||||||
ShowCompletions,
|
|
||||||
/// Shows the system character palette.
|
/// Shows the system character palette.
|
||||||
ShowCharacterPalette,
|
ShowCharacterPalette,
|
||||||
/// Shows edit prediction at cursor.
|
/// Shows edit prediction at cursor.
|
||||||
|
|||||||
@@ -252,17 +252,8 @@ enum MarkdownCacheKey {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum CompletionsMenuSource {
|
pub enum CompletionsMenuSource {
|
||||||
/// Show all completions (words, snippets, LSP)
|
|
||||||
Normal,
|
Normal,
|
||||||
/// Show only snippets (not words or LSP)
|
|
||||||
///
|
|
||||||
/// Used after typing a non-word character
|
|
||||||
SnippetsOnly,
|
|
||||||
/// Tab stops within a snippet that have a predefined finite set of choices
|
|
||||||
SnippetChoices,
|
SnippetChoices,
|
||||||
/// Show only words (not snippets or LSP)
|
|
||||||
///
|
|
||||||
/// Used when word completions are explicitly triggered
|
|
||||||
Words { ignore_threshold: bool },
|
Words { ignore_threshold: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2281,16 +2281,32 @@ impl Editor {
|
|||||||
|editor, _, e: &EditorEvent, window, cx| match e {
|
|editor, _, e: &EditorEvent, window, cx| match e {
|
||||||
EditorEvent::ScrollPositionChanged { local, .. } => {
|
EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||||
if *local {
|
if *local {
|
||||||
let new_anchor = editor.scroll_manager.anchor();
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
editor.update_restoration_data(cx, move |data| {
|
|
||||||
data.scroll_position = (
|
|
||||||
new_anchor.top_row(snapshot.buffer_snapshot()),
|
|
||||||
new_anchor.offset,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
||||||
editor.inline_blame_popover.take();
|
editor.inline_blame_popover.take();
|
||||||
|
editor.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(50))
|
||||||
|
.await;
|
||||||
|
editor
|
||||||
|
.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.register_visible_buffers(cx);
|
||||||
|
editor.refresh_colors_for_visible_range(None, window, cx);
|
||||||
|
editor.refresh_inlay_hints(
|
||||||
|
InlayHintRefreshReason::NewLinesShown,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_anchor = editor.scroll_manager.anchor();
|
||||||
|
let snapshot = editor.snapshot(window, cx);
|
||||||
|
editor.update_restoration_data(cx, move |data| {
|
||||||
|
data.scroll_position = (
|
||||||
|
new_anchor.top_row(snapshot.buffer_snapshot()),
|
||||||
|
new_anchor.offset,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditorEvent::Edited { .. } => {
|
EditorEvent::Edited { .. } => {
|
||||||
@@ -2461,10 +2477,6 @@ impl Editor {
|
|||||||
key_context.add("renaming");
|
key_context.add("renaming");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.snippet_stack.is_empty() {
|
|
||||||
key_context.add("in_snippet");
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.context_menu.borrow().as_ref() {
|
match self.context_menu.borrow().as_ref() {
|
||||||
Some(CodeContextMenu::Completions(menu)) => {
|
Some(CodeContextMenu::Completions(menu)) => {
|
||||||
if menu.visible() {
|
if menu.visible() {
|
||||||
@@ -3142,7 +3154,7 @@ impl Editor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if continue_showing {
|
if continue_showing {
|
||||||
self.open_or_update_completions_menu(None, None, false, window, cx);
|
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
} else {
|
} else {
|
||||||
self.hide_context_menu(window, cx);
|
self.hide_context_menu(window, cx);
|
||||||
}
|
}
|
||||||
@@ -4972,18 +4984,57 @@ impl Editor {
|
|||||||
ignore_threshold: false,
|
ignore_threshold: false,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
trigger_in_words,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => self.open_or_update_completions_menu(
|
Some(CompletionsMenuSource::Normal)
|
||||||
None,
|
| Some(CompletionsMenuSource::SnippetChoices)
|
||||||
Some(text.to_owned()).filter(|x| !x.is_empty()),
|
| None
|
||||||
true,
|
if self.is_completion_trigger(
|
||||||
window,
|
text,
|
||||||
|
trigger_in_words,
|
||||||
|
completions_source.is_some(),
|
||||||
|
cx,
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
self.show_completions(
|
||||||
|
&ShowCompletions {
|
||||||
|
trigger: Some(text.to_owned()).filter(|x| !x.is_empty()),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.hide_context_menu(window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
trigger_in_words: bool,
|
||||||
|
menu_is_open: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let position = self.selections.newest_anchor().head();
|
||||||
|
let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(completion_provider) = &self.completion_provider {
|
||||||
|
completion_provider.is_completion_trigger(
|
||||||
|
&buffer,
|
||||||
|
position.text_anchor,
|
||||||
|
text,
|
||||||
|
trigger_in_words,
|
||||||
|
menu_is_open,
|
||||||
cx,
|
cx,
|
||||||
),
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5261,7 +5312,6 @@ impl Editor {
|
|||||||
ignore_threshold: true,
|
ignore_threshold: true,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
false,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -5269,18 +5319,17 @@ impl Editor {
|
|||||||
|
|
||||||
pub fn show_completions(
|
pub fn show_completions(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ShowCompletions,
|
options: &ShowCompletions,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.open_or_update_completions_menu(None, None, false, window, cx);
|
self.open_or_update_completions_menu(None, options.trigger.as_deref(), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_or_update_completions_menu(
|
fn open_or_update_completions_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
requested_source: Option<CompletionsMenuSource>,
|
requested_source: Option<CompletionsMenuSource>,
|
||||||
trigger: Option<String>,
|
trigger: Option<&str>,
|
||||||
trigger_in_words: bool,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
@@ -5288,15 +5337,6 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let completions_source = self
|
|
||||||
.context_menu
|
|
||||||
.borrow()
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|menu| match menu {
|
|
||||||
CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source),
|
|
||||||
CodeContextMenu::CodeActions(_) => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
let multibuffer_snapshot = self.buffer.read(cx).read(cx);
|
let multibuffer_snapshot = self.buffer.read(cx).read(cx);
|
||||||
|
|
||||||
// Typically `start` == `end`, but with snippet tabstop choices the default choice is
|
// Typically `start` == `end`, but with snippet tabstop choices the default choice is
|
||||||
@@ -5344,8 +5384,7 @@ impl Editor {
|
|||||||
ignore_word_threshold = ignore_threshold;
|
ignore_word_threshold = ignore_threshold;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Some(CompletionsMenuSource::SnippetChoices)
|
Some(CompletionsMenuSource::SnippetChoices) => {
|
||||||
| Some(CompletionsMenuSource::SnippetsOnly) => {
|
|
||||||
log::error!("bug: SnippetChoices requested_source is not handled");
|
log::error!("bug: SnippetChoices requested_source is not handled");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -5359,19 +5398,13 @@ impl Editor {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|provider| provider.filter_completions());
|
.is_none_or(|provider| provider.filter_completions());
|
||||||
|
|
||||||
let was_snippets_only = matches!(
|
|
||||||
completions_source,
|
|
||||||
Some(CompletionsMenuSource::SnippetsOnly)
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
|
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
|
||||||
if filter_completions {
|
if filter_completions {
|
||||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||||
}
|
}
|
||||||
// When `is_incomplete` is false, no need to re-query completions when the current query
|
// When `is_incomplete` is false, no need to re-query completions when the current query
|
||||||
// is a suffix of the initial query.
|
// is a suffix of the initial query.
|
||||||
let was_complete = !menu.is_incomplete;
|
if !menu.is_incomplete {
|
||||||
if was_complete && !was_snippets_only {
|
|
||||||
// If the new query is a suffix of the old query (typing more characters) and
|
// If the new query is a suffix of the old query (typing more characters) and
|
||||||
// the previous result was complete, the existing completions can be filtered.
|
// the previous result was complete, the existing completions can be filtered.
|
||||||
//
|
//
|
||||||
@@ -5395,6 +5428,23 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let trigger_kind = match trigger {
|
||||||
|
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
|
||||||
|
CompletionTriggerKind::TRIGGER_CHARACTER
|
||||||
|
}
|
||||||
|
_ => CompletionTriggerKind::INVOKED,
|
||||||
|
};
|
||||||
|
let completion_context = CompletionContext {
|
||||||
|
trigger_character: trigger.and_then(|trigger| {
|
||||||
|
if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
|
||||||
|
Some(String::from(trigger))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
trigger_kind,
|
||||||
|
};
|
||||||
|
|
||||||
let Anchor {
|
let Anchor {
|
||||||
excerpt_id: buffer_excerpt_id,
|
excerpt_id: buffer_excerpt_id,
|
||||||
text_anchor: buffer_position,
|
text_anchor: buffer_position,
|
||||||
@@ -5452,72 +5502,49 @@ impl Editor {
|
|||||||
&& match &query {
|
&& match &query {
|
||||||
Some(query) => query.chars().count() < completion_settings.words_min_length,
|
Some(query) => query.chars().count() < completion_settings.words_min_length,
|
||||||
None => completion_settings.words_min_length != 0,
|
None => completion_settings.words_min_length != 0,
|
||||||
})
|
});
|
||||||
|| (provider.is_some() && completion_settings.words == WordsCompletionMode::Disabled);
|
|
||||||
|
|
||||||
let mut words = if omit_word_completions {
|
let (mut words, provider_responses) = match &provider {
|
||||||
Task::ready(BTreeMap::default())
|
Some(provider) => {
|
||||||
} else {
|
let provider_responses = provider.completions(
|
||||||
cx.background_spawn(async move {
|
buffer_excerpt_id,
|
||||||
buffer_snapshot.words_in_range(WordsQuery {
|
|
||||||
fuzzy_contents: None,
|
|
||||||
range: word_search_range,
|
|
||||||
skip_digits,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let load_provider_completions = provider.as_ref().is_some_and(|provider| {
|
|
||||||
trigger.as_ref().is_none_or(|trigger| {
|
|
||||||
provider.is_completion_trigger(
|
|
||||||
&buffer,
|
&buffer,
|
||||||
position.text_anchor,
|
buffer_position,
|
||||||
trigger,
|
completion_context,
|
||||||
trigger_in_words,
|
window,
|
||||||
completions_source.is_some(),
|
|
||||||
cx,
|
cx,
|
||||||
)
|
);
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let provider_responses = if let Some(provider) = &provider
|
let words = match (omit_word_completions, completion_settings.words) {
|
||||||
&& load_provider_completions
|
(true, _) | (_, WordsCompletionMode::Disabled) => {
|
||||||
{
|
Task::ready(BTreeMap::default())
|
||||||
let trigger_character =
|
}
|
||||||
trigger.filter(|trigger| buffer.read(cx).completion_triggers().contains(trigger));
|
(false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
|
||||||
let completion_context = CompletionContext {
|
.background_spawn(async move {
|
||||||
trigger_kind: match &trigger_character {
|
buffer_snapshot.words_in_range(WordsQuery {
|
||||||
Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER,
|
fuzzy_contents: None,
|
||||||
None => CompletionTriggerKind::INVOKED,
|
range: word_search_range,
|
||||||
},
|
skip_digits,
|
||||||
trigger_character,
|
})
|
||||||
};
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
provider.completions(
|
(words, provider_responses)
|
||||||
buffer_excerpt_id,
|
}
|
||||||
&buffer,
|
None => {
|
||||||
buffer_position,
|
let words = if omit_word_completions {
|
||||||
completion_context,
|
Task::ready(BTreeMap::default())
|
||||||
window,
|
} else {
|
||||||
cx,
|
cx.background_spawn(async move {
|
||||||
)
|
buffer_snapshot.words_in_range(WordsQuery {
|
||||||
} else {
|
fuzzy_contents: None,
|
||||||
Task::ready(Ok(Vec::new()))
|
range: word_search_range,
|
||||||
};
|
skip_digits,
|
||||||
|
})
|
||||||
let snippets = if let Some(provider) = &provider
|
})
|
||||||
&& provider.show_snippets()
|
};
|
||||||
&& let Some(project) = self.project()
|
(words, Task::ready(Ok(Vec::new())))
|
||||||
{
|
}
|
||||||
project.update(cx, |project, cx| {
|
|
||||||
snippet_completions(project, &buffer, buffer_position, cx)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Task::ready(Ok(CompletionResponse {
|
|
||||||
completions: Vec::new(),
|
|
||||||
display_options: Default::default(),
|
|
||||||
is_incomplete: false,
|
|
||||||
}))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
||||||
@@ -5575,13 +5602,6 @@ impl Editor {
|
|||||||
confirm: None,
|
confirm: None,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
completions.extend(
|
|
||||||
snippets
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|response| response.completions),
|
|
||||||
);
|
|
||||||
|
|
||||||
let menu = if completions.is_empty() {
|
let menu = if completions.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -5593,11 +5613,7 @@ impl Editor {
|
|||||||
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
||||||
let menu = CompletionsMenu::new(
|
let menu = CompletionsMenu::new(
|
||||||
id,
|
id,
|
||||||
requested_source.unwrap_or(if load_provider_completions {
|
requested_source.unwrap_or(CompletionsMenuSource::Normal),
|
||||||
CompletionsMenuSource::Normal
|
|
||||||
} else {
|
|
||||||
CompletionsMenuSource::SnippetsOnly
|
|
||||||
}),
|
|
||||||
sort_completions,
|
sort_completions,
|
||||||
show_completion_documentation,
|
show_completion_documentation,
|
||||||
position,
|
position,
|
||||||
@@ -5927,7 +5943,7 @@ impl Editor {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|confirm| confirm(intent, window, cx));
|
.is_some_and(|confirm| confirm(intent, window, cx));
|
||||||
if show_new_completions_on_confirm {
|
if show_new_completions_on_confirm {
|
||||||
self.open_or_update_completions_menu(None, None, false, window, cx);
|
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider = self.completion_provider.as_ref()?;
|
let provider = self.completion_provider.as_ref()?;
|
||||||
@@ -7599,18 +7615,17 @@ impl Editor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
|
fn is_cmd_or_ctrl_pressed(modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
|
||||||
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
|
match EditorSettings::get_global(cx).multi_cursor_modifier {
|
||||||
if invert {
|
MultiCursorModifier::Alt => modifiers.secondary(),
|
||||||
match multi_cursor_setting {
|
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
|
||||||
MultiCursorModifier::Alt => modifiers.alt,
|
}
|
||||||
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
|
}
|
||||||
}
|
|
||||||
} else {
|
fn is_alt_pressed(modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
|
||||||
match multi_cursor_setting {
|
match EditorSettings::get_global(cx).multi_cursor_modifier {
|
||||||
MultiCursorModifier::Alt => modifiers.secondary(),
|
MultiCursorModifier::Alt => modifiers.alt,
|
||||||
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
|
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7619,9 +7634,9 @@ impl Editor {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<ColumnarMode> {
|
) -> Option<ColumnarMode> {
|
||||||
if modifiers.shift && modifiers.number_of_modifiers() == 2 {
|
if modifiers.shift && modifiers.number_of_modifiers() == 2 {
|
||||||
if Self::multi_cursor_modifier(false, modifiers, cx) {
|
if Self::is_cmd_or_ctrl_pressed(modifiers, cx) {
|
||||||
Some(ColumnarMode::FromMouse)
|
Some(ColumnarMode::FromMouse)
|
||||||
} else if Self::multi_cursor_modifier(true, modifiers, cx) {
|
} else if Self::is_alt_pressed(modifiers, cx) {
|
||||||
Some(ColumnarMode::FromSelection)
|
Some(ColumnarMode::FromSelection)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -9976,38 +9991,6 @@ impl Editor {
|
|||||||
self.outdent(&Outdent, window, cx);
|
self.outdent(&Outdent, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_snippet_tabstop(
|
|
||||||
&mut self,
|
|
||||||
_: &NextSnippetTabstop,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.mode.is_single_line() || self.snippet_stack.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.move_to_next_snippet_tabstop(window, cx) {
|
|
||||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous_snippet_tabstop(
|
|
||||||
&mut self,
|
|
||||||
_: &PreviousSnippetTabstop,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.mode.is_single_line() || self.snippet_stack.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.move_to_prev_snippet_tabstop(window, cx) {
|
|
||||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
@@ -12720,10 +12703,6 @@ impl Editor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🤔 | .. | show_in_menu |
|
|
||||||
// | .. | true true
|
|
||||||
// | had_edit_prediction | false true
|
|
||||||
|
|
||||||
let trigger_in_words =
|
let trigger_in_words =
|
||||||
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
|
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
|
||||||
|
|
||||||
@@ -22925,10 +22904,6 @@ pub trait CompletionProvider {
|
|||||||
fn filter_completions(&self) -> bool {
|
fn filter_completions(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_snippets(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CodeActionProvider {
|
pub trait CodeActionProvider {
|
||||||
@@ -23189,8 +23164,16 @@ impl CompletionProvider for Entity<Project> {
|
|||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||||
self.update(cx, |project, cx| {
|
self.update(cx, |project, cx| {
|
||||||
let task = project.completions(buffer, buffer_position, options, cx);
|
let snippets = snippet_completions(project, buffer, buffer_position, cx);
|
||||||
cx.background_spawn(task)
|
let project_completions = project.completions(buffer, buffer_position, options, cx);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let mut responses = project_completions.await?;
|
||||||
|
let snippets = snippets.await?;
|
||||||
|
if !snippets.completions.is_empty() {
|
||||||
|
responses.push(snippets);
|
||||||
|
}
|
||||||
|
Ok(responses)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23262,10 +23245,6 @@ impl CompletionProvider for Entity<Project> {
|
|||||||
|
|
||||||
buffer.completion_triggers().contains(text)
|
buffer.completion_triggers().contains(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_snippets(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SemanticsProvider for Entity<Project> {
|
impl SemanticsProvider for Entity<Project> {
|
||||||
|
|||||||
@@ -11137,129 +11137,6 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx, |_| {});
|
|
||||||
|
|
||||||
fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
|
|
||||||
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
|
|
||||||
assert_eq!(editor.text(cx), expected_text);
|
|
||||||
assert_eq!(
|
|
||||||
editor
|
|
||||||
.selections
|
|
||||||
.ranges::<usize>(&editor.display_snapshot(cx)),
|
|
||||||
selection_ranges
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (text, insertion_ranges) = marked_text_ranges(
|
|
||||||
indoc! {"
|
|
||||||
ˇ
|
|
||||||
"},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
|
|
||||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
|
|
||||||
|
|
||||||
_ = editor.update_in(cx, |editor, window, cx| {
|
|
||||||
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
|
|
||||||
|
|
||||||
editor
|
|
||||||
.insert_snippet(&insertion_ranges, snippet, window, cx)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_state(
|
|
||||||
editor,
|
|
||||||
cx,
|
|
||||||
indoc! {"
|
|
||||||
type «» = ;•
|
|
||||||
"},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
editor.context_menu_visible(),
|
|
||||||
"Context menu should be visible for placeholder choices"
|
|
||||||
);
|
|
||||||
|
|
||||||
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
|
|
||||||
|
|
||||||
assert_state(
|
|
||||||
editor,
|
|
||||||
cx,
|
|
||||||
indoc! {"
|
|
||||||
type = «»;•
|
|
||||||
"},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!editor.context_menu_visible(),
|
|
||||||
"Context menu should be hidden after moving to next tabstop"
|
|
||||||
);
|
|
||||||
|
|
||||||
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
|
|
||||||
|
|
||||||
assert_state(
|
|
||||||
editor,
|
|
||||||
cx,
|
|
||||||
indoc! {"
|
|
||||||
type = ; ˇ
|
|
||||||
"},
|
|
||||||
);
|
|
||||||
|
|
||||||
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
|
|
||||||
|
|
||||||
assert_state(
|
|
||||||
editor,
|
|
||||||
cx,
|
|
||||||
indoc! {"
|
|
||||||
type = ; ˇ
|
|
||||||
"},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
_ = editor.update_in(cx, |editor, window, cx| {
|
|
||||||
editor.select_all(&SelectAll, window, cx);
|
|
||||||
editor.backspace(&Backspace, window, cx);
|
|
||||||
|
|
||||||
let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
|
|
||||||
let insertion_ranges = editor
|
|
||||||
.selections
|
|
||||||
.all(&editor.display_snapshot(cx))
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.range())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
editor
|
|
||||||
.insert_snippet(&insertion_ranges, snippet, window, cx)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_state(editor, cx, "fn «» = value;•");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
editor.context_menu_visible(),
|
|
||||||
"Context menu should be visible for placeholder choices"
|
|
||||||
);
|
|
||||||
|
|
||||||
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
|
|
||||||
|
|
||||||
assert_state(editor, cx, "fn = «valueˇ»;•");
|
|
||||||
|
|
||||||
editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
|
|
||||||
|
|
||||||
assert_state(editor, cx, "fn «» = value;•");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
editor.context_menu_visible(),
|
|
||||||
"Context menu should be visible again after returning to first tabstop"
|
|
||||||
);
|
|
||||||
|
|
||||||
editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
|
|
||||||
|
|
||||||
assert_state(editor, cx, "fn «» = value;•");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_snippets(cx: &mut TestAppContext) {
|
async fn test_snippets(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
@@ -13883,7 +13760,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
|||||||
|
|
||||||
cx.set_state(&run.initial_state);
|
cx.set_state(&run.initial_state);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let counter = Arc::new(AtomicUsize::new(0));
|
let counter = Arc::new(AtomicUsize::new(0));
|
||||||
@@ -13943,7 +13820,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
|||||||
|
|
||||||
cx.set_state(initial_state);
|
cx.set_state(initial_state);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let counter = Arc::new(AtomicUsize::new(0));
|
let counter = Arc::new(AtomicUsize::new(0));
|
||||||
@@ -13979,7 +13856,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
|||||||
|
|
||||||
cx.set_state(initial_state);
|
cx.set_state(initial_state);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
@@ -14066,7 +13943,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
|||||||
"};
|
"};
|
||||||
cx.set_state(initial_state);
|
cx.set_state(initial_state);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
@@ -14120,7 +13997,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
|||||||
"};
|
"};
|
||||||
cx.set_state(initial_state);
|
cx.set_state(initial_state);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
@@ -14169,7 +14046,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
|||||||
"};
|
"};
|
||||||
cx.set_state(initial_state);
|
cx.set_state(initial_state);
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
@@ -14320,7 +14197,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
|
|||||||
});
|
});
|
||||||
|
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
fake_server
|
fake_server
|
||||||
@@ -14559,7 +14436,7 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||||||
cx.assert_editor_state("editor.cloˇ");
|
cx.assert_editor_state("editor.cloˇ");
|
||||||
assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
|
assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
handle_completion_request(
|
handle_completion_request(
|
||||||
"editor.<clo|>",
|
"editor.<clo|>",
|
||||||
@@ -14958,7 +14835,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
|||||||
4.5f32
|
4.5f32
|
||||||
"});
|
"});
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions::default(), window, cx);
|
||||||
});
|
});
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.condition(|editor, _| editor.context_menu_visible())
|
cx.condition(|editor, _| editor.context_menu_visible())
|
||||||
@@ -14984,7 +14861,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
|||||||
33.35f32
|
33.35f32
|
||||||
"});
|
"});
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions::default(), window, cx);
|
||||||
});
|
});
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.condition(|editor, _| editor.context_menu_visible())
|
cx.condition(|editor, _| editor.context_menu_visible())
|
||||||
@@ -15408,7 +15285,13 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
|
|||||||
cx.set_state("fn a() {}\n nˇ");
|
cx.set_state("fn a() {}\n nˇ");
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.trigger_completion_on_input("n", true, window, cx)
|
editor.show_completions(
|
||||||
|
&ShowCompletions {
|
||||||
|
trigger: Some("\n".into()),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
@@ -15506,7 +15389,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
|
|||||||
})))
|
})))
|
||||||
});
|
});
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
@@ -15555,7 +15438,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
|
|||||||
})))
|
})))
|
||||||
});
|
});
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
@@ -18045,7 +17928,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.update_editor(|editor, window, cx| {
|
cx.update_editor(|editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
completion_requests.next().await;
|
completion_requests.next().await;
|
||||||
cx.condition(|editor, _| editor.context_menu_visible())
|
cx.condition(|editor, _| editor.context_menu_visible())
|
||||||
@@ -24441,7 +24324,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
|
|||||||
])))
|
])))
|
||||||
});
|
});
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.show_completions(&ShowCompletions, window, cx);
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
});
|
});
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
completion_handle.next().await.unwrap();
|
completion_handle.next().await.unwrap();
|
||||||
|
|||||||
@@ -232,8 +232,6 @@ impl EditorElement {
|
|||||||
register_action(editor, window, Editor::blame_hover);
|
register_action(editor, window, Editor::blame_hover);
|
||||||
register_action(editor, window, Editor::delete);
|
register_action(editor, window, Editor::delete);
|
||||||
register_action(editor, window, Editor::tab);
|
register_action(editor, window, Editor::tab);
|
||||||
register_action(editor, window, Editor::next_snippet_tabstop);
|
|
||||||
register_action(editor, window, Editor::previous_snippet_tabstop);
|
|
||||||
register_action(editor, window, Editor::backtab);
|
register_action(editor, window, Editor::backtab);
|
||||||
register_action(editor, window, Editor::indent);
|
register_action(editor, window, Editor::indent);
|
||||||
register_action(editor, window, Editor::outdent);
|
register_action(editor, window, Editor::outdent);
|
||||||
@@ -820,7 +818,7 @@ impl EditorElement {
|
|||||||
editor.select(
|
editor.select(
|
||||||
SelectPhase::Begin {
|
SelectPhase::Begin {
|
||||||
position,
|
position,
|
||||||
add: Editor::multi_cursor_modifier(true, &modifiers, cx),
|
add: Editor::is_alt_pressed(&modifiers, cx),
|
||||||
click_count,
|
click_count,
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
@@ -1004,7 +1002,7 @@ impl EditorElement {
|
|||||||
let text_hitbox = &position_map.text_hitbox;
|
let text_hitbox = &position_map.text_hitbox;
|
||||||
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
|
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
|
||||||
|
|
||||||
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx);
|
let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx);
|
||||||
|
|
||||||
if let Some(mouse_position) = event.mouse_position()
|
if let Some(mouse_position) = event.mouse_position()
|
||||||
&& !pending_nonempty_selections
|
&& !pending_nonempty_selections
|
||||||
@@ -4006,41 +4004,51 @@ impl EditorElement {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(h_flex().gap_2().map(|path_header| {
|
||||||
h_flex()
|
let filename = filename
|
||||||
.gap_2()
|
.map(SharedString::from)
|
||||||
.map(|path_header| {
|
.unwrap_or_else(|| "untitled".into());
|
||||||
let filename = filename
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| "untitled".into());
|
|
||||||
|
|
||||||
path_header
|
path_header
|
||||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||||
let path = path::Path::new(filename.as_str());
|
let path = path::Path::new(filename.as_str());
|
||||||
let icon = FileIcons::get_icon(path, cx)
|
let icon =
|
||||||
.unwrap_or_default();
|
FileIcons::get_icon(path, cx).unwrap_or_default();
|
||||||
let icon =
|
let icon = Icon::from_path(icon).color(Color::Muted);
|
||||||
Icon::from_path(icon).color(Color::Muted);
|
el.child(icon)
|
||||||
el.child(icon)
|
|
||||||
})
|
|
||||||
.child(Label::new(filename).single_line().when_some(
|
|
||||||
file_status,
|
|
||||||
|el, status| {
|
|
||||||
el.color(if status.is_conflicted() {
|
|
||||||
Color::Conflict
|
|
||||||
} else if status.is_modified() {
|
|
||||||
Color::Modified
|
|
||||||
} else if status.is_deleted() {
|
|
||||||
Color::Disabled
|
|
||||||
} else {
|
|
||||||
Color::Created
|
|
||||||
})
|
|
||||||
.when(status.is_deleted(), |el| {
|
|
||||||
el.strikethrough()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
|
.child(
|
||||||
|
ButtonLike::new("filename-button")
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.child(
|
||||||
|
Label::new(filename)
|
||||||
|
.single_line()
|
||||||
|
.color(file_status_label_color(
|
||||||
|
file_status,
|
||||||
|
))
|
||||||
|
.when(
|
||||||
|
file_status.is_some_and(|s| {
|
||||||
|
s.is_deleted()
|
||||||
|
}),
|
||||||
|
|label| label.strikethrough(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.group_hover("", |div| div.underline()),
|
||||||
|
)
|
||||||
|
.on_click(window.listener_for(&self.editor, {
|
||||||
|
let jump_data = jump_data.clone();
|
||||||
|
move |editor, e: &ClickEvent, window, cx| {
|
||||||
|
editor.open_excerpts_common(
|
||||||
|
Some(jump_data.clone()),
|
||||||
|
e.modifiers().secondary(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
.when_some(parent_path, |then, path| {
|
.when_some(parent_path, |then, path| {
|
||||||
then.child(div().child(path).text_color(
|
then.child(div().child(path).text_color(
|
||||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||||
@@ -4049,33 +4057,47 @@ impl EditorElement {
|
|||||||
colors.text_muted
|
colors.text_muted
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}),
|
})
|
||||||
)
|
}))
|
||||||
.when(
|
.when(
|
||||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||||
|el| {
|
|el| {
|
||||||
el.child(
|
el.child(
|
||||||
h_flex()
|
ButtonLike::new("open-file-button")
|
||||||
.id("jump-to-file-button")
|
.style(ButtonStyle::OutlinedGhost)
|
||||||
.gap_2p5()
|
.child(
|
||||||
.child(Label::new("Jump To File"))
|
h_flex()
|
||||||
.child(KeyBinding::for_action_in(
|
.gap_2p5()
|
||||||
&OpenExcerpts,
|
.child(Label::new("Open file"))
|
||||||
&focus_handle,
|
.child(KeyBinding::for_action_in(
|
||||||
cx,
|
&OpenExcerpts,
|
||||||
)),
|
&focus_handle,
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.on_click(window.listener_for(&self.editor, {
|
||||||
|
let jump_data = jump_data.clone();
|
||||||
|
move |editor, e: &ClickEvent, window, cx| {
|
||||||
|
editor.open_excerpts_common(
|
||||||
|
Some(jump_data.clone()),
|
||||||
|
e.modifiers().secondary(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
.on_click(window.listener_for(&self.editor, {
|
.on_click(window.listener_for(&self.editor, {
|
||||||
move |editor, e: &ClickEvent, window, cx| {
|
let buffer_id = for_excerpt.buffer_id;
|
||||||
editor.open_excerpts_common(
|
move |editor, _e: &ClickEvent, _window, cx| {
|
||||||
Some(jump_data.clone()),
|
if is_folded {
|
||||||
e.modifiers().secondary(),
|
editor.unfold_buffer(buffer_id, cx);
|
||||||
window,
|
} else {
|
||||||
cx,
|
editor.fold_buffer(buffer_id, cx);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
@@ -7514,6 +7536,22 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
|
||||||
|
file_status.map_or(Color::Default, |status| {
|
||||||
|
if status.is_conflicted() {
|
||||||
|
Color::Conflict
|
||||||
|
} else if status.is_modified() {
|
||||||
|
Color::Modified
|
||||||
|
} else if status.is_deleted() {
|
||||||
|
Color::Disabled
|
||||||
|
} else if status.is_created() {
|
||||||
|
Color::Created
|
||||||
|
} else {
|
||||||
|
Color::Default
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn header_jump_data(
|
fn header_jump_data(
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
block_row_start: DisplayRow,
|
block_row_start: DisplayRow,
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ impl Editor {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
|
let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx);
|
||||||
if !hovered_link_modifier || self.has_pending_selection() {
|
if !hovered_link_modifier || self.has_pending_selection() {
|
||||||
self.hide_hovered_link(cx);
|
self.hide_hovered_link(cx);
|
||||||
return;
|
return;
|
||||||
@@ -241,8 +241,8 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let navigate_task =
|
let split = Self::is_alt_pressed(&modifiers, cx);
|
||||||
self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
|
let navigate_task = self.navigate_to_hover_links(None, links, split, window, cx);
|
||||||
self.select(SelectPhase::End, window, cx);
|
self.select(SelectPhase::End, window, cx);
|
||||||
return navigate_task;
|
return navigate_task;
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,8 @@ impl Editor {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let navigate_task = if point.as_valid().is_some() {
|
let navigate_task = if point.as_valid().is_some() {
|
||||||
match (modifiers.shift, modifiers.alt) {
|
let split = Self::is_alt_pressed(&modifiers, cx);
|
||||||
|
match (modifiers.shift, split) {
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
|
self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ impl Editor {
|
|||||||
scroll_position
|
scroll_position
|
||||||
};
|
};
|
||||||
|
|
||||||
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
|
self.scroll_manager.set_scroll_position(
|
||||||
adjusted_position,
|
adjusted_position,
|
||||||
&display_map,
|
&display_map,
|
||||||
local,
|
local,
|
||||||
@@ -611,22 +611,7 @@ impl Editor {
|
|||||||
workspace_id,
|
workspace_id,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
|
|
||||||
self.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(Duration::from_millis(50))
|
|
||||||
.await;
|
|
||||||
editor
|
|
||||||
.update_in(cx, |editor, window, cx| {
|
|
||||||
editor.register_visible_buffers(cx);
|
|
||||||
editor.refresh_colors_for_visible_range(None, window, cx);
|
|
||||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
editor_was_scrolled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {
|
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ pub struct AgentServerManifestEntry {
|
|||||||
/// cmd = "node"
|
/// cmd = "node"
|
||||||
/// args = ["index.js", "--port", "3000"]
|
/// args = ["index.js", "--port", "3000"]
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note: All commands are executed with the archive extraction directory as the
|
||||||
|
/// working directory, so relative paths in args (like "index.js") will resolve
|
||||||
|
/// relative to the extracted archive contents.
|
||||||
pub targets: HashMap<String, TargetConfig>,
|
pub targets: HashMap<String, TargetConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use std::{fs, path::Path, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
||||||
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
||||||
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
|
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
|
||||||
radians, size,
|
radians, size,
|
||||||
@@ -11,6 +13,7 @@ pub struct Svg {
|
|||||||
interactivity: Interactivity,
|
interactivity: Interactivity,
|
||||||
transformation: Option<Transformation>,
|
transformation: Option<Transformation>,
|
||||||
path: Option<SharedString>,
|
path: Option<SharedString>,
|
||||||
|
external_path: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new SVG element.
|
/// Create a new SVG element.
|
||||||
@@ -20,6 +23,7 @@ pub fn svg() -> Svg {
|
|||||||
interactivity: Interactivity::new(),
|
interactivity: Interactivity::new(),
|
||||||
transformation: None,
|
transformation: None,
|
||||||
path: None,
|
path: None,
|
||||||
|
external_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +34,12 @@ impl Svg {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the path to the SVG file for this element.
|
||||||
|
pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||||
|
self.external_path = Some(path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Transform the SVG element with the given transformation.
|
/// Transform the SVG element with the given transformation.
|
||||||
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
|
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
|
||||||
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
|
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
|
||||||
@@ -117,7 +127,35 @@ impl Element for Svg {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
window
|
window
|
||||||
.paint_svg(bounds, path.clone(), transformation, color, cx)
|
.paint_svg(bounds, path.clone(), None, transformation, color, cx)
|
||||||
|
.log_err();
|
||||||
|
} else if let Some((path, color)) =
|
||||||
|
self.external_path.as_ref().zip(style.text.color)
|
||||||
|
{
|
||||||
|
let Some(bytes) = window
|
||||||
|
.use_asset::<SvgAsset>(path, cx)
|
||||||
|
.and_then(|asset| asset.log_err())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let transformation = self
|
||||||
|
.transformation
|
||||||
|
.as_ref()
|
||||||
|
.map(|transformation| {
|
||||||
|
transformation.into_matrix(bounds.center(), window.scale_factor())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
window
|
||||||
|
.paint_svg(
|
||||||
|
bounds,
|
||||||
|
path.clone(),
|
||||||
|
Some(&bytes),
|
||||||
|
transformation,
|
||||||
|
color,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -219,3 +257,21 @@ impl Transformation {
|
|||||||
.translate(center.scale(scale_factor).negate())
|
.translate(center.scale(scale_factor).negate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SvgAsset {}
|
||||||
|
|
||||||
|
impl Asset for SvgAsset {
|
||||||
|
type Source = SharedString;
|
||||||
|
type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
source: Self::Source,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||||
|
async move {
|
||||||
|
let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
|
||||||
|
let bytes = Arc::from(bytes);
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,10 +26,8 @@ pub struct KeyDownEvent {
|
|||||||
/// Whether the key is currently held down.
|
/// Whether the key is currently held down.
|
||||||
pub is_held: bool,
|
pub is_held: bool,
|
||||||
|
|
||||||
/// Whether the modifiers are excessive for producing this character.
|
/// Whether to prefer character input over keybindings for this keystroke.
|
||||||
/// When false, the modifiers are essential for character input (e.g., AltGr),
|
/// In some cases, like AltGr on Windows, modifiers are significant for character input.
|
||||||
/// and character input should be prioritized over keybindings.
|
|
||||||
/// When true, the modifiers are for keybindings (e.g., Ctrl+A).
|
|
||||||
pub prefer_character_input: bool,
|
pub prefer_character_input: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -572,6 +572,14 @@ impl Modifiers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns [`Modifiers`] with just function.
|
||||||
|
pub fn function() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
function: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns [`Modifiers`] with command + shift.
|
/// Returns [`Modifiers`] with command + shift.
|
||||||
pub fn command_shift() -> Modifiers {
|
pub fn command_shift() -> Modifiers {
|
||||||
Modifiers {
|
Modifiers {
|
||||||
|
|||||||
@@ -1124,7 +1124,32 @@ impl Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it wasn't a string, try the various supported image types.
|
// Next, check for URL flavors (including file URLs). Some tools only provide a URL
|
||||||
|
// with no plain text entry.
|
||||||
|
{
|
||||||
|
// Try the modern UTType identifiers first.
|
||||||
|
let file_url_type: id = ns_string("public.file-url");
|
||||||
|
let url_type: id = ns_string("public.url");
|
||||||
|
|
||||||
|
let url_data = if msg_send![types, containsObject: file_url_type] {
|
||||||
|
pasteboard.dataForType(file_url_type)
|
||||||
|
} else if msg_send![types, containsObject: url_type] {
|
||||||
|
pasteboard.dataForType(url_type)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
};
|
||||||
|
|
||||||
|
if url_data != nil && !url_data.bytes().is_null() {
|
||||||
|
let bytes = slice::from_raw_parts(
|
||||||
|
url_data.bytes() as *mut u8,
|
||||||
|
url_data.length() as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Some(self.read_string_from_clipboard(&state, bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it wasn't a string or URL, try the various supported image types.
|
||||||
for format in ImageFormat::iter() {
|
for format in ImageFormat::iter() {
|
||||||
if let Some(item) = try_clipboard_image(pasteboard, format) {
|
if let Some(item) = try_clipboard_image(pasteboard, format) {
|
||||||
return Some(item);
|
return Some(item);
|
||||||
@@ -1132,7 +1157,7 @@ impl Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it wasn't a string or a supported image type, give up.
|
// If it wasn't a string, URL, or a supported image type, give up.
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1707,6 +1732,40 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_url_reads_as_url_string() {
|
||||||
|
let platform = build_platform();
|
||||||
|
|
||||||
|
// Create a file URL for an arbitrary test path and write it to the pasteboard.
|
||||||
|
// This path does not need to exist; we only validate URL→path conversion.
|
||||||
|
let mock_path = "/tmp/zed-clipboard-file-url-test";
|
||||||
|
unsafe {
|
||||||
|
// Build an NSURL from the file path
|
||||||
|
let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(mock_path)];
|
||||||
|
let abs: id = msg_send![url, absoluteString];
|
||||||
|
|
||||||
|
// Encode the URL string as UTF-8 bytes
|
||||||
|
let len: usize = msg_send![abs, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
|
||||||
|
let bytes_ptr = abs.UTF8String() as *const u8;
|
||||||
|
let data = NSData::dataWithBytes_length_(nil, bytes_ptr as *const c_void, len as u64);
|
||||||
|
|
||||||
|
// Write as public.file-url to the unique pasteboard
|
||||||
|
let file_url_type: id = ns_string("public.file-url");
|
||||||
|
platform
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.pasteboard
|
||||||
|
.setData_forType(data, file_url_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the clipboard read returns the URL string, not a converted path
|
||||||
|
let expected_url = format!("file://{}", mock_path);
|
||||||
|
assert_eq!(
|
||||||
|
platform.read_from_clipboard(),
|
||||||
|
Some(ClipboardItem::new_string(expected_url))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn build_platform() -> MacPlatform {
|
fn build_platform() -> MacPlatform {
|
||||||
let platform = MacPlatform::new(false);
|
let platform = MacPlatform::new(false);
|
||||||
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||||
|
|||||||
@@ -1753,9 +1753,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send key equivalents to the input handler,
|
// Don't send key equivalents to the input handler if there are key modifiers other
|
||||||
// or macOS shortcuts like cmd-` will stop working.
|
// than Function key, or macOS shortcuts like cmd-` will stop working.
|
||||||
if key_equivalent {
|
if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1370,7 +1370,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
|||||||
scan_code as u32,
|
scan_code as u32,
|
||||||
Some(&keyboard_state),
|
Some(&keyboard_state),
|
||||||
&mut buffer_c,
|
&mut buffer_c,
|
||||||
0x4,
|
0x5,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if result_c < 0 {
|
if result_c < 0 {
|
||||||
@@ -1387,6 +1387,8 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround for some bug that makes the compiler think keyboard_state is still zeroed out
|
||||||
|
let keyboard_state = std::hint::black_box(keyboard_state);
|
||||||
let ctrl_down = (keyboard_state[VK_CONTROL.0 as usize] & 0x80) != 0;
|
let ctrl_down = (keyboard_state[VK_CONTROL.0 as usize] & 0x80) != 0;
|
||||||
let alt_down = (keyboard_state[VK_MENU.0 as usize] & 0x80) != 0;
|
let alt_down = (keyboard_state[VK_MENU.0 as usize] & 0x80) != 0;
|
||||||
let win_down = (keyboard_state[VK_LWIN.0 as usize] & 0x80) != 0
|
let win_down = (keyboard_state[VK_LWIN.0 as usize] & 0x80) != 0
|
||||||
@@ -1413,7 +1415,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
|||||||
scan_code as u32,
|
scan_code as u32,
|
||||||
Some(&state_no_modifiers),
|
Some(&state_no_modifiers),
|
||||||
&mut buffer_c_no_modifiers,
|
&mut buffer_c_no_modifiers,
|
||||||
0x4,
|
0x5,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if result_c_no_modifiers <= 0 {
|
if result_c_no_modifiers <= 0 {
|
||||||
|
|||||||
@@ -95,27 +95,34 @@ impl SvgRenderer {
|
|||||||
pub(crate) fn render_alpha_mask(
|
pub(crate) fn render_alpha_mask(
|
||||||
&self,
|
&self,
|
||||||
params: &RenderSvgParams,
|
params: &RenderSvgParams,
|
||||||
|
bytes: Option<&[u8]>,
|
||||||
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
|
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
|
||||||
anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
|
anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
|
||||||
|
|
||||||
// Load the tree.
|
let render_pixmap = |bytes| {
|
||||||
let Some(bytes) = self.asset_source.load(¶ms.path)? else {
|
let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?;
|
||||||
return Ok(None);
|
|
||||||
|
// Convert the pixmap's pixels into an alpha mask.
|
||||||
|
let size = Size::new(
|
||||||
|
DevicePixels(pixmap.width() as i32),
|
||||||
|
DevicePixels(pixmap.height() as i32),
|
||||||
|
);
|
||||||
|
let alpha_mask = pixmap
|
||||||
|
.pixels()
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.alpha())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Some((size, alpha_mask)))
|
||||||
};
|
};
|
||||||
|
|
||||||
let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
|
if let Some(bytes) = bytes {
|
||||||
|
render_pixmap(bytes)
|
||||||
// Convert the pixmap's pixels into an alpha mask.
|
} else if let Some(bytes) = self.asset_source.load(¶ms.path)? {
|
||||||
let size = Size::new(
|
render_pixmap(&bytes)
|
||||||
DevicePixels(pixmap.width() as i32),
|
} else {
|
||||||
DevicePixels(pixmap.height() as i32),
|
Ok(None)
|
||||||
);
|
}
|
||||||
let alpha_mask = pixmap
|
|
||||||
.pixels()
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.alpha())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
Ok(Some((size, alpha_mask)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
||||||
|
|||||||
@@ -3084,6 +3084,7 @@ impl Window {
|
|||||||
&mut self,
|
&mut self,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
path: SharedString,
|
path: SharedString,
|
||||||
|
mut data: Option<&[u8]>,
|
||||||
transformation: TransformationMatrix,
|
transformation: TransformationMatrix,
|
||||||
color: Hsla,
|
color: Hsla,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
@@ -3104,7 +3105,8 @@ impl Window {
|
|||||||
let Some(tile) =
|
let Some(tile) =
|
||||||
self.sprite_atlas
|
self.sprite_atlas
|
||||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms)? else {
|
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)?
|
||||||
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
Ok(Some((size, Cow::Owned(bytes))))
|
Ok(Some((size, Cow::Owned(bytes))))
|
||||||
|
|||||||
@@ -3366,7 +3366,19 @@ impl BufferSnapshot {
|
|||||||
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer<'_>> {
|
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer<'_>> {
|
||||||
let offset = position.to_offset(self);
|
let offset = position.to_offset(self);
|
||||||
self.syntax_layers_for_range(offset..offset, false)
|
self.syntax_layers_for_range(offset..offset, false)
|
||||||
.filter(|l| l.node().end_byte() > offset)
|
.filter(|l| {
|
||||||
|
if let Some(ranges) = l.included_sub_ranges {
|
||||||
|
ranges.iter().any(|range| {
|
||||||
|
let start = range.start.to_offset(self);
|
||||||
|
start <= offset && {
|
||||||
|
let end = range.end.to_offset(self);
|
||||||
|
offset < end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
l.node().start_byte() <= offset && l.node().end_byte() > offset
|
||||||
|
}
|
||||||
|
})
|
||||||
.last()
|
.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2633,7 +2633,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut App) {
|
|||||||
buffer.set_language_registry(language_registry.clone());
|
buffer.set_language_registry(language_registry.clone());
|
||||||
buffer.set_language(
|
buffer.set_language(
|
||||||
language_registry
|
language_registry
|
||||||
.language_for_name("ERB")
|
.language_for_name("HTML+ERB")
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.ok(),
|
.ok(),
|
||||||
@@ -2753,6 +2753,50 @@ fn test_language_at_for_markdown_code_block(cx: &mut App) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_syntax_layer_at_for_injected_languages(cx: &mut App) {
|
||||||
|
init_settings(cx, |_| {});
|
||||||
|
|
||||||
|
cx.new(|cx| {
|
||||||
|
let text = r#"
|
||||||
|
```html+erb
|
||||||
|
<div>Hello</div>
|
||||||
|
<%= link_to "Some", "https://zed.dev" %>
|
||||||
|
```
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
|
language_registry.add(Arc::new(erb_lang()));
|
||||||
|
language_registry.add(Arc::new(html_lang()));
|
||||||
|
language_registry.add(Arc::new(ruby_lang()));
|
||||||
|
|
||||||
|
let mut buffer = Buffer::local(text, cx);
|
||||||
|
buffer.set_language_registry(language_registry.clone());
|
||||||
|
buffer.set_language(
|
||||||
|
language_registry
|
||||||
|
.language_for_name("HTML+ERB")
|
||||||
|
.now_or_never()
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
|
||||||
|
// Test points in the code line
|
||||||
|
let html_point = Point::new(1, 4);
|
||||||
|
let language = snapshot.language_at(html_point).unwrap();
|
||||||
|
assert_eq!(language.name().as_ref(), "HTML");
|
||||||
|
|
||||||
|
let ruby_point = Point::new(2, 6);
|
||||||
|
let language = snapshot.language_at(ruby_point).unwrap();
|
||||||
|
assert_eq!(language.name().as_ref(), "Ruby");
|
||||||
|
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_serialization(cx: &mut gpui::App) {
|
fn test_serialization(cx: &mut gpui::App) {
|
||||||
let mut now = Instant::now();
|
let mut now = Instant::now();
|
||||||
@@ -3655,7 +3699,7 @@ fn html_lang() -> Language {
|
|||||||
fn erb_lang() -> Language {
|
fn erb_lang() -> Language {
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
name: "ERB".into(),
|
name: "HTML+ERB".into(),
|
||||||
matcher: LanguageMatcher {
|
matcher: LanguageMatcher {
|
||||||
path_suffixes: vec!["erb".to_string()],
|
path_suffixes: vec!["erb".to_string()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -3673,15 +3717,15 @@ fn erb_lang() -> Language {
|
|||||||
.with_injection_query(
|
.with_injection_query(
|
||||||
r#"
|
r#"
|
||||||
(
|
(
|
||||||
(code) @injection.content
|
(code) @content
|
||||||
(#set! injection.language "ruby")
|
(#set! "language" "ruby")
|
||||||
(#set! injection.combined)
|
(#set! "combined")
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
(content) @injection.content
|
(content) @content
|
||||||
(#set! injection.language "html")
|
(#set! "language" "html")
|
||||||
(#set! injection.combined)
|
(#set! "combined")
|
||||||
)
|
)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -587,6 +587,8 @@ impl SyntaxSnapshot {
|
|||||||
let changed_ranges;
|
let changed_ranges;
|
||||||
|
|
||||||
let mut included_ranges = step.included_ranges;
|
let mut included_ranges = step.included_ranges;
|
||||||
|
let is_combined = matches!(step.mode, ParseMode::Combined { .. });
|
||||||
|
|
||||||
for range in &mut included_ranges {
|
for range in &mut included_ranges {
|
||||||
range.start_byte -= step_start_byte;
|
range.start_byte -= step_start_byte;
|
||||||
range.end_byte -= step_start_byte;
|
range.end_byte -= step_start_byte;
|
||||||
@@ -749,16 +751,20 @@ impl SyntaxSnapshot {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let included_sub_ranges: Option<Vec<Range<Anchor>>> =
|
let included_sub_ranges: Option<Vec<Range<Anchor>>> = if is_combined {
|
||||||
(included_ranges.len() > 1).then_some(
|
Some(
|
||||||
included_ranges
|
included_ranges
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|r| r.start_byte < r.end_byte)
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
text.anchor_before(r.start_byte + step_start_byte)
|
text.anchor_before(r.start_byte + step_start_byte)
|
||||||
..text.anchor_after(r.end_byte + step_start_byte)
|
..text.anchor_after(r.end_byte + step_start_byte)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
SyntaxLayerContent::Parsed {
|
SyntaxLayerContent::Parsed {
|
||||||
tree,
|
tree,
|
||||||
language,
|
language,
|
||||||
|
|||||||
@@ -538,27 +538,27 @@ impl OpenAiEventMapper {
|
|||||||
return events;
|
return events;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(content) = choice.delta.content.clone() {
|
if let Some(delta) = choice.delta.as_ref() {
|
||||||
if !content.is_empty() {
|
if let Some(content) = delta.content.clone() {
|
||||||
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
|
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
|
if let Some(tool_calls) = delta.tool_calls.as_ref() {
|
||||||
for tool_call in tool_calls {
|
for tool_call in tool_calls {
|
||||||
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
|
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
|
||||||
|
|
||||||
if let Some(tool_id) = tool_call.id.clone() {
|
if let Some(tool_id) = tool_call.id.clone() {
|
||||||
entry.id = tool_id;
|
entry.id = tool_id;
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(function) = tool_call.function.as_ref() {
|
|
||||||
if let Some(name) = function.name.clone() {
|
|
||||||
entry.name = name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(arguments) = function.arguments.clone() {
|
if let Some(function) = tool_call.function.as_ref() {
|
||||||
entry.arguments.push_str(&arguments);
|
if let Some(name) = function.name.clone() {
|
||||||
|
entry.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(arguments) = function.arguments.clone() {
|
||||||
|
entry.arguments.push_str(&arguments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ struct ManagedNodeRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedNodeRuntime {
|
impl ManagedNodeRuntime {
|
||||||
const VERSION: &str = "v22.5.1";
|
const VERSION: &str = "v24.11.0";
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
const NODE_PATH: &str = "bin/node";
|
const NODE_PATH: &str = "bin/node";
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ pub struct Usage {
|
|||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ChoiceDelta {
|
pub struct ChoiceDelta {
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
pub delta: ResponseMessageDelta,
|
pub delta: Option<ResponseMessageDelta>,
|
||||||
pub finish_reason: Option<String>,
|
pub finish_reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -438,6 +438,13 @@ impl AgentServerStore {
|
|||||||
cx.emit(AgentServersUpdated);
|
cx.emit(AgentServersUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn node_runtime(&self) -> Option<NodeRuntime> {
|
||||||
|
match &self.state {
|
||||||
|
AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
node_runtime: NodeRuntime,
|
node_runtime: NodeRuntime,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
@@ -1560,7 +1567,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
|
|||||||
env: Some(env),
|
env: Some(env),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((command, root_dir.to_string_lossy().into_owned(), None))
|
Ok((command, version_dir.to_string_lossy().into_owned(), None))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1946,6 +1953,51 @@ mod extension_agent_tests {
|
|||||||
assert_eq!(target.args, vec!["index.js"]);
|
assert_eq!(target.args, vec!["index.js"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) {
|
||||||
|
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||||
|
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||||
|
let node_runtime = NodeRuntime::unavailable();
|
||||||
|
let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone()));
|
||||||
|
let project_environment = cx.new(|cx| {
|
||||||
|
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let agent = LocalExtensionArchiveAgent {
|
||||||
|
fs: fs.clone(),
|
||||||
|
http_client,
|
||||||
|
node_runtime,
|
||||||
|
project_environment,
|
||||||
|
extension_id: Arc::from("test-ext"),
|
||||||
|
agent_id: Arc::from("test-agent"),
|
||||||
|
targets: {
|
||||||
|
let mut map = HashMap::default();
|
||||||
|
map.insert(
|
||||||
|
"darwin-aarch64".to_string(),
|
||||||
|
extension::TargetConfig {
|
||||||
|
archive: "https://example.com/test.zip".into(),
|
||||||
|
cmd: "node".into(),
|
||||||
|
args: vec![
|
||||||
|
"server.js".into(),
|
||||||
|
"--config".into(),
|
||||||
|
"./config.json".into(),
|
||||||
|
],
|
||||||
|
sha256: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
map
|
||||||
|
},
|
||||||
|
env: HashMap::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the agent is configured with relative paths in args
|
||||||
|
let target = agent.targets.get("darwin-aarch64").unwrap();
|
||||||
|
assert_eq!(target.args[0], "server.js");
|
||||||
|
assert_eq!(target.args[2], "./config.json");
|
||||||
|
// These relative paths will resolve relative to the extraction directory
|
||||||
|
// when the command is executed
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tilde_expansion_in_settings() {
|
fn test_tilde_expansion_in_settings() {
|
||||||
let settings = settings::BuiltinAgentServerSettings {
|
let settings = settings::BuiltinAgentServerSettings {
|
||||||
|
|||||||
@@ -12349,10 +12349,7 @@ impl LspStore {
|
|||||||
.update(cx, |buffer, _| buffer.wait_for_version(version))?
|
.update(cx, |buffer, _| buffer.wait_for_version(version))?
|
||||||
.await?;
|
.await?;
|
||||||
lsp_store.update(cx, |lsp_store, cx| {
|
lsp_store.update(cx, |lsp_store, cx| {
|
||||||
let lsp_data = lsp_store
|
let lsp_data = lsp_store.latest_lsp_data(&buffer, cx);
|
||||||
.lsp_data
|
|
||||||
.entry(buffer_id)
|
|
||||||
.or_insert_with(|| BufferLspData::new(&buffer, cx));
|
|
||||||
let chunks_queried_for = lsp_data
|
let chunks_queried_for = lsp_data
|
||||||
.inlay_hints
|
.inlay_hints
|
||||||
.applicable_chunks(&[range])
|
.applicable_chunks(&[range])
|
||||||
|
|||||||
@@ -1070,14 +1070,21 @@ impl SshSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn shell(&self) -> String {
|
async fn shell(&self) -> String {
|
||||||
|
let default_shell = "sh";
|
||||||
match self
|
match self
|
||||||
.run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"])
|
.run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(shell) => shell.trim().to_owned(),
|
Ok(shell) => match shell.trim() {
|
||||||
|
"" => {
|
||||||
|
log::error!("$SHELL is not set, falling back to {default_shell}");
|
||||||
|
default_shell.to_owned()
|
||||||
|
}
|
||||||
|
shell => shell.to_owned(),
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get shell: {e}");
|
log::error!("Failed to get shell: {e}");
|
||||||
"sh".to_owned()
|
default_shell.to_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -891,7 +891,7 @@ impl SettingsPageItem {
|
|||||||
.px_8()
|
.px_8()
|
||||||
.child(discriminant_element.when(has_sub_fields, |this| this.pb_4())),
|
.child(discriminant_element.when(has_sub_fields, |this| this.pb_4())),
|
||||||
)
|
)
|
||||||
.when(!has_sub_fields, |this| {
|
.when(!has_sub_fields && !is_last, |this| {
|
||||||
this.child(h_flex().px_8().child(Divider::horizontal()))
|
this.child(h_flex().px_8().child(Divider::horizontal()))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ pub struct ContextMenuEntry {
|
|||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconName>,
|
icon: Option<IconName>,
|
||||||
custom_icon_path: Option<SharedString>,
|
custom_icon_path: Option<SharedString>,
|
||||||
|
custom_icon_svg: Option<SharedString>,
|
||||||
icon_position: IconPosition,
|
icon_position: IconPosition,
|
||||||
icon_size: IconSize,
|
icon_size: IconSize,
|
||||||
icon_color: Option<Color>,
|
icon_color: Option<Color>,
|
||||||
@@ -68,6 +69,7 @@ impl ContextMenuEntry {
|
|||||||
label: label.into(),
|
label: label.into(),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_position: IconPosition::Start,
|
icon_position: IconPosition::Start,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -94,7 +96,15 @@ impl ContextMenuEntry {
|
|||||||
|
|
||||||
pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
|
pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||||
self.custom_icon_path = Some(path.into());
|
self.custom_icon_path = Some(path.into());
|
||||||
self.icon = None; // Clear IconName if custom path is set
|
self.custom_icon_svg = None; // Clear other icon sources if custom path is set
|
||||||
|
self.icon = None;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn custom_icon_svg(mut self, svg: impl Into<SharedString>) -> Self {
|
||||||
|
self.custom_icon_svg = Some(svg.into());
|
||||||
|
self.custom_icon_path = None; // Clear other icon sources if custom path is set
|
||||||
|
self.icon = None;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,6 +406,7 @@ impl ContextMenu {
|
|||||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -425,6 +436,7 @@ impl ContextMenu {
|
|||||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -454,6 +466,7 @@ impl ContextMenu {
|
|||||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -482,6 +495,7 @@ impl ContextMenu {
|
|||||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_position: position,
|
icon_position: position,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -541,6 +555,7 @@ impl ContextMenu {
|
|||||||
}),
|
}),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -572,6 +587,7 @@ impl ContextMenu {
|
|||||||
}),
|
}),
|
||||||
icon: None,
|
icon: None,
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -593,6 +609,7 @@ impl ContextMenu {
|
|||||||
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
||||||
icon: Some(IconName::ArrowUpRight),
|
icon: Some(IconName::ArrowUpRight),
|
||||||
custom_icon_path: None,
|
custom_icon_path: None,
|
||||||
|
custom_icon_svg: None,
|
||||||
icon_size: IconSize::XSmall,
|
icon_size: IconSize::XSmall,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
@@ -913,6 +930,7 @@ impl ContextMenu {
|
|||||||
handler,
|
handler,
|
||||||
icon,
|
icon,
|
||||||
custom_icon_path,
|
custom_icon_path,
|
||||||
|
custom_icon_svg,
|
||||||
icon_position,
|
icon_position,
|
||||||
icon_size,
|
icon_size,
|
||||||
icon_color,
|
icon_color,
|
||||||
@@ -965,6 +983,28 @@ impl ContextMenu {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
} else if let Some(custom_icon_svg) = custom_icon_svg {
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.when(
|
||||||
|
*icon_position == IconPosition::Start && toggle.is_none(),
|
||||||
|
|flex| {
|
||||||
|
flex.child(
|
||||||
|
Icon::from_external_svg(custom_icon_svg.clone())
|
||||||
|
.size(*icon_size)
|
||||||
|
.color(icon_color),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(Label::new(label.clone()).color(label_color).truncate())
|
||||||
|
.when(*icon_position == IconPosition::End, |flex| {
|
||||||
|
flex.child(
|
||||||
|
Icon::from_external_svg(custom_icon_svg.clone())
|
||||||
|
.size(*icon_size)
|
||||||
|
.color(icon_color),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
} else if let Some(icon_name) = icon {
|
} else if let Some(icon_name) = icon {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
|
|||||||
@@ -115,24 +115,24 @@ impl From<IconName> for Icon {
|
|||||||
/// The source of an icon.
|
/// The source of an icon.
|
||||||
enum IconSource {
|
enum IconSource {
|
||||||
/// An SVG embedded in the Zed binary.
|
/// An SVG embedded in the Zed binary.
|
||||||
Svg(SharedString),
|
Embedded(SharedString),
|
||||||
/// An image file located at the specified path.
|
/// An image file located at the specified path.
|
||||||
///
|
///
|
||||||
/// Currently our SVG renderer is missing support for the following features:
|
/// Currently our SVG renderer is missing support for rendering polychrome SVGs.
|
||||||
/// 1. Loading SVGs from external files.
|
|
||||||
/// 2. Rendering polychrome SVGs.
|
|
||||||
///
|
///
|
||||||
/// In order to support icon themes, we render the icons as images instead.
|
/// In order to support icon themes, we render the icons as images instead.
|
||||||
Image(Arc<Path>),
|
External(Arc<Path>),
|
||||||
|
/// An SVG not embedded in the Zed binary.
|
||||||
|
ExternalSvg(SharedString),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconSource {
|
impl IconSource {
|
||||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
if path.starts_with("icons/") {
|
if path.starts_with("icons/") {
|
||||||
Self::Svg(path)
|
Self::Embedded(path)
|
||||||
} else {
|
} else {
|
||||||
Self::Image(Arc::from(PathBuf::from(path.as_ref())))
|
Self::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ pub struct Icon {
|
|||||||
impl Icon {
|
impl Icon {
|
||||||
pub fn new(icon: IconName) -> Self {
|
pub fn new(icon: IconName) -> Self {
|
||||||
Self {
|
Self {
|
||||||
source: IconSource::Svg(icon.path().into()),
|
source: IconSource::Embedded(icon.path().into()),
|
||||||
color: Color::default(),
|
color: Color::default(),
|
||||||
size: IconSize::default().rems(),
|
size: IconSize::default().rems(),
|
||||||
transformation: Transformation::default(),
|
transformation: Transformation::default(),
|
||||||
@@ -164,6 +164,15 @@ impl Icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_external_svg(svg: SharedString) -> Self {
|
||||||
|
Self {
|
||||||
|
source: IconSource::ExternalSvg(svg),
|
||||||
|
color: Color::default(),
|
||||||
|
size: IconSize::default().rems(),
|
||||||
|
transformation: Transformation::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn color(mut self, color: Color) -> Self {
|
pub fn color(mut self, color: Color) -> Self {
|
||||||
self.color = color;
|
self.color = color;
|
||||||
self
|
self
|
||||||
@@ -193,14 +202,21 @@ impl Transformable for Icon {
|
|||||||
impl RenderOnce for Icon {
|
impl RenderOnce for Icon {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
match self.source {
|
match self.source {
|
||||||
IconSource::Svg(path) => svg()
|
IconSource::Embedded(path) => svg()
|
||||||
.with_transformation(self.transformation)
|
.with_transformation(self.transformation)
|
||||||
.size(self.size)
|
.size(self.size)
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.path(path)
|
.path(path)
|
||||||
.text_color(self.color.color(cx))
|
.text_color(self.color.color(cx))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
IconSource::Image(path) => img(path)
|
IconSource::ExternalSvg(path) => svg()
|
||||||
|
.external_path(path)
|
||||||
|
.with_transformation(self.transformation)
|
||||||
|
.size(self.size)
|
||||||
|
.flex_none()
|
||||||
|
.text_color(self.color.color(cx))
|
||||||
|
.into_any_element(),
|
||||||
|
IconSource::External(path) => img(path)
|
||||||
.size(self.size)
|
.size(self.size)
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.text_color(self.color.color(cx))
|
.text_color(self.color.color(cx))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
|
use gpui::{Animation, AnimationExt, FontWeight};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
@@ -84,38 +84,29 @@ impl RenderOnce for LoadingLabel {
|
|||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let text = self.text.clone();
|
let text = self.text.clone();
|
||||||
|
|
||||||
self.base
|
self.base.color(Color::Muted).with_animations(
|
||||||
.color(Color::Muted)
|
"loading_label",
|
||||||
.with_animations(
|
vec![
|
||||||
"loading_label",
|
Animation::new(Duration::from_secs(1)),
|
||||||
vec![
|
Animation::new(Duration::from_secs(1)).repeat(),
|
||||||
Animation::new(Duration::from_secs(1)),
|
],
|
||||||
Animation::new(Duration::from_secs(1)).repeat(),
|
move |mut label, animation_ix, delta| {
|
||||||
],
|
match animation_ix {
|
||||||
move |mut label, animation_ix, delta| {
|
0 => {
|
||||||
match animation_ix {
|
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
||||||
0 => {
|
let text = SharedString::from(text[0..chars_to_show].to_string());
|
||||||
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
label.set_text(text);
|
||||||
let text = SharedString::from(text[0..chars_to_show].to_string());
|
|
||||||
label.set_text(text);
|
|
||||||
}
|
|
||||||
1 => match delta {
|
|
||||||
d if d < 0.25 => label.set_text(text.clone()),
|
|
||||||
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
|
||||||
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
|
||||||
_ => label.set_text(format!("{}...", text)),
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
label
|
1 => match delta {
|
||||||
},
|
d if d < 0.25 => label.set_text(text.clone()),
|
||||||
)
|
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
||||||
.with_animation(
|
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
||||||
"pulsating-label",
|
_ => label.set_text(format!("{}...", text)),
|
||||||
Animation::new(Duration::from_secs(2))
|
},
|
||||||
.repeat()
|
_ => {}
|
||||||
.with_easing(pulsating_between(0.6, 1.)),
|
}
|
||||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
label
|
||||||
)
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub enum SpinnerVariant {
|
|||||||
#[default]
|
#[default]
|
||||||
Dots,
|
Dots,
|
||||||
DotsVariant,
|
DotsVariant,
|
||||||
|
Sand,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A spinner indication, based on the label component, that loops through
|
/// A spinner indication, based on the label component, that loops through
|
||||||
@@ -41,6 +42,11 @@ impl SpinnerVariant {
|
|||||||
match self {
|
match self {
|
||||||
SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
||||||
SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"],
|
SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"],
|
||||||
|
SpinnerVariant::Sand => vec![
|
||||||
|
"⠁", "⠂", "⠄", "⡀", "⡈", "⡐", "⡠", "⣀", "⣁", "⣂", "⣄", "⣌", "⣔", "⣤", "⣥", "⣦",
|
||||||
|
"⣮", "⣶", "⣷", "⣿", "⡿", "⠿", "⢟", "⠟", "⡛", "⠛", "⠫", "⢋", "⠋", "⠍", "⡉", "⠉",
|
||||||
|
"⠑", "⠡", "⢁",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +54,7 @@ impl SpinnerVariant {
|
|||||||
match self {
|
match self {
|
||||||
SpinnerVariant::Dots => Duration::from_millis(1000),
|
SpinnerVariant::Dots => Duration::from_millis(1000),
|
||||||
SpinnerVariant::DotsVariant => Duration::from_millis(1000),
|
SpinnerVariant::DotsVariant => Duration::from_millis(1000),
|
||||||
|
SpinnerVariant::Sand => Duration::from_millis(2000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +62,7 @@ impl SpinnerVariant {
|
|||||||
match self {
|
match self {
|
||||||
SpinnerVariant::Dots => "spinner_label_dots",
|
SpinnerVariant::Dots => "spinner_label_dots",
|
||||||
SpinnerVariant::DotsVariant => "spinner_label_dots_variant",
|
SpinnerVariant::DotsVariant => "spinner_label_dots_variant",
|
||||||
|
SpinnerVariant::Sand => "spinner_label_dots_variant_2",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +91,10 @@ impl SpinnerLabel {
|
|||||||
pub fn dots_variant() -> Self {
|
pub fn dots_variant() -> Self {
|
||||||
Self::with_variant(SpinnerVariant::DotsVariant)
|
Self::with_variant(SpinnerVariant::DotsVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sand() -> Self {
|
||||||
|
Self::with_variant(SpinnerVariant::Sand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LabelCommon for SpinnerLabel {
|
impl LabelCommon for SpinnerLabel {
|
||||||
@@ -185,6 +197,7 @@ impl Component for SpinnerLabel {
|
|||||||
"Dots Variant",
|
"Dots Variant",
|
||||||
SpinnerLabel::dots_variant().into_any_element(),
|
SpinnerLabel::dots_variant().into_any_element(),
|
||||||
),
|
),
|
||||||
|
single_example("Sand Variant", SpinnerLabel::sand().into_any_element()),
|
||||||
];
|
];
|
||||||
|
|
||||||
Some(example_group(examples).vertical().into_any_element())
|
Some(example_group(examples).vertical().into_any_element())
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ command-fds = "0.3.1"
|
|||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
nix = { workspace = true, features = ["user"] }
|
nix = { workspace = true, features = ["user"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
mach2.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
tendril = "0.4.3"
|
tendril = "0.4.3"
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,77 @@ pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
|||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
|
||||||
|
// Create a std::process::Command first so we can use pre_exec
|
||||||
|
let mut std_cmd = std::process::Command::new(program);
|
||||||
|
|
||||||
|
// WORKAROUND: Reset exception ports before exec to prevent inheritance of
|
||||||
|
// crash handler exception ports. Due to a timing issue, child processes can
|
||||||
|
// inherit the parent's exception ports before they're fully stabilized,
|
||||||
|
// which can block child process spawning.
|
||||||
|
// See: https://github.com/zed-industries/zed/issues/36754
|
||||||
|
unsafe {
|
||||||
|
std_cmd.pre_exec(|| {
|
||||||
|
// Reset all exception ports to system defaults for this task.
|
||||||
|
// This prevents the child from inheriting the parent's crash handler
|
||||||
|
// exception ports.
|
||||||
|
reset_exception_ports();
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to async_process::Command via From trait
|
||||||
|
smol::process::Command::from(std_cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
|
||||||
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||||
smol::process::Command::new(program)
|
smol::process::Command::new(program)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn reset_exception_ports() {
|
||||||
|
use mach2::exception_types::{
|
||||||
|
EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
|
||||||
|
};
|
||||||
|
use mach2::kern_return::{KERN_SUCCESS, kern_return_t};
|
||||||
|
use mach2::mach_types::task_t;
|
||||||
|
use mach2::port::{MACH_PORT_NULL, mach_port_t};
|
||||||
|
use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
|
||||||
|
use mach2::traps::mach_task_self;
|
||||||
|
|
||||||
|
// FFI binding for task_set_exception_ports (not exposed by mach2 crate)
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn task_set_exception_ports(
|
||||||
|
task: task_t,
|
||||||
|
exception_mask: exception_mask_t,
|
||||||
|
new_port: mach_port_t,
|
||||||
|
behavior: exception_behavior_t,
|
||||||
|
new_flavor: thread_state_flavor_t,
|
||||||
|
) -> kern_return_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let task = mach_task_self();
|
||||||
|
// Reset all exception ports to MACH_PORT_NULL (system default)
|
||||||
|
// This prevents the child process from inheriting the parent's crash handler
|
||||||
|
let kr = task_set_exception_ports(
|
||||||
|
task,
|
||||||
|
EXC_MASK_ALL,
|
||||||
|
MACH_PORT_NULL,
|
||||||
|
EXCEPTION_DEFAULT as exception_behavior_t,
|
||||||
|
THREAD_STATE_NONE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if kr != KERN_SUCCESS {
|
||||||
|
// Log but don't fail - the process can still work without this workaround
|
||||||
|
eprintln!(
|
||||||
|
"Warning: failed to reset exception ports in child process (kern_return: {})",
|
||||||
|
kr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.212.0"
|
version = "0.213.0"
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
authors = ["Zed Team <hi@zed.dev>"]
|
authors = ["Zed Team <hi@zed.dev>"]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ To debug code written in a specific language, Zed needs to find a debug adapter
|
|||||||
- [C](./languages/c.md#debugging) (built-in)
|
- [C](./languages/c.md#debugging) (built-in)
|
||||||
- [C++](./languages/cpp.md#debugging) (built-in)
|
- [C++](./languages/cpp.md#debugging) (built-in)
|
||||||
- [Go](./languages/go.md#debugging) (built-in)
|
- [Go](./languages/go.md#debugging) (built-in)
|
||||||
|
- [Java](./languages/java.md#debugging) (provided by extension)
|
||||||
- [JavaScript](./languages/javascript.md#debugging) (built-in)
|
- [JavaScript](./languages/javascript.md#debugging) (built-in)
|
||||||
- [PHP](./languages/php.md#debugging) (built-in)
|
- [PHP](./languages/php.md#debugging) (built-in)
|
||||||
- [Python](./languages/python.md#debugging) (built-in)
|
- [Python](./languages/python.md#debugging) (built-in)
|
||||||
|
|||||||
@@ -75,30 +75,30 @@ Zed supports machines with Intel (x86_64) or Apple (aarch64) processors that mee
|
|||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
Zed supports 64bit Intel/AMD (x86_64) and 64Bit ARM (aarch64) processors.
|
Zed supports 64-bit Intel/AMD (x86_64) and 64-bit Arm (aarch64) processors.
|
||||||
|
|
||||||
Zed requires a Vulkan 1.3 driver, and the following desktop portals:
|
Zed requires a Vulkan 1.3 driver and the following desktop portals:
|
||||||
|
|
||||||
- `org.freedesktop.portal.FileChooser`
|
- `org.freedesktop.portal.FileChooser`
|
||||||
- `org.freedesktop.portal.OpenURI`
|
- `org.freedesktop.portal.OpenURI`
|
||||||
- `org.freedesktop.portal.Secret`, or `org.freedesktop.Secrets`
|
- `org.freedesktop.portal.Secret` or `org.freedesktop.Secrets`
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
Zed supports the follow Windows releases:
|
Zed supports the following Windows releases:
|
||||||
| Version | Microsoft Status | Zed Status |
|
| Version | Zed Status |
|
||||||
| ------------------------- | ------------------ | ------------------- |
|
| ------------------------- | ------------------- |
|
||||||
| Windows 11 (all releases) | Supported | Supported |
|
| Windows 11, version 22H2 and later | Supported |
|
||||||
| Windows 10 (64-bit) | Supported | Supported |
|
| Windows 10, version 1903 and later | Supported |
|
||||||
|
|
||||||
|
A 64-bit operating system is required to run Zed.
|
||||||
|
|
||||||
#### Windows Hardware
|
#### Windows Hardware
|
||||||
|
|
||||||
Zed supports machines with Intel or AMD 64-bit (x86_64) processors that meet the above Windows requirements:
|
Zed supports machines with x64 (Intel, AMD) or Arm64 (Qualcomm) processors that meet the following requirements:
|
||||||
|
|
||||||
- Windows 11 (64-bit)
|
|
||||||
- Windows 10 (64-bit)
|
|
||||||
- Graphics: A GPU that supports DirectX 11 (most PCs from 2012+).
|
- Graphics: A GPU that supports DirectX 11 (most PCs from 2012+).
|
||||||
- Driver: Current NVIDIA/AMD/Intel driver (not the Microsoft Basic Display Adapter).
|
- Driver: Current NVIDIA/AMD/Intel/Qualcomm driver (not the Microsoft Basic Display Adapter).
|
||||||
|
|
||||||
### FreeBSD
|
### FreeBSD
|
||||||
|
|
||||||
|
|||||||
@@ -19,150 +19,149 @@ Or manually download and install [OpenJDK 23](https://jdk.java.net/23/).
|
|||||||
|
|
||||||
## Extension Install
|
## Extension Install
|
||||||
|
|
||||||
You can install either by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
|
You can install by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
|
||||||
|
|
||||||
## Settings / Initialization Options
|
## Quick start and configuration
|
||||||
|
|
||||||
The extension will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself.
|
For the majority of users, Java support should work out of the box.
|
||||||
|
|
||||||
For available `initialization_options` please see the [Initialize Request section of the Eclipse.jdt.ls Wiki](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request).
|
- It is generally recommended to open projects with the Zed-project root at the Java project root folder (where you would commonly have your `pom.xml` or `build.gradle` file).
|
||||||
|
|
||||||
You can add these customizations to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}) or by using a `.zed/setting.json` inside your project.
|
- By default the extension will download and run the latest official version of JDTLS for you, but this requires Java version 21 to be available on your system via either the `$JAVA_HOME` environment variable or as a `java(.exe)` executable on your `$PATH`. If your project requires a lower Java version in the environment, you can specify a different JDK to use for running JDTLS via the `java_home` configuration option.
|
||||||
|
|
||||||
### Zed Java Settings
|
- You can provide a **custom launch script for JDTLS**, by adding an executable named `jdtls` (or `jdtls.bat` on Windows) to your `$PATH` environment variable. If this is present, the extension will skip downloading and launching a managed instance and use the one from the environment.
|
||||||
|
|
||||||
```json [settings]
|
- To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`.
|
||||||
|
|
||||||
|
Here is a common `settings.json` including the above mentioned configurations:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
{
|
{
|
||||||
"lsp": {
|
"lsp": {
|
||||||
"jdtls": {
|
"jdtls": {
|
||||||
"initialization_options": {}
|
"settings": {
|
||||||
}
|
"java_home": "/path/to/your/JDK21+",
|
||||||
}
|
"lombok_support": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example Configs
|
## Debugging
|
||||||
|
|
||||||
### JDTLS Binary
|
Debug support is enabled via our [Fork of Java Debug](https://github.com/zed-industries/java-debug), which the extension will automatically download and start for you. Please refer to the [Debugger Documentation](https://zed.dev/docs/debugger#getting-started) for general information about how debugging works in Zed.
|
||||||
|
|
||||||
By default, zed will look in your `PATH` for a `jdtls` binary, if you wish to specify an explicit binary you can do so via settings:
|
To get started with Java, click the `edit debug.json` button in the Debug menu, and replace the contents of the file with the following:
|
||||||
|
|
||||||
```json [settings]
|
```jsonc
|
||||||
"lsp": {
|
[
|
||||||
"jdtls": {
|
{
|
||||||
"binary": {
|
"adapter": "Java",
|
||||||
"path": "/path/to/java/bin/jdtls",
|
"request": "launch",
|
||||||
// "arguments": [],
|
"label": "Launch Debugger",
|
||||||
// "env": {},
|
// if your project has multiple entry points, specify the one to use:
|
||||||
"ignore_system_version": true
|
// "mainClass": "com.myorganization.myproject.MyMainClass",
|
||||||
}
|
//
|
||||||
}
|
// this effectively sets a breakpoint at your program entry:
|
||||||
}
|
"stopOnEntry": true,
|
||||||
|
// the working directory for the debug process
|
||||||
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
},
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Zed Java Initialization Options
|
You should then be able to start a new Debug Session with the "Launch Debugger" scenario from the debug menu.
|
||||||
|
|
||||||
There are also many more options you can pass directly to the language server, for example:
|
## Launch Scripts (aka Tasks) in Windows
|
||||||
|
|
||||||
```json [settings]
|
This extension provides tasks for running your application and tests from within Zed via little play buttons next to tests/entry points. However, due to current limitations of Zed's extension interface, we can not provide scripts that will work across Maven and Gradle on both Windows and Unix-compatible systems, so out of the box the launch scripts only work on Mac and Linux.
|
||||||
|
|
||||||
|
There is a fairly straightforward fix that you can apply to make it work on Windows by supplying your own task scripts. Please see [this Issue](https://github.com/zed-extensions/java/issues/94) for information on how to do that and read the [Tasks section in Zeds documentation](https://zed.dev/docs/tasks) for more information.
|
||||||
|
|
||||||
|
## Advanced Configuration/JDTLS initialization Options
|
||||||
|
|
||||||
|
JDTLS provides many configuration options that can be passed via the `initialize` LSP-request. The extension will pass the JSON-object from `lsp.jdtls.settings.initialization_options` in your settings on to JDTLS. Please refer to the [JDTLS Configuration Wiki Page](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request) for the available options and values. Below is an example `settings.json` that would pass on the example configuration from the above wiki page to JDTLS:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
{
|
{
|
||||||
"lsp": {
|
"lsp": {
|
||||||
"jdtls": {
|
"jdtls": {
|
||||||
"initialization_options": {
|
"settings": {
|
||||||
"bundles": [],
|
// this will be sent to JDTLS as initializationOptions:
|
||||||
"workspaceFolders": ["file:///home/snjeza/Project"],
|
"initialization_options": {
|
||||||
"settings": {
|
"bundles": [],
|
||||||
"java": {
|
// use this if your zed project root folder is not the same as the java project root:
|
||||||
"home": "/usr/local/jdk-9.0.1",
|
"workspaceFolders": ["file:///home/snjeza/Project"],
|
||||||
"errors": {
|
"settings": {
|
||||||
"incompleteClasspath": {
|
"java": {
|
||||||
"severity": "warning"
|
"home": "/usr/local/jdk-9.0.1",
|
||||||
}
|
"errors": {
|
||||||
},
|
"incompleteClasspath": {
|
||||||
"configuration": {
|
"severity": "warning",
|
||||||
"updateBuildConfiguration": "interactive",
|
},
|
||||||
"maven": {
|
|
||||||
"userSettings": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"trace": {
|
|
||||||
"server": "verbose"
|
|
||||||
},
|
|
||||||
"import": {
|
|
||||||
"gradle": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
},
|
||||||
"maven": {
|
"configuration": {
|
||||||
"enabled": true
|
"updateBuildConfiguration": "interactive",
|
||||||
|
"maven": {
|
||||||
|
"userSettings": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"gradle": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
"maven": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
"exclusions": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/.metadata/**",
|
||||||
|
"**/archetype-resources/**",
|
||||||
|
"**/META-INF/maven/**",
|
||||||
|
"/**/test/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"referencesCodeLens": {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
|
"signatureHelp": {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
|
"implementationCodeLens": "all",
|
||||||
|
"format": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
"saveActions": {
|
||||||
|
"organizeImports": false,
|
||||||
|
},
|
||||||
|
"contentProvider": {
|
||||||
|
"preferred": null,
|
||||||
|
},
|
||||||
|
"autobuild": {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
|
"completion": {
|
||||||
|
"favoriteStaticMembers": [
|
||||||
|
"org.junit.Assert.*",
|
||||||
|
"org.junit.Assume.*",
|
||||||
|
"org.junit.jupiter.api.Assertions.*",
|
||||||
|
"org.junit.jupiter.api.Assumptions.*",
|
||||||
|
"org.junit.jupiter.api.DynamicContainer.*",
|
||||||
|
"org.junit.jupiter.api.DynamicTest.*",
|
||||||
|
],
|
||||||
|
"importOrder": ["java", "javax", "com", "org"],
|
||||||
},
|
},
|
||||||
"exclusions": [
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/.metadata/**",
|
|
||||||
"**/archetype-resources/**",
|
|
||||||
"**/META-INF/maven/**",
|
|
||||||
"/**/test/**"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"jdt": {
|
},
|
||||||
"ls": {
|
},
|
||||||
"lombokSupport": {
|
},
|
||||||
"enabled": false // Set this to true to enable lombok support
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
"referencesCodeLens": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"signatureHelp": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"implementationsCodeLens": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"saveActions": {
|
|
||||||
"organizeImports": false
|
|
||||||
},
|
|
||||||
"contentProvider": {
|
|
||||||
"preferred": null
|
|
||||||
},
|
|
||||||
"autobuild": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"completion": {
|
|
||||||
"favoriteStaticMembers": [
|
|
||||||
"org.junit.Assert.*",
|
|
||||||
"org.junit.Assume.*",
|
|
||||||
"org.junit.jupiter.api.Assertions.*",
|
|
||||||
"org.junit.jupiter.api.Assumptions.*",
|
|
||||||
"org.junit.jupiter.api.DynamicContainer.*",
|
|
||||||
"org.junit.jupiter.api.DynamicTest.*"
|
|
||||||
],
|
|
||||||
"importOrder": ["java", "javax", "com", "org"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual JDTLS Install
|
|
||||||
|
|
||||||
If you prefer, you can install JDTLS yourself and the extension can be configured to use that instead.
|
|
||||||
|
|
||||||
- macOS: `brew install jdtls`
|
|
||||||
- Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls)
|
|
||||||
|
|
||||||
Or manually download install:
|
|
||||||
|
|
||||||
- [JDTLS Milestone Builds](http://download.eclipse.org/jdtls/milestones/) (updated every two weeks)
|
|
||||||
- [JDTLS Snapshot Builds](https://download.eclipse.org/jdtls/snapshots/) (frequent updates)
|
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [Zed Java Repo](https://github.com/zed-extensions/java)
|
[Zed Java Repo](https://github.com/zed-extensions/java)
|
||||||
- [Zed Java Issues](https://github.com/zed-extensions/java/issues)
|
[Eclipse JDTLS Repo](https://github.com/eclipse-jdtls/eclipse.jdt.ls)
|
||||||
|
|||||||
@@ -71,6 +71,18 @@ To switch to `ruby-lsp`, add the following to your `settings.json`:
|
|||||||
"languages": {
|
"languages": {
|
||||||
"Ruby": {
|
"Ruby": {
|
||||||
"language_servers": ["ruby-lsp", "!solargraph", "!rubocop", "..."]
|
"language_servers": ["ruby-lsp", "!solargraph", "!rubocop", "..."]
|
||||||
|
},
|
||||||
|
// Enable herb and ruby-lsp for *.html.erb files
|
||||||
|
"HTML+ERB": {
|
||||||
|
"language_servers": ["herb", "ruby-lsp", "..."]
|
||||||
|
},
|
||||||
|
// Enable ruby-lsp for *.js.erb files
|
||||||
|
"JS+ERB": {
|
||||||
|
"language_servers": ["ruby-lsp", "..."]
|
||||||
|
},
|
||||||
|
// Enable ruby-lsp for *.yaml.erb files
|
||||||
|
"YAML+ERB": {
|
||||||
|
"language_servers": ["ruby-lsp", "..."]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ For detailed instructions on setting up and using remote development features, i
|
|||||||
|
|
||||||
### Zed fails to start or shows a blank window
|
### Zed fails to start or shows a blank window
|
||||||
|
|
||||||
- Update your GPU drivers from your GPU vendor (Intel/AMD/NVIDIA).
|
- Check that your hardware and operating system version are compatible with Zed. See our [installation guide](./installation.md) for more information.
|
||||||
|
- Update your GPU drivers from your GPU vendor (Intel/AMD/NVIDIA/Qualcomm).
|
||||||
- Ensure hardware acceleration is enabled in Windows and not blocked by third‑party software.
|
- Ensure hardware acceleration is enabled in Windows and not blocked by third‑party software.
|
||||||
- Try launching Zed with no extensions or custom settings to isolate conflicts.
|
- Try launching Zed with no extensions or custom settings to isolate conflicts.
|
||||||
|
|
||||||
@@ -39,14 +40,14 @@ When prompted for credentials, use the graphical askpass dialog. If it doesn’t
|
|||||||
|
|
||||||
#### Zed fails to open / degraded performance
|
#### Zed fails to open / degraded performance
|
||||||
|
|
||||||
Zed requires a DX11 compatible GPU to run, if Zed doesn't open for you it is possible that your GPU does not meet the minimum requirements.
|
Zed requires a DirectX 11 compatible GPU to run. If Zed fails to open, your GPU may not meet the minimum requirements.
|
||||||
|
|
||||||
To check if your GPU supports DX11, you can use the following command:
|
To check if your GPU supports DirectX 11, run the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
dxdiag
|
dxdiag
|
||||||
```
|
```
|
||||||
|
|
||||||
Which will open the diagnostic tool that will show the minimum DirectX version your GPU supports under `System` → `System Information` → `DirectX Version`.
|
This will open the DirectX Diagnostic Tool, which shows the DirectX version your GPU supports under `System` → `System Information` → `DirectX Version`.
|
||||||
|
|
||||||
You might also be trying to run Zed inside a virtual machine in which case it will use the emulated adapter that your VM provides, while Zed will work the performance will be degraded.
|
If you're running Zed inside a virtual machine, it will use the emulated adapter provided by your VM. While Zed will work in this environment, performance may be degraded.
|
||||||
|
|||||||
Reference in New Issue
Block a user