Compare commits
18 Commits
diff-perf-
...
thread-edi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
257d10f324 | ||
|
|
eccdfed32b | ||
|
|
2664596a34 | ||
|
|
23f2fb6089 | ||
|
|
fb2c2c55dc | ||
|
|
8315fde1ff | ||
|
|
fc87440682 | ||
|
|
c996eadaf5 | ||
|
|
e8c6c1ba04 | ||
|
|
b8364d7c33 | ||
|
|
7c23ef89ec | ||
|
|
2f463370cc | ||
|
|
feed34cafe | ||
|
|
4724aa5cb8 | ||
|
|
366a5db2c0 | ||
|
|
81e87c4cd6 | ||
|
|
b8ba663c20 | ||
|
|
27fb1098fa |
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -211,8 +211,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -228,9 +226,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -18618,6 +18614,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.5.0",
|
||||
"nix 0.29.0",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.2",
|
||||
@@ -21135,7 +21132,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.212.0"
|
||||
version = "0.213.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -440,7 +440,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
|
||||
agent-client-protocol = { path = "../agent-client-protocol", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
|
||||
@@ -735,14 +735,6 @@
|
||||
"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
|
||||
//
|
||||
// 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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && in_snippet",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-right": "editor::NextSnippetTabstop",
|
||||
"alt-left": "editor::PreviousSnippetTabstop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && edit_prediction",
|
||||
"bindings": {
|
||||
|
||||
@@ -739,14 +739,6 @@
|
||||
"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
|
||||
//
|
||||
// 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;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserMessage {
|
||||
pub struct UserMessage<T> {
|
||||
pub id: Option<UserMessageId>,
|
||||
pub content: ContentBlock,
|
||||
pub chunks: Vec<acp::ContentBlock>,
|
||||
pub chunks: Vec<acp::ContentBlock<T>>,
|
||||
pub checkpoint: Option<Checkpoint>,
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct Checkpoint {
|
||||
pub show: bool,
|
||||
}
|
||||
|
||||
impl UserMessage {
|
||||
impl<T> UserMessage<T> {
|
||||
fn to_markdown(&self, cx: &App) -> String {
|
||||
let mut markdown = String::new();
|
||||
if self
|
||||
@@ -116,13 +116,13 @@ impl AssistantMessageChunk {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AgentThreadEntry {
|
||||
UserMessage(UserMessage),
|
||||
pub enum AgentThreadEntry<T> {
|
||||
UserMessage(UserMessage<T>),
|
||||
AssistantMessage(AssistantMessage),
|
||||
ToolCall(ToolCall),
|
||||
}
|
||||
|
||||
impl AgentThreadEntry {
|
||||
impl<T> AgentThreadEntry<T> {
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
match self {
|
||||
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 {
|
||||
Some(message)
|
||||
} else {
|
||||
@@ -802,9 +802,11 @@ pub struct RetryStatus {
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
pub struct AcpThread {
|
||||
title: SharedString,
|
||||
entries: Vec<AgentThreadEntry>,
|
||||
pub struct AnchoredText;
|
||||
|
||||
pub struct AcpThread<T = SharedString> {
|
||||
title: T,
|
||||
entries: Vec<AgentThreadEntry<AnchoredText>>,
|
||||
plan: Plan,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
@@ -1002,7 +1004,7 @@ impl Display for LoadError {
|
||||
|
||||
impl Error for LoadError {}
|
||||
|
||||
impl AcpThread {
|
||||
impl<T> AcpThread<T> {
|
||||
pub fn new(
|
||||
title: impl Into<SharedString>,
|
||||
connection: Rc<dyn AgentConnection>,
|
||||
@@ -1152,7 +1154,7 @@ impl AcpThread {
|
||||
pub fn push_user_content_block(
|
||||
&mut self,
|
||||
message_id: Option<UserMessageId>,
|
||||
chunk: acp::ContentBlock,
|
||||
chunk: acp::ContentBlock<T>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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);
|
||||
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
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
|
||||
@@ -4,6 +4,7 @@ mod message_editor;
|
||||
mod mode_selector;
|
||||
mod model_selector;
|
||||
mod model_selector_popover;
|
||||
mod thread_editor;
|
||||
mod thread_history;
|
||||
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();
|
||||
|
||||
// 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
|
||||
let login = task::SpawnInTerminal {
|
||||
id: task::TaskId(format!("external-agent-{}-login", label)),
|
||||
@@ -1514,6 +1520,7 @@ impl AcpThreadView {
|
||||
command: Some(command.to_string()),
|
||||
args,
|
||||
command_label: label.to_string(),
|
||||
cwd,
|
||||
env,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
@@ -1526,8 +1533,9 @@ impl AcpThreadView {
|
||||
pending_auth_method.replace(method.clone());
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = self.project.clone();
|
||||
let authenticate = Self::spawn_external_agent_login(
|
||||
login, workspace, false, window, cx,
|
||||
login, workspace, project, false, true, window, cx,
|
||||
);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
@@ -1671,7 +1679,10 @@ impl AcpThreadView {
|
||||
&& let Some(login) = self.login.clone()
|
||||
{
|
||||
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 {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -1721,17 +1732,40 @@ impl AcpThreadView {
|
||||
fn spawn_external_agent_login(
|
||||
login: task::SpawnInTerminal,
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
previous_attempt: bool,
|
||||
check_exit_code: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
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 {
|
||||
program: task.command.take().expect("login command should be set"),
|
||||
args: std::mem::take(&mut task.args),
|
||||
@@ -1749,44 +1783,65 @@ impl AcpThreadView {
|
||||
})?;
|
||||
|
||||
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
|
||||
.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(());
|
||||
if check_exit_code {
|
||||
// For extension-based auth, wait for the process to exit and check exit code
|
||||
let exit_status = terminal
|
||||
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.await;
|
||||
|
||||
match exit_status {
|
||||
Some(status) if status.success() => {
|
||||
Ok(())
|
||||
}
|
||||
Some(status) => {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
})
|
||||
.fuse();
|
||||
futures::pin_mut!(logged_in);
|
||||
futures::select_biased! {
|
||||
result = logged_in => {
|
||||
if let Err(e) = result {
|
||||
log::error!("{e}");
|
||||
_ = exit_status => {
|
||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
||||
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await
|
||||
}
|
||||
return Err(anyhow!("exited before logging in"));
|
||||
}
|
||||
}
|
||||
_ = exit_status => {
|
||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
||||
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(())
|
||||
}
|
||||
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2081,7 +2081,7 @@ impl AgentPanel {
|
||||
let mut entry =
|
||||
ContextMenuEntry::new(format!("New {}", agent_name));
|
||||
if let Some(icon_path) = icon_path {
|
||||
entry = entry.custom_icon_path(icon_path);
|
||||
entry = entry.custom_icon_svg(icon_path);
|
||||
} else {
|
||||
entry = entry.icon(IconName::Terminal);
|
||||
}
|
||||
@@ -2150,7 +2150,7 @@ impl AgentPanel {
|
||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||
let label = selected_agent_label.clone();
|
||||
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::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
})
|
||||
|
||||
@@ -477,7 +477,7 @@ impl TextThreadEditor {
|
||||
editor.insert(&format!("/{name}"), window, cx);
|
||||
if command.accepts_arguments() {
|
||||
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 channel::{Channel, ChannelEvent, ChannelStore};
|
||||
use client::{ChannelId, Client, Contact, User, UserStore};
|
||||
use collections::{HashMap, HashSet};
|
||||
use contact_finder::ContactFinder;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
|
||||
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
|
||||
@@ -30,9 +31,9 @@ use smallvec::SmallVec;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, Icon, IconButton,
|
||||
IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip, prelude::*,
|
||||
tooltip_container,
|
||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
|
||||
prelude::*, tooltip_container,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::{
|
||||
@@ -261,6 +262,8 @@ enum ListEntry {
|
||||
channel: Arc<Channel>,
|
||||
depth: usize,
|
||||
has_children: bool,
|
||||
// `None` when the channel is a parent of a matched channel.
|
||||
string_match: Option<StringMatch>,
|
||||
},
|
||||
ChannelNotes {
|
||||
channel_id: ChannelId,
|
||||
@@ -630,6 +633,10 @@ impl CollabPanel {
|
||||
.enumerate()
|
||||
.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(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -639,14 +646,34 @@ impl CollabPanel {
|
||||
&Default::default(),
|
||||
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
|
||||
&& matches!(state, ChannelEditingState::Create { location: None, .. })
|
||||
{
|
||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||
}
|
||||
let mut collapse_depth = None;
|
||||
for mat in matches {
|
||||
let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
|
||||
for (idx, channel) in channels.into_iter().enumerate() {
|
||||
let depth = channel.parent_path.len();
|
||||
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
@@ -663,7 +690,7 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
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]));
|
||||
|
||||
match &self.channel_editing_state {
|
||||
@@ -675,6 +702,7 @@ impl CollabPanel {
|
||||
channel: channel.clone(),
|
||||
depth,
|
||||
has_children: false,
|
||||
string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
|
||||
});
|
||||
self.entries
|
||||
.push(ListEntry::ChannelEditor { depth: depth + 1 });
|
||||
@@ -690,6 +718,7 @@ impl CollabPanel {
|
||||
channel: channel.clone(),
|
||||
depth,
|
||||
has_children,
|
||||
string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2321,8 +2350,17 @@ impl CollabPanel {
|
||||
channel,
|
||||
depth,
|
||||
has_children,
|
||||
string_match,
|
||||
} => 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(),
|
||||
ListEntry::ChannelEditor { depth } => self
|
||||
.render_channel_editor(*depth, window, cx)
|
||||
@@ -2719,6 +2757,7 @@ impl CollabPanel {
|
||||
has_children: bool,
|
||||
is_selected: bool,
|
||||
ix: usize,
|
||||
string_match: Option<&StringMatch>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let channel_id = channel.id;
|
||||
@@ -2855,7 +2894,14 @@ impl CollabPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.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())),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -265,9 +265,10 @@ impl minidumper::ServerHandler for CrashServer {
|
||||
3 => {
|
||||
let gpu_specs: system_specs::GpuSpecs =
|
||||
bincode::deserialize(&buffer).expect("gpu specs");
|
||||
self.active_gpu
|
||||
.set(gpu_specs)
|
||||
.expect("already set active gpu");
|
||||
// we ignore the case where it was already set because this message is sent
|
||||
// on each new window. in theory all zed windows should be using the same
|
||||
// GPU so this is fine.
|
||||
self.active_gpu.set(gpu_specs).ok();
|
||||
}
|
||||
_ => {
|
||||
panic!("invalid message kind");
|
||||
|
||||
@@ -213,6 +213,15 @@ pub struct ExpandExcerptsDown {
|
||||
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.
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
@@ -612,8 +621,6 @@ actions!(
|
||||
NextEditPrediction,
|
||||
/// Scrolls to the next screen.
|
||||
NextScreen,
|
||||
/// Goes to the next snippet tabstop if one exists.
|
||||
NextSnippetTabstop,
|
||||
/// Opens the context menu at cursor position.
|
||||
OpenContextMenu,
|
||||
/// Opens excerpts from the current file.
|
||||
@@ -647,8 +654,6 @@ actions!(
|
||||
Paste,
|
||||
/// Navigates to the previous edit prediction.
|
||||
PreviousEditPrediction,
|
||||
/// Goes to the previous snippet tabstop if one exists.
|
||||
PreviousSnippetTabstop,
|
||||
/// Redoes the last undone edit.
|
||||
Redo,
|
||||
/// Redoes the last selection change.
|
||||
@@ -727,8 +732,6 @@ actions!(
|
||||
SelectToStartOfParagraph,
|
||||
/// Extends selection up.
|
||||
SelectUp,
|
||||
/// Shows code completion suggestions at the cursor position.
|
||||
ShowCompletions,
|
||||
/// Shows the system character palette.
|
||||
ShowCharacterPalette,
|
||||
/// Shows edit prediction at cursor.
|
||||
|
||||
@@ -252,17 +252,8 @@ enum MarkdownCacheKey {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CompletionsMenuSource {
|
||||
/// Show all completions (words, snippets, LSP)
|
||||
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,
|
||||
/// Show only words (not snippets or LSP)
|
||||
///
|
||||
/// Used when word completions are explicitly triggered
|
||||
Words { ignore_threshold: bool },
|
||||
}
|
||||
|
||||
|
||||
@@ -2281,16 +2281,32 @@ impl Editor {
|
||||
|editor, _, e: &EditorEvent, window, cx| match e {
|
||||
EditorEvent::ScrollPositionChanged { 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.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 { .. } => {
|
||||
@@ -2461,10 +2477,6 @@ impl Editor {
|
||||
key_context.add("renaming");
|
||||
}
|
||||
|
||||
if !self.snippet_stack.is_empty() {
|
||||
key_context.add("in_snippet");
|
||||
}
|
||||
|
||||
match self.context_menu.borrow().as_ref() {
|
||||
Some(CodeContextMenu::Completions(menu)) => {
|
||||
if menu.visible() {
|
||||
@@ -3142,7 +3154,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
if continue_showing {
|
||||
self.open_or_update_completions_menu(None, None, false, window, cx);
|
||||
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
} else {
|
||||
self.hide_context_menu(window, cx);
|
||||
}
|
||||
@@ -4972,18 +4984,57 @@ impl Editor {
|
||||
ignore_threshold: false,
|
||||
}),
|
||||
None,
|
||||
trigger_in_words,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => self.open_or_update_completions_menu(
|
||||
None,
|
||||
Some(text.to_owned()).filter(|x| !x.is_empty()),
|
||||
true,
|
||||
window,
|
||||
Some(CompletionsMenuSource::Normal)
|
||||
| Some(CompletionsMenuSource::SnippetChoices)
|
||||
| None
|
||||
if self.is_completion_trigger(
|
||||
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,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5261,7 +5312,6 @@ impl Editor {
|
||||
ignore_threshold: true,
|
||||
}),
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -5269,18 +5319,17 @@ impl Editor {
|
||||
|
||||
pub fn show_completions(
|
||||
&mut self,
|
||||
_: &ShowCompletions,
|
||||
options: &ShowCompletions,
|
||||
window: &mut Window,
|
||||
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(
|
||||
&mut self,
|
||||
requested_source: Option<CompletionsMenuSource>,
|
||||
trigger: Option<String>,
|
||||
trigger_in_words: bool,
|
||||
trigger: Option<&str>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -5288,15 +5337,6 @@ impl Editor {
|
||||
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);
|
||||
|
||||
// Typically `start` == `end`, but with snippet tabstop choices the default choice is
|
||||
@@ -5344,8 +5384,7 @@ impl Editor {
|
||||
ignore_word_threshold = ignore_threshold;
|
||||
None
|
||||
}
|
||||
Some(CompletionsMenuSource::SnippetChoices)
|
||||
| Some(CompletionsMenuSource::SnippetsOnly) => {
|
||||
Some(CompletionsMenuSource::SnippetChoices) => {
|
||||
log::error!("bug: SnippetChoices requested_source is not handled");
|
||||
None
|
||||
}
|
||||
@@ -5359,19 +5398,13 @@ impl Editor {
|
||||
.as_ref()
|
||||
.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 filter_completions {
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
}
|
||||
// When `is_incomplete` is false, no need to re-query completions when the current query
|
||||
// is a suffix of the initial query.
|
||||
let was_complete = !menu.is_incomplete;
|
||||
if was_complete && !was_snippets_only {
|
||||
if !menu.is_incomplete {
|
||||
// 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.
|
||||
//
|
||||
@@ -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 {
|
||||
excerpt_id: buffer_excerpt_id,
|
||||
text_anchor: buffer_position,
|
||||
@@ -5452,72 +5502,49 @@ impl Editor {
|
||||
&& match &query {
|
||||
Some(query) => query.chars().count() < completion_settings.words_min_length,
|
||||
None => completion_settings.words_min_length != 0,
|
||||
})
|
||||
|| (provider.is_some() && completion_settings.words == WordsCompletionMode::Disabled);
|
||||
});
|
||||
|
||||
let mut words = if omit_word_completions {
|
||||
Task::ready(BTreeMap::default())
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
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(
|
||||
let (mut words, provider_responses) = match &provider {
|
||||
Some(provider) => {
|
||||
let provider_responses = provider.completions(
|
||||
buffer_excerpt_id,
|
||||
&buffer,
|
||||
position.text_anchor,
|
||||
trigger,
|
||||
trigger_in_words,
|
||||
completions_source.is_some(),
|
||||
buffer_position,
|
||||
completion_context,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
);
|
||||
|
||||
let provider_responses = if let Some(provider) = &provider
|
||||
&& load_provider_completions
|
||||
{
|
||||
let trigger_character =
|
||||
trigger.filter(|trigger| buffer.read(cx).completion_triggers().contains(trigger));
|
||||
let completion_context = CompletionContext {
|
||||
trigger_kind: match &trigger_character {
|
||||
Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
None => CompletionTriggerKind::INVOKED,
|
||||
},
|
||||
trigger_character,
|
||||
};
|
||||
let words = match (omit_word_completions, completion_settings.words) {
|
||||
(true, _) | (_, WordsCompletionMode::Disabled) => {
|
||||
Task::ready(BTreeMap::default())
|
||||
}
|
||||
(false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
|
||||
.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
provider.completions(
|
||||
buffer_excerpt_id,
|
||||
&buffer,
|
||||
buffer_position,
|
||||
completion_context,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
};
|
||||
|
||||
let snippets = if let Some(provider) = &provider
|
||||
&& provider.show_snippets()
|
||||
&& let Some(project) = self.project()
|
||||
{
|
||||
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,
|
||||
}))
|
||||
(words, provider_responses)
|
||||
}
|
||||
None => {
|
||||
let words = if omit_word_completions {
|
||||
Task::ready(BTreeMap::default())
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
})
|
||||
};
|
||||
(words, Task::ready(Ok(Vec::new())))
|
||||
}
|
||||
};
|
||||
|
||||
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
||||
@@ -5575,13 +5602,6 @@ impl Editor {
|
||||
confirm: None,
|
||||
}));
|
||||
|
||||
completions.extend(
|
||||
snippets
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions),
|
||||
);
|
||||
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -5593,11 +5613,7 @@ impl Editor {
|
||||
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
||||
let menu = CompletionsMenu::new(
|
||||
id,
|
||||
requested_source.unwrap_or(if load_provider_completions {
|
||||
CompletionsMenuSource::Normal
|
||||
} else {
|
||||
CompletionsMenuSource::SnippetsOnly
|
||||
}),
|
||||
requested_source.unwrap_or(CompletionsMenuSource::Normal),
|
||||
sort_completions,
|
||||
show_completion_documentation,
|
||||
position,
|
||||
@@ -5927,7 +5943,7 @@ impl Editor {
|
||||
.as_ref()
|
||||
.is_some_and(|confirm| confirm(intent, window, cx));
|
||||
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()?;
|
||||
@@ -9975,38 +9991,6 @@ impl Editor {
|
||||
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>) {
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
@@ -12719,10 +12703,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
// 🤔 | .. | show_in_menu |
|
||||
// | .. | true true
|
||||
// | had_edit_prediction | false true
|
||||
|
||||
let trigger_in_words =
|
||||
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
|
||||
|
||||
@@ -22924,10 +22904,6 @@ pub trait CompletionProvider {
|
||||
fn filter_completions(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn show_snippets(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
@@ -23188,8 +23164,16 @@ impl CompletionProvider for Entity<Project> {
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
let task = project.completions(buffer, buffer_position, options, cx);
|
||||
cx.background_spawn(task)
|
||||
let snippets = snippet_completions(project, buffer, buffer_position, cx);
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23261,10 +23245,6 @@ impl CompletionProvider for Entity<Project> {
|
||||
|
||||
buffer.completion_triggers().contains(text)
|
||||
}
|
||||
|
||||
fn show_snippets(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
async fn test_snippets(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -13883,7 +13760,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||
|
||||
cx.set_state(&run.initial_state);
|
||||
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));
|
||||
@@ -13943,7 +13820,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||
|
||||
cx.set_state(initial_state);
|
||||
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));
|
||||
@@ -13979,7 +13856,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||
|
||||
cx.set_state(initial_state);
|
||||
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(
|
||||
&mut cx,
|
||||
@@ -14066,7 +13943,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
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(
|
||||
&mut cx,
|
||||
@@ -14120,7 +13997,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
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(
|
||||
&mut cx,
|
||||
@@ -14169,7 +14046,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
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(
|
||||
&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.show_completions(&ShowCompletions, window, cx);
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
|
||||
fake_server
|
||||
@@ -14559,7 +14436,7 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state("editor.cloˇ");
|
||||
assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions, window, cx);
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
handle_completion_request(
|
||||
"editor.<clo|>",
|
||||
@@ -14958,7 +14835,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
||||
4.5f32
|
||||
"});
|
||||
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.condition(|editor, _| editor.context_menu_visible())
|
||||
@@ -14984,7 +14861,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
||||
33.35f32
|
||||
"});
|
||||
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.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.executor().run_until_parked();
|
||||
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();
|
||||
|
||||
@@ -15506,7 +15389,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
|
||||
})))
|
||||
});
|
||||
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.update_editor(|editor, window, cx| {
|
||||
@@ -15555,7 +15438,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
|
||||
})))
|
||||
});
|
||||
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.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| {
|
||||
editor.show_completions(&ShowCompletions, window, cx);
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
completion_requests.next().await;
|
||||
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.show_completions(&ShowCompletions, window, cx);
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
completion_handle.next().await.unwrap();
|
||||
|
||||
@@ -232,8 +232,6 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::blame_hover);
|
||||
register_action(editor, window, Editor::delete);
|
||||
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::indent);
|
||||
register_action(editor, window, Editor::outdent);
|
||||
@@ -4006,41 +4004,51 @@ impl EditorElement {
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
.child(h_flex().gap_2().map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon = FileIcons::get_icon(path, cx)
|
||||
.unwrap_or_default();
|
||||
let icon =
|
||||
Icon::from_path(icon).color(Color::Muted);
|
||||
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()
|
||||
})
|
||||
},
|
||||
))
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon =
|
||||
FileIcons::get_icon(path, cx).unwrap_or_default();
|
||||
let icon = Icon::from_path(icon).color(Color::Muted);
|
||||
el.child(icon)
|
||||
})
|
||||
.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| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
@@ -4049,33 +4057,47 @@ impl EditorElement {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
ButtonLike::new("open-file-button")
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2p5()
|
||||
.child(Label::new("Open file"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&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_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
move |editor, _e: &ClickEvent, _window, cx| {
|
||||
if is_folded {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
} else {
|
||||
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(
|
||||
snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
|
||||
@@ -603,7 +603,7 @@ impl Editor {
|
||||
scroll_position
|
||||
};
|
||||
|
||||
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
|
||||
self.scroll_manager.set_scroll_position(
|
||||
adjusted_position,
|
||||
&display_map,
|
||||
local,
|
||||
@@ -611,22 +611,7 @@ impl Editor {
|
||||
workspace_id,
|
||||
window,
|
||||
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> {
|
||||
|
||||
@@ -173,6 +173,10 @@ pub struct AgentServerManifestEntry {
|
||||
/// cmd = "node"
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
|
||||
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,
|
||||
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
|
||||
radians, size,
|
||||
@@ -11,6 +13,7 @@ pub struct Svg {
|
||||
interactivity: Interactivity,
|
||||
transformation: Option<Transformation>,
|
||||
path: Option<SharedString>,
|
||||
external_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// Create a new SVG element.
|
||||
@@ -20,6 +23,7 @@ pub fn svg() -> Svg {
|
||||
interactivity: Interactivity::new(),
|
||||
transformation: None,
|
||||
path: None,
|
||||
external_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +34,12 @@ impl Svg {
|
||||
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.
|
||||
/// 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 {
|
||||
@@ -117,7 +127,35 @@ impl Element for Svg {
|
||||
.unwrap_or_default();
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
@@ -219,3 +257,21 @@ impl Transformation {
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
pub fn command_shift() -> 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() {
|
||||
if let Some(item) = try_clipboard_image(pasteboard, format) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
let platform = MacPlatform::new(false);
|
||||
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,
|
||||
// or macOS shortcuts like cmd-` will stop working.
|
||||
if key_equivalent {
|
||||
// Don't send key equivalents to the input handler if there are key modifiers other
|
||||
// than Function key, or macOS shortcuts like cmd-` will stop working.
|
||||
if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
@@ -1370,7 +1370,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
||||
scan_code as u32,
|
||||
Some(&keyboard_state),
|
||||
&mut buffer_c,
|
||||
0x4,
|
||||
0x5,
|
||||
)
|
||||
};
|
||||
if result_c < 0 {
|
||||
@@ -1415,7 +1415,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
||||
scan_code as u32,
|
||||
Some(&state_no_modifiers),
|
||||
&mut buffer_c_no_modifiers,
|
||||
0x4,
|
||||
0x5,
|
||||
)
|
||||
};
|
||||
if result_c_no_modifiers <= 0 {
|
||||
|
||||
@@ -95,27 +95,34 @@ impl SvgRenderer {
|
||||
pub(crate) fn render_alpha_mask(
|
||||
&self,
|
||||
params: &RenderSvgParams,
|
||||
bytes: Option<&[u8]>,
|
||||
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
|
||||
anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
|
||||
|
||||
// Load the tree.
|
||||
let Some(bytes) = self.asset_source.load(¶ms.path)? else {
|
||||
return Ok(None);
|
||||
let render_pixmap = |bytes| {
|
||||
let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?;
|
||||
|
||||
// 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))?;
|
||||
|
||||
// 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)))
|
||||
if let Some(bytes) = bytes {
|
||||
render_pixmap(bytes)
|
||||
} else if let Some(bytes) = self.asset_source.load(¶ms.path)? {
|
||||
render_pixmap(&bytes)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
||||
|
||||
@@ -3084,6 +3084,7 @@ impl Window {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
path: SharedString,
|
||||
mut data: Option<&[u8]>,
|
||||
transformation: TransformationMatrix,
|
||||
color: Hsla,
|
||||
cx: &App,
|
||||
@@ -3104,7 +3105,8 @@ impl Window {
|
||||
let Some(tile) =
|
||||
self.sprite_atlas
|
||||
.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);
|
||||
};
|
||||
Ok(Some((size, Cow::Owned(bytes))))
|
||||
|
||||
@@ -376,7 +376,7 @@ struct ManagedNodeRuntime {
|
||||
}
|
||||
|
||||
impl ManagedNodeRuntime {
|
||||
const VERSION: &str = "v22.5.1";
|
||||
const VERSION: &str = "v24.11.0";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NODE_PATH: &str = "bin/node";
|
||||
|
||||
@@ -438,6 +438,13 @@ impl AgentServerStore {
|
||||
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(
|
||||
node_runtime: NodeRuntime,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -1560,7 +1567,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
|
||||
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"]);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_tilde_expansion_in_settings() {
|
||||
let settings = settings::BuiltinAgentServerSettings {
|
||||
|
||||
@@ -891,7 +891,7 @@ impl SettingsPageItem {
|
||||
.px_8()
|
||||
.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()))
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct ContextMenuEntry {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
custom_icon_path: Option<SharedString>,
|
||||
custom_icon_svg: Option<SharedString>,
|
||||
icon_position: IconPosition,
|
||||
icon_size: IconSize,
|
||||
icon_color: Option<Color>,
|
||||
@@ -68,6 +69,7 @@ impl ContextMenuEntry {
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::Start,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -94,7 +96,15 @@ impl ContextMenuEntry {
|
||||
|
||||
pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -396,6 +406,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -425,6 +436,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -454,6 +466,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -482,6 +495,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: position,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -541,6 +555,7 @@ impl ContextMenu {
|
||||
}),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -572,6 +587,7 @@ impl ContextMenu {
|
||||
}),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_size: IconSize::Small,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
@@ -593,6 +609,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
||||
icon: Some(IconName::ArrowUpRight),
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_size: IconSize::XSmall,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
@@ -913,6 +930,7 @@ impl ContextMenu {
|
||||
handler,
|
||||
icon,
|
||||
custom_icon_path,
|
||||
custom_icon_svg,
|
||||
icon_position,
|
||||
icon_size,
|
||||
icon_color,
|
||||
@@ -965,6 +983,28 @@ impl ContextMenu {
|
||||
)
|
||||
})
|
||||
.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 {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
|
||||
@@ -115,24 +115,24 @@ impl From<IconName> for Icon {
|
||||
/// The source of an icon.
|
||||
enum IconSource {
|
||||
/// An SVG embedded in the Zed binary.
|
||||
Svg(SharedString),
|
||||
Embedded(SharedString),
|
||||
/// An image file located at the specified path.
|
||||
///
|
||||
/// Currently our SVG renderer is missing support for the following features:
|
||||
/// 1. Loading SVGs from external files.
|
||||
/// 2. Rendering polychrome SVGs.
|
||||
/// Currently our SVG renderer is missing support for rendering polychrome SVGs.
|
||||
///
|
||||
/// 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 {
|
||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
if path.starts_with("icons/") {
|
||||
Self::Svg(path)
|
||||
Self::Embedded(path)
|
||||
} 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 {
|
||||
pub fn new(icon: IconName) -> Self {
|
||||
Self {
|
||||
source: IconSource::Svg(icon.path().into()),
|
||||
source: IconSource::Embedded(icon.path().into()),
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
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 {
|
||||
self.color = color;
|
||||
self
|
||||
@@ -193,14 +202,21 @@ impl Transformable for Icon {
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
match self.source {
|
||||
IconSource::Svg(path) => svg()
|
||||
IconSource::Embedded(path) => svg()
|
||||
.with_transformation(self.transformation)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.path(path)
|
||||
.text_color(self.color.color(cx))
|
||||
.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)
|
||||
.flex_none()
|
||||
.text_color(self.color.color(cx))
|
||||
|
||||
@@ -51,6 +51,9 @@ command-fds = "0.3.1"
|
||||
libc.workspace = true
|
||||
nix = { workspace = true, features = ["user"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
mach2.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tendril = "0.4.3"
|
||||
|
||||
|
||||
@@ -26,7 +26,77 @@ pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::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 {
|
||||
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."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.212.0"
|
||||
version = "0.213.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -75,30 +75,30 @@ Zed supports machines with Intel (x86_64) or Apple (aarch64) processors that mee
|
||||
|
||||
### 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.OpenURI`
|
||||
- `org.freedesktop.portal.Secret`, or `org.freedesktop.Secrets`
|
||||
- `org.freedesktop.portal.Secret` or `org.freedesktop.Secrets`
|
||||
|
||||
### Windows
|
||||
|
||||
Zed supports the follow Windows releases:
|
||||
| Version | Microsoft Status | Zed Status |
|
||||
| ------------------------- | ------------------ | ------------------- |
|
||||
| Windows 11 (all releases) | Supported | Supported |
|
||||
| Windows 10 (64-bit) | Supported | Supported |
|
||||
Zed supports the following Windows releases:
|
||||
| Version | Zed Status |
|
||||
| ------------------------- | ------------------- |
|
||||
| Windows 11, version 22H2 and later | Supported |
|
||||
| Windows 10, version 1903 and later | Supported |
|
||||
|
||||
A 64-bit operating system is required to run Zed.
|
||||
|
||||
#### 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+).
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
- 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.
|
||||
- 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 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
|
||||
```
|
||||
|
||||
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