Compare commits

...

18 Commits

Author SHA1 Message Date
Nathan Sobo
257d10f324 This commit is locking up diagnostics on Zed eccdfed32b 2025-11-05 20:15:59 -07:00
Sean Timm
eccdfed32b gpui: Convert macOS clipboard file URLs to paths for paste (#36848)
- On macOS, pasting now inserts the actual file path when the clipboard
contains a file URL (public.file-url/public.url)
- Terminal paste remains text-only; no temp files or data URLs are
created. If only raw image bytes exist on the clipboard, paste is a
no-op.
- Scope: macOS only; no dependency changes.
- Added a test (test_file_url_converts_to_path) that verifies URL→path
conversion using a unique pasteboard.

Release Notes:

- Improved pasting on macOS: now inserts the actual file path when the
clipboard contains a file URL (enables image paste support for Claude
Code)
2025-11-06 00:35:52 +00:00
Cyandev
2664596a34 gpui: Fix incorrect handling of Function key modifier on macOS (#38518)
On macOS, the Function key is reserved for system use and should not be
used in application code.

This commit updated keystroke matching and key event handling to ignore
the Function key modifier while users are typing or pressing
keybindings.

For some keyboards with compact layout (like my 65% keyboard), there is
no separated backtick key. Esc and it shares the same physical key. To
input backtick, users may press `Fn-Esc`. However, macOS will still
deliver events with Fn key modifier to applications. Cocoa framework can
handle this correctly, which typically ignore the Fn directly. GPUI
should also follow the same rule, otherwise, the backtick key on those
keyboards won't work.

Release Notes:

- Fixed a bug where typing fn-\` on macOS would not insert a `.
2025-11-05 23:19:32 +00:00
Richard Feldman
23f2fb6089 Run ACP login from same cwd as agent server (#42038)
This makes it possible to do login via things like `cmd: "node", args:
["my-node-file.js", "login"]`

Also, that command will now use Zed's managed `node` instance.

Release Notes:

- ACP extensions can now run terminal login commands using relative
paths
2025-11-05 18:17:50 -05:00
Julia Ryan
fb2c2c55dc Fix windows crash handler (#42039)
Closes #41471

We were killing the crash handler when it received a second copy of any
of the messages, but this GPU specs one is sent on each new window
rather than once at startup. We could gate the sending to only happen
once, but it's simpler to just allow multiple gpu specs messages.

Release Notes:

- N/A
2025-11-05 22:34:05 +00:00
Rémi Kalbe
8315fde1ff Fix LSP spawning by resetting exception ports in child processes (#40716)
## Summary

Fixes #36754

This PR fixes an issue where LSPs fail to spawn after the crash handler
is initialized.

## Problem

After PR #35263 added minidump crash reporting, some users experienced
LSP spawn failures. The issue manifests as:
- LSPs fail to spawn with no clear error messages
- The problem only occurs after crash handler initialization
- LSPs work when a debugger is attached, revealing a timing issue

### Root Cause

The crash handler installs Mach exception ports for minidump generation.
Due to a timing issue, child processes inherit these exception ports
before they're fully stabilized, which can block child process spawning.

## Solution

Reset exception ports in child processes using the `pre_exec()` hook,
which runs after `fork()` but before `exec()`. This prevents children
from inheriting the parent's crash handler exception ports.

### Implementation

- Adds macOS-specific implementation of `new_smol_command()` that resets
exception ports before exec
- Calls `task_set_exception_ports` to reset all exception ports to
`MACH_PORT_NULL`
- Graceful error handling: logs warnings but doesn't fail process
spawning if port reset fails

Release Notes:

- Fixed LSPs failing to spawn on some macOS systems

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-11-05 22:05:34 +00:00
John Tur
fc87440682 Update Windows docs (#41423)
- Document Arm64 support
- Document minimum Windows version requirements

Release Notes:

- N/A
2025-11-05 21:23:48 +00:00
Kirill Bulatov
c996eadaf5 Update editor data only after real scroll reports (#42035)
Release Notes:

- N/A
2025-11-05 21:22:29 +00:00
Danilo Leal
e8c6c1ba04 agent_ui: Fix how icons from external agents are displayed (#42034)
Release Notes:

- N/A
2025-11-05 21:14:16 +00:00
versecafe
b8364d7c33 node: Move managed runtime to v24 LTS (#41956)
Release Notes:

- Moved managed Node runtime to v24 LTS
2025-11-05 14:10:42 -07:00
John Tur
7c23ef89ec Fix corrupted characters being inserted when Alt is pressed (#42033)
The Alt+Numpad buffer that's maintained by the input stack is getting
corrupted, leading to garbage characters being inserted on keystrokes
like Alt+Up. Disable the automatic handling of Alt+Numpad for now until
the cause of this corruption is understood. The Alt+Numpad input did not
work anyway, so this does not regress anything.

Release Notes:

- windows: Fixed corrupted characters being inserted when Alt is pressed
(preview only)
2025-11-05 21:03:36 +00:00
Matt Miller
2f463370cc Refactor buffer headers to collapse on click (#42021)
Release Notes:
Updated how clicking on multi-buffer headers works to provide better
control and prevent unexpected navigation:

Clicking the header now collapses/expands the file section instead of
opening the file.
Opening files can be done by clicking the filename or the "Open file"
button on the right side of the header.
Existing shortcuts continue to work: use the left chevron to collapse or
your keyboard shortcut to jump to the file

**Demo:**

https://github.com/user-attachments/assets/dca9ccc5-bd98-416c-97af-43b4e4b2f903
2025-11-05 14:59:50 -06:00
Danilo Leal
feed34cafe gpui: Add support for rendering SVG from external files (#42024)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-05 16:12:30 -03:00
Joseph T. Lyons
4724aa5cb8 Bump Zed to v0.213 (#42018)
Release Notes:

- N/A
2025-11-05 17:31:18 +00:00
Cameron Mcloughlin
366a5db2c0 collab_ui: Show parents when searching channels (#42005)
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-11-05 16:51:10 +00:00
Danilo Leal
81e87c4cd6 settings ui: Fix divider in items that doesn't have subfields (#42016)
Release Notes:

- N/A

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2025-11-05 16:49:19 +00:00
Andrew Farkas
b8ba663c20 Revert "Refactor completions" (#42014)
Reverts zed-industries/zed#41939
2025-11-05 16:48:28 +00:00
Anthony Eid
27fb1098fa Revert "editor: Add action to move between snippet tabstop positions" (#42008)
Reverts zed-industries/zed#41466

This PR would add "in_snippet" context when there wasn't a completion
menu visible, causing some actions to not be hit.

Release Note:

- N/A
2025-11-05 11:16:39 -05:00
37 changed files with 820 additions and 538 deletions

9
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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": {

View File

@@ -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

View File

@@ -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()

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
use acp_thread::AcpThread;
use gpui::Entity;
pub struct ThreadEditor {
thread: Entity<AcpThread>,
}

View File

@@ -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(())
})
}

View File

@@ -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)
})

View File

@@ -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);
}
});
});

View File

@@ -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())),
),
)

View File

@@ -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");

View File

@@ -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.

View File

@@ -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 },
}

View File

@@ -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> {

View File

@@ -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");
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();

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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>,
}

View File

@@ -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)
}
}
}

View File

@@ -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 {

View File

@@ -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) };

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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(&params.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(&params.path)? {
render_pixmap(&bytes)
} else {
Ok(None)
}
}
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {

View File

@@ -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(&params.clone().into(), &mut || {
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(&params)? else {
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(&params, data)?
else {
return Ok(None);
};
Ok(Some((size, Cow::Owned(bytes))))

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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()))
});

View File

@@ -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()

View File

@@ -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))

View File

@@ -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"

View File

@@ -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
);
}
}
}

View File

@@ -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>"]

View File

@@ -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

View File

@@ -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 thirdparty 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 doesnt
#### 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.