Compare commits

...

26 Commits

Author SHA1 Message Date
Cole Miller
a808ecb503 zed 0.209.2 2025-10-20 12:21:33 -04:00
Danilo Leal
b69d0ab1be ai onboarding: Add dismiss button to the sign in banner (#40660)
Release Notes:

- N/A
2025-10-20 12:09:28 -04:00
Cole Miller
eb33d3009c Set the minimum log level to info for the remote server (#40543)
`env_logger` defaults to only showing error-level logs, but we show
info-level logs and above for the main Zed process, so I think it makes
sense for the remote server to behave the same way.

Release Notes:

- N/A
2025-10-20 10:36:46 -04:00
Conrad Irwin
caa5d624ea Disallow rename/copy/delete on unshared files (#40540)
Release Notes:

- Disallow rename/delete/copy on unshared files

Co-Authored-By: Cole <cole@zed.dev>
2025-10-17 16:34:43 -06:00
Cole Miller
de7e0b47ba windows: Unpin Gemini CLI (#40288)
Updates #40212

v0.9.0 is now stable and contains the fix for the line endings bug, so
we can return to installing from the stable channel as usual. This also
bumps the minimum version on Windows to v0.9.0 so that anyone on v0.8.x
or v0.9.0-preview.4 will be upgraded automatically.

Release Notes:

- N/A
2025-10-16 21:00:16 -04:00
Zed Bot
bebf4b0497 Bump to 0.209.1 for @probably-neb 2025-10-16 19:29:09 +00:00
Ben Kunkle
fe00b6cb53 Revert deprecate code actions on format (#40409)
Closes #40334

This reverts the change made in #39983, and includes a replacement
migration that will transform formatter settings values consisting of
only `code_action` format steps into the previously deprecated
`code_actions_on_format` in an attempt to restore the behavior to what
it was before the migration that deprecated `code_actions_on_format`.

This PR will result in a modified order in the `code_actions_on_format`
setting if it existed, however the decision was made to explicitly
ignore this for now, as this PR is primarily targeting users who have
already had the deprecation migration run, and no longer have the
`code_actions_on_format` key

Release Notes:

- Fixed an issue with a settings migration that deprecated the
`code_actions_on_format` setting. The `code_actions_on_format` setting
has been un-deprecated, and affected users will have the bad migration
rolled back with an updated migration

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
2025-10-16 14:30:30 -04:00
Lukas Wirth
70d197876e editor: Fix SelectionsCollection::disjoint not being ordered correctly (#40249)
We've been seeing the occasional `cannot seek backwards` panic within
`SelectionsCollection` without means to reproduce.

I believe the cause is one of the callers of
`MutableSelectionsCollection::select` not passing a well formed
`Selection` where `start > end`, so this PR enforces the invariant in
`select` by swapping the fields and setting `reversed` as required as
the other mutator functions already do that as well.

We could also just assert this instead, but it callers usually won't
care about this so its the less user facing annoyance to just fix this
invariant up internally.

Fixes ZED-253
Fixes ZED-ZJ
Fixes ZED-23S
Fixes ZED-222
Fixes ZED-1ZV
Fixes ZED-1SN
Fixes ZED-1Z0
Fixes ZED-10E
Fixes ZED-1X0
Fixes ZED-12M
Fixes ZED-1GR
Fixes ZED-1VE
Fixes ZED-13X
Fixes ZED-1G4

Release Notes:

- Fixed occasional panics when querying selections
2025-10-16 18:49:40 +02:00
Lukas Wirth
10c540bf20 windows: Fix panic when quitting dialogs that do not have a cancel button (#40348)
`TaskDialogIndirect` may return `IDCANCEL` when the user quits the
dialog via escape or alt+f4, so we need to account for that.

Fixes ZED-25H

Release Notes:

- Fixed panic when hitting escape in dialogs on windows
2025-10-16 18:49:10 +02:00
Lukas Wirth
bb7de4ee04 windows: Fix occasional RefCell already mutably borrowed panic (#40336)
Release Notes:

- Fixed occasional `RefCell already mutably borrowed` panic in windows
event handling
2025-10-16 18:48:57 +02:00
Lukas Wirth
87b9b9f452 languages: Fix go completion labels creating out of bounds highlight runs (#40355)
Fixes ZED-26Q

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-16 18:48:22 +02:00
Lukas Wirth
e17609c938 editor: Fix invalid excerpt panic in Editor::hover_links (#40387)
Fixes ZED-17N
Fixes ZED-26Z

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-16 18:48:05 +02:00
Julia Ryan
353e936a7a Fix mkdir nushell flags (#40306)
Closes #40269

Release Notes:

- N/A
2025-10-16 18:20:49 +02:00
Ben Brandt
1d89ddb776 Revert "acp: Don't collapse tool calls by default" (#40395)
Reverts zed-industries/zed#40164

Release Notes:

- N/A
2025-10-16 18:20:49 +02:00
Lukas Wirth
c8903a0010 markdown_preview: Fix alt text causing mismatched highlighting runs (#40374)
Fixes ZED-277

Release Notes:

- Fixed alt text in markdown preview creating inconsistent highlighting
2025-10-16 18:20:49 +02:00
Julia Ryan
ac002d0a7f Add winget release job (#40293)
This will automatically open PRs against the winget package registry to
bump our version there when we do a release.

Release Notes:

- N/A
2025-10-16 08:31:33 -07:00
Ben Brandt
66112d81b0 acp: Add nicer WSL warning for Codex (#40354)
Moves the Codex warning into the thread so that we can render it nicer,
as well as provide an option to open the folder in WSL.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-10-16 12:38:35 +02:00
Ben Kunkle
c3eaa757e8 settings_ui: Scale window size based on UI font size (#40257)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 21:16:02 -04:00
localcc
2793dd77ad Fix duplicate WSL entries (#40255)
Release Notes:

- N/A
2025-10-15 16:33:53 +02:00
localcc
10720d64a3 Improve musl libc detection (#40254)
Release Notes:

- N/A
2025-10-15 16:33:28 +02:00
Ben Brandt
2adb979ed7 acp: Fix /logout for agents that support it (#40248)
We were clearing the message editor too early. We only want to clear the
message editor if we are going to short circuit and return early before
submitting.
Otherwise, the agents that can handle this themselves won't have the
ability to do so.

Release Notes:

- acp: Fix /logout not working for some agents
2025-10-15 08:25:55 -06:00
Ben Brandt
bec6cd94a4 acp: Allow updating default mode for Codex (#40238)
Release Notes:

- acp: Save default mode for codex
2025-10-15 08:25:55 -06:00
Conrad Irwin
f6c0fa43ef Avoid gap between titlebar and body on linux
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-14 22:41:34 -06:00
Cole Miller
62da0dc402 Fix triggers for debugger thread and session lists not rendering (#40227)
Release Notes:

- N/A
2025-10-14 22:37:49 -06:00
Cole Miller
154405ff3b Fix a couple of bugs in remote browser debugging implementation (#40225)
Follow-up to #39248 

- Correctly forward ports over SSH, including the port from the debug
scenario's `url`
- Give the companion time to start up, instead of bailing if the first
connection attempt fails

Release Notes:

- Fixed not being able to launch a browser debugging session in an SSH
project.
2025-10-14 23:23:27 -04:00
Mikayla Maki
b558313181 v0.209.x preview 2025-10-14 17:33:03 -07:00
42 changed files with 842 additions and 714 deletions

View File

@@ -38,6 +38,26 @@ jobs:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest

2
Cargo.lock generated
View File

@@ -21203,7 +21203,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.209.0"
version = "0.209.2"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -1527,6 +1527,7 @@
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
},
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {},
@@ -1696,7 +1697,9 @@
"preferred_line_length": 72
},
"Go": {
"formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
"code_actions_on_format": {
"source.organizeImports": true
},
"debuggers": ["Delve"]
},
"GraphQL": {

View File

@@ -1,11 +1,16 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CODEX_NAME;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
use settings::{SettingsStore, update_settings_file};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
#[derive(Clone)]
pub struct Codex;
@@ -30,6 +35,27 @@ impl AgentServer for Codex {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -278,7 +278,7 @@ pub struct AcpThreadView {
thread_feedback: ThreadFeedbackState,
list_state: ListState,
auth_task: Option<Task<()>>,
collapsed_tool_calls: HashSet<acp::ToolCallId>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
plan_expanded: bool,
@@ -292,6 +292,8 @@ pub struct AcpThreadView {
resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5],
#[cfg(target_os = "windows")]
show_codex_windows_warning: bool,
}
enum ThreadState {
@@ -394,6 +396,10 @@ impl AcpThreadView {
),
];
#[cfg(target_os = "windows")]
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
== Some(crate::ExternalAgent::Codex);
Self {
agent: agent.clone(),
workspace: workspace.clone(),
@@ -419,7 +425,7 @@ impl AcpThreadView {
thread_error: None,
thread_feedback: Default::default(),
auth_task: None,
collapsed_tool_calls: HashSet::default(),
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
editing_message: None,
edits_expanded: false,
@@ -436,6 +442,8 @@ impl AcpThreadView {
focus_handle: cx.focus_handle(),
new_server_version_available: None,
resume_thread_metadata: resume_thread,
#[cfg(target_os = "windows")]
show_codex_windows_warning,
}
}
@@ -954,17 +962,17 @@ impl AcpThreadView {
) {
match &event.view_event {
ViewEvent::NewDiff(tool_call_id) => {
if !AgentSettings::get_global(cx).expand_edit_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
if AgentSettings::get_global(cx).expand_edit_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::NewTerminal(tool_call_id) => {
if !AgentSettings::get_global(cx).expand_terminal_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
if AgentSettings::get_global(cx).expand_terminal_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
self.collapsed_tool_calls.insert(tool_call_id.clone());
self.expanded_tool_calls.remove(tool_call_id);
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
if let Some(thread) = self.thread()
@@ -1045,9 +1053,6 @@ impl AcpThreadView {
return;
};
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let connection = thread.read(cx).connection().clone();
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
@@ -1058,6 +1063,9 @@ impl AcpThreadView {
.iter()
.any(|command| command.name == "logout");
if can_login && !logout_supported {
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
@@ -2119,7 +2127,7 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || !self.collapsed_tool_calls.contains(&tool_call.id);
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let tool_output_display =
if is_open {
@@ -2269,9 +2277,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.collapsed_tool_calls.insert(id.clone());
this.expanded_tool_calls.remove(&id);
} else {
this.collapsed_tool_calls.remove(&id);
this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
@@ -2473,7 +2481,7 @@ impl AcpThreadView {
.icon_color(Color::Muted)
.on_click(cx.listener({
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
this.collapsed_tool_calls.insert(tool_call_id.clone());
this.expanded_tool_calls.remove(&tool_call_id);
cx.notify();
}
})),
@@ -2751,7 +2759,7 @@ impl AcpThreadView {
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
let is_expanded = !self.collapsed_tool_calls.contains(&tool_call.id);
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex()
.id(header_id)
@@ -2886,9 +2894,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
if is_expanded {
this.collapsed_tool_calls.insert(id.clone());
this.expanded_tool_calls.remove(&id);
} else {
this.collapsed_tool_calls.remove(&id);
this.expanded_tool_calls.insert(id.clone());
}
}
})),
@@ -5025,6 +5033,49 @@ impl AcpThreadView {
)
}
#[cfg(target_os = "windows")]
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.show_codex_windows_warning {
Some(
Callout::new()
.icon(IconName::Warning)
.severity(Severity::Warning)
.title("Codex on Windows")
.description(
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
)
.actions_slot(
Button::new("open-wsl-modal", "Open in WSL")
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
cx,
);
cx.notify();
}
})),
)
.dismiss_action(
IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss Warning"))
.on_click(cx.listener({
move |this, _, _, cx| {
this.show_codex_windows_warning = false;
cx.notify();
}
})),
),
)
} else {
None
}
}
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let content = match self.thread_error.as_ref()? {
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
@@ -5512,6 +5563,16 @@ impl Render for AcpThreadView {
_ => this,
})
.children(self.render_thread_retry_status_callout(window, cx))
.children({
#[cfg(target_os = "windows")]
{
self.render_codex_windows_warning(cx)
}
#[cfg(not(target_os = "windows"))]
{
Vec::<Empty>::new()
}
})
.children(self.render_thread_error(window, cx))
.when_some(
self.new_server_version_available.as_ref().filter(|_| {

View File

@@ -222,12 +222,11 @@ enum WhichFontSize {
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
Zed,
NativeAgent,
TextThread,
Gemini,
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
@@ -237,8 +236,7 @@ pub enum AgentType {
impl AgentType {
fn label(&self) -> SharedString {
match self {
Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2".into(),
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code".into(),
Self::Codex => "Codex".into(),
@@ -248,7 +246,7 @@ impl AgentType {
fn icon(&self) -> Option<IconName> {
match self {
Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude),
Self::Codex => Some(IconName::AiOpenAi),
@@ -813,7 +811,7 @@ impl AgentPanel {
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
#[derive(Default, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
struct LastUsedExternalAgent {
agent: crate::ExternalAgent,
}
@@ -854,18 +852,18 @@ impl AgentPanel {
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.unwrap_or_default()
.agent
.map(|agent| agent.agent)
.unwrap_or(ExternalAgent::NativeAgent)
}
}
};
if !loading {
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
}
let server = ext_agent.server(fs, history);
if !loading {
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
}
this.update_in(cx, |this, window, cx| {
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
@@ -1345,15 +1343,6 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
match agent {
AgentType::Zed => {
window.dispatch_action(
NewThread {
from_thread_id: None,
}
.boxed_clone(),
cx,
);
}
AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}

View File

@@ -161,10 +161,9 @@ pub struct NewNativeAgentThreadFromSummary {
}
// TODO unify this with AgentType
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ExternalAgent {
#[default]
pub enum ExternalAgent {
Gemini,
ClaudeCode,
Codex,
@@ -184,13 +183,13 @@ fn placeholder_command() -> AgentServerCommand {
}
impl ExternalAgent {
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Codex => "codex",
Self::Custom { .. } => "custom",
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
match server.telemetry_id() {
"gemini-cli" => Some(Self::Gemini),
"claude-code" => Some(Self::ClaudeCode),
"codex" => Some(Self::Codex),
"zed" => Some(Self::NativeAgent),
_ => None,
}
}

View File

@@ -84,10 +84,32 @@ impl ZedAiOnboarding {
self
}
fn render_dismiss_button(&self) -> Option<AnyElement> {
self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
let callback = dismiss_callback.clone();
h_flex()
.absolute()
.top_0()
.right_0()
.child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
callback(window, cx)
}),
)
.into_any_element()
})
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
@@ -109,6 +131,7 @@ impl ZedAiOnboarding {
}
}),
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -180,27 +203,7 @@ impl ZedAiOnboarding {
)
.child(PlanDefinitions.free_plan(is_v2)),
)
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.children(self.render_dismiss_button())
.child(
v_flex()
.mt_2()
@@ -245,26 +248,7 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_trial(is_v2, false))
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -278,26 +262,7 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_plan(is_v2, false))
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.children(self.render_dismiss_button())
.into_any_element()
}
}

View File

@@ -1,7 +1,7 @@
use std::rc::Rc;
use collections::HashMap;
use gpui::{Entity, WeakEntity};
use gpui::{Corner, Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
@@ -211,6 +211,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
@@ -322,6 +323,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.disabled(session_terminated)
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),

View File

@@ -3899,6 +3899,9 @@ impl Editor {
}
})
.collect::<Vec<_>>();
if selection_ranges.is_empty() {
return;
}
let ranges = match columnar_state {
ColumnarSelectionState::FromMouse { .. } => {

View File

@@ -493,22 +493,15 @@ pub fn show_link_definition(
}
let trigger_anchor = trigger_point.anchor();
let Some((buffer, buffer_position)) = editor
.buffer
.read(cx)
.text_anchor_for_position(*trigger_anchor, cx)
else {
let anchor = snapshot.buffer_snapshot().anchor_before(*trigger_anchor);
let Some(buffer) = editor.buffer().read(cx).buffer_for_anchor(anchor, cx) else {
return;
};
let Some((excerpt_id, _, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(*trigger_anchor, cx)
else {
return;
};
let Anchor {
excerpt_id,
text_anchor,
..
} = anchor;
let same_kind = hovered_link_state.preferred_kind == preferred_kind
|| hovered_link_state
.links
@@ -538,7 +531,7 @@ pub fn show_link_definition(
async move {
let result = match &trigger_point {
TriggerPoint::Text(_) => {
if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
if let Some((url_range, url)) = find_url(&buffer, text_anchor, cx.clone()) {
this.read_with(cx, |_, _| {
let range = maybe!({
let start =
@@ -550,7 +543,7 @@ pub fn show_link_definition(
})
.ok()
} else if let Some((filename_range, filename)) =
find_file(&buffer, project.clone(), buffer_position, cx).await
find_file(&buffer, project.clone(), text_anchor, cx).await
{
let range = maybe!({
let start =
@@ -562,7 +555,7 @@ pub fn show_link_definition(
Some((range, vec![HoverLink::File(filename)]))
} else if let Some(provider) = provider {
let task = cx.update(|_, cx| {
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
provider.definitions(&buffer, text_anchor, preferred_kind, cx)
})?;
if let Some(task) = task {
task.await.ok().flatten().map(|definition_result| {

View File

@@ -610,21 +610,32 @@ impl<'a> MutableSelectionsCollection<'a> {
self.select(selections);
}
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
T: ToOffset + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let mut selections = selections
.into_iter()
.map(|selection| selection.map(|it| it.to_offset(&buffer)))
.map(|mut selection| {
if selection.start > selection.end {
mem::swap(&mut selection.start, &mut selection.end);
selection.reversed = true
}
selection
})
.collect::<Vec<_>>();
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i - 1].end >= selections[i].start {
if selections[i].start <= selections[i - 1].end {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if removed.end > selections[i - 1].end {
if selections[i - 1].end < removed.end {
selections[i - 1].end = removed.end;
}
} else {
@@ -968,13 +979,10 @@ impl DerefMut for MutableSelectionsCollection<'_> {
}
}
fn selection_to_anchor_selection<T>(
selection: Selection<T>,
fn selection_to_anchor_selection(
selection: Selection<usize>,
buffer: &MultiBufferSnapshot,
) -> Selection<Anchor>
where
T: ToOffset + Ord,
{
) -> Selection<Anchor> {
let end_bias = if selection.start == selection.end {
Bias::Right
} else {
@@ -1012,7 +1020,7 @@ fn resolve_selections_point<'a>(
})
}
// Panics if passed selections are not in order
/// Panics if passed selections are not in order
fn resolve_selections_display<'a>(
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
map: &'a DisplaySnapshot,
@@ -1044,7 +1052,7 @@ fn resolve_selections_display<'a>(
coalesce_selections(selections)
}
// Panics if passed selections are not in order
/// Panics if passed selections are not in order
pub(crate) fn resolve_selections<'a, D, I>(
selections: I,
map: &'a DisplaySnapshot,

View File

@@ -530,8 +530,18 @@ impl WindowsWindowInner {
};
let scale_factor = lock.scale_factor;
let wheel_scroll_amount = match modifiers.shift {
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
true => {
self.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_chars
}
false => {
self.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_lines
}
};
drop(lock);
@@ -574,7 +584,11 @@ impl WindowsWindowInner {
return Some(1);
};
let scale_factor = lock.scale_factor;
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
let wheel_scroll_chars = self
.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_chars;
drop(lock);
let wheel_distance =
@@ -707,11 +721,8 @@ impl WindowsWindowInner {
// used by Chrome. However, it may result in one row of pixels being obscured
// in our client area. But as Chrome says, "there seems to be no better solution."
if is_maximized
&& let Some(ref taskbar_position) = self
.state
.borrow()
.system_settings
.auto_hide_taskbar_position
&& let Some(ref taskbar_position) =
self.system_settings.borrow().auto_hide_taskbar_position
{
// For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
// so the window isn't treated as a "fullscreen app", which would cause
@@ -1101,9 +1112,11 @@ impl WindowsWindowInner {
if wparam.0 != 0 {
let mut lock = self.state.borrow_mut();
let display = lock.display;
lock.system_settings.update(display, wparam.0);
lock.click_state.system_update(wparam.0);
lock.border_offset.update(handle).log_err();
// system settings may emit a window message which wants to take the refcell lock, so drop it
drop(lock);
self.system_settings.borrow_mut().update(display, wparam.0);
} else {
self.handle_system_theme_changed(handle, lparam)?;
};

View File

@@ -51,7 +51,6 @@ pub struct WindowsWindowState {
pub renderer: DirectXRenderer,
pub click_state: ClickState,
pub system_settings: WindowsSystemSettings,
pub current_cursor: Option<HCURSOR>,
pub nc_button_pressed: Option<u32>,
@@ -66,6 +65,7 @@ pub(crate) struct WindowsWindowInner {
pub(super) this: Weak<Self>,
drop_target_helper: IDropTargetHelper,
pub(crate) state: RefCell<WindowsWindowState>,
pub(crate) system_settings: RefCell<WindowsSystemSettings>,
pub(crate) handle: AnyWindowHandle,
pub(crate) hide_title_bar: bool,
pub(crate) is_movable: bool,
@@ -115,7 +115,6 @@ impl WindowsWindowState {
let system_key_handled = false;
let hovered = false;
let click_state = ClickState::new();
let system_settings = WindowsSystemSettings::new(display);
let nc_button_pressed = None;
let fullscreen = None;
let initial_placement = None;
@@ -138,7 +137,6 @@ impl WindowsWindowState {
hovered,
renderer,
click_state,
system_settings,
current_cursor,
nc_button_pressed,
display,
@@ -231,6 +229,7 @@ impl WindowsWindowInner {
validation_number: context.validation_number,
main_receiver: context.main_receiver.clone(),
platform_window_handle: context.platform_window_handle,
system_settings: RefCell::new(WindowsSystemSettings::new(context.display)),
}))
}
@@ -644,10 +643,12 @@ impl PlatformWindow for WindowsWindow {
let mut btn_encoded = Vec::new();
for (index, btn) in answers.iter().enumerate() {
let encoded = HSTRING::from(btn.label().as_ref());
let button_id = if btn.is_cancel() {
IDCANCEL.0
} else {
index as i32 - 100
let button_id = match btn {
PromptButton::Ok(_) => IDOK.0,
PromptButton::Cancel(_) => IDCANCEL.0,
// the first few low integer values are reserved for known buttons
// so for simplicity we just go backwards from -1
PromptButton::Other(_) => -(index as i32) - 1,
};
button_id_map.push(button_id);
buttons.push(TASKDIALOG_BUTTON {
@@ -665,11 +666,11 @@ impl PlatformWindow for WindowsWindow {
.context("unable to create task dialog")
.log_err();
let clicked = button_id_map
.iter()
.position(|&button_id| button_id == res)
.unwrap();
let _ = done_tx.send(clicked);
if let Some(clicked) =
button_id_map.iter().position(|&button_id| button_id == res)
{
let _ = done_tx.send(clicked);
}
}
})
.detach();

View File

@@ -142,6 +142,8 @@ pub struct LanguageSettings {
pub auto_indent_on_paste: bool,
/// Controls how the editor handles the autoclosed characters.
pub always_treat_brackets_as_autoclosed: bool,
/// Which code actions to run on save
pub code_actions_on_format: HashMap<String, bool>,
/// Whether to perform linked edits
pub linked_edits: bool,
/// Task configuration for this language.
@@ -576,6 +578,7 @@ impl settings::Settings for AllLanguageSettings {
always_treat_brackets_as_autoclosed: settings
.always_treat_brackets_as_autoclosed
.unwrap(),
code_actions_on_format: settings.code_actions_on_format.unwrap(),
linked_edits: settings.linked_edits.unwrap(),
tasks: LanguageTaskSettings {
variables: tasks.variables.unwrap_or_default(),

View File

@@ -222,7 +222,7 @@ impl LspAdapter for GoLspAdapter {
Some((lsp::CompletionItemKind::MODULE, detail)) => {
let text = format!("{label} {detail}");
let source = Rope::from(format!("import {text}").as_str());
let runs = language.highlight_text(&source, 7..7 + text.len());
let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
let filter_range = completion
.filter_text
.as_deref()
@@ -246,7 +246,7 @@ impl LspAdapter for GoLspAdapter {
Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 4..4 + text.len()),
language.highlight_text(&source, 4..4 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -267,7 +267,7 @@ impl LspAdapter for GoLspAdapter {
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text.len()),
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -288,7 +288,7 @@ impl LspAdapter for GoLspAdapter {
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text.len()),
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -310,7 +310,7 @@ impl LspAdapter for GoLspAdapter {
Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 16..16 + text.len()),
language.highlight_text(&source, 16..16 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -332,7 +332,7 @@ impl LspAdapter for GoLspAdapter {
let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text.len()),
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text

View File

@@ -57,29 +57,65 @@ impl RustLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
#[cfg(target_os = "linux")]
enum LibcType {
Gnu,
Musl,
}
impl RustLspAdapter {
#[cfg(target_os = "linux")]
fn build_arch_server_name_linux() -> String {
enum LibcType {
Gnu,
Musl,
async fn determine_libc_type() -> LibcType {
use futures::pin_mut;
use smol::process::Command;
async fn from_ldd_version() -> Option<LibcType> {
let ldd_output = Command::new("ldd").arg("--version").output().await.ok()?;
let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
Some(LibcType::Gnu)
} else if ldd_version.contains("musl") {
Some(LibcType::Musl)
} else {
None
}
}
let has_musl = std::fs::exists(&format!("/lib/ld-musl-{}.so.1", std::env::consts::ARCH))
.unwrap_or(false);
let has_gnu = std::fs::exists(&format!("/lib/ld-linux-{}.so.1", std::env::consts::ARCH))
.unwrap_or(false);
if let Some(libc_type) = from_ldd_version().await {
return libc_type;
}
let libc_type = match (has_musl, has_gnu) {
let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {
// defaulting to gnu because nix doesn't have /lib files due to not following FHS
return LibcType::Gnu;
};
let dir_entries = dir_entries.filter_map(async move |e| e.ok());
pin_mut!(dir_entries);
let mut has_musl = false;
let mut has_gnu = false;
while let Some(entry) = dir_entries.next().await {
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name.starts_with("ld-musl-") {
has_musl = true;
} else if file_name.starts_with("ld-linux-") {
has_gnu = true;
}
}
match (has_musl, has_gnu) {
(true, _) => LibcType::Musl,
(_, true) => LibcType::Gnu,
_ => {
// defaulting to gnu because nix doesn't have either of those files due to not following FHS
LibcType::Gnu
}
};
_ => LibcType::Gnu,
}
}
let libc = match libc_type {
#[cfg(target_os = "linux")]
async fn build_arch_server_name_linux() -> String {
let libc = match Self::determine_libc_type().await {
LibcType::Musl => "musl",
LibcType::Gnu => "gnu",
};
@@ -87,7 +123,7 @@ impl RustLspAdapter {
format!("{}-{}", Self::ARCH_SERVER_NAME, libc)
}
fn build_asset_name() -> String {
async fn build_asset_name() -> String {
let extension = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => "tar.gz",
AssetKind::Gz => "gz",
@@ -95,7 +131,7 @@ impl RustLspAdapter {
};
#[cfg(target_os = "linux")]
let arch_server_name = Self::build_arch_server_name_linux();
let arch_server_name = Self::build_arch_server_name_linux().await;
#[cfg(not(target_os = "linux"))]
let arch_server_name = Self::ARCH_SERVER_NAME.to_string();
@@ -447,7 +483,7 @@ impl LspInstaller for RustLspAdapter {
delegate.http_client(),
)
.await?;
let asset_name = Self::build_asset_name();
let asset_name = Self::build_asset_name().await;
let asset = release
.assets
.into_iter()

View File

@@ -9,7 +9,9 @@ use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
use language::LanguageRegistry;
use markup5ever_rcdom::RcDom;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
use std::{cell::RefCell, collections::HashMap, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec};
use std::{
cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
};
pub async fn parse_markdown(
markdown_input: &str,
@@ -407,6 +409,9 @@ impl<'a> MarkdownParser<'a> {
if let Some(mut image) = image.take() {
if !text.is_empty() {
image.set_alt_text(std::mem::take(&mut text).into());
mem::take(&mut highlights);
mem::take(&mut region_ranges);
mem::take(&mut regions);
}
markdown_text_like.push(MarkdownParagraphChunk::Image(image));
}
@@ -1275,17 +1280,40 @@ mod tests {
panic!("Expected a paragraph");
};
assert_eq!(
paragraph[0],
MarkdownParagraphChunk::Image(Image {
source_range: 0..111,
link: Link::Web {
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
},
alt_text: Some("test".into()),
height: None,
width: None,
},)
);
paragraph[0],
MarkdownParagraphChunk::Image(Image {
source_range: 0..111,
link: Link::Web {
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
},
alt_text: Some("test".into()),
height: None,
width: None,
},)
);
}
#[gpui::test]
async fn test_image_alt_text() {
let parsed = parse("[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)\n ").await;
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
text
} else {
panic!("Expected a paragraph");
};
assert_eq!(
paragraph[0],
MarkdownParagraphChunk::Image(Image {
source_range: 0..142,
link: Link::Web {
url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
},
alt_text: Some("Zed".into()),
height: None,
width: None,
},)
);
}
#[gpui::test]

View File

@@ -118,8 +118,8 @@ pub(crate) mod m_2025_10_03 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_10_10 {
pub(crate) mod m_2025_10_16 {
mod settings;
pub(crate) use settings::remove_code_actions_on_format;
pub(crate) use settings::restore_code_actions_on_format;
}

View File

@@ -1,70 +0,0 @@
use anyhow::Result;
use serde_json::Value;
pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> {
remove_code_actions_on_format_inner(value, &[])?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
remove_code_actions_on_format_inner(language, &path)?;
}
}
Ok(())
}
fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let Some(code_actions_on_format) = obj.get("code_actions_on_format").cloned() else {
return Ok(());
};
fn fmt_path(path: &[&str], key: &str) -> String {
let mut path = path.to_vec();
path.push(key);
path.join(".")
}
anyhow::ensure!(
code_actions_on_format.is_object(),
r#"The `code_actions_on_format` setting is deprecated, but it is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
fmt_path(path, "code_actions_on_format"),
);
let code_actions_map = code_actions_on_format.as_object().unwrap();
let mut code_actions = vec![];
for (code_action, code_action_enabled) in code_actions_map {
if code_action_enabled.as_bool().map_or(false, |b| !b) {
continue;
}
code_actions.push(code_action.clone());
}
let mut formatter_array = vec![];
if let Some(formatter) = obj.get("formatter") {
if let Some(array) = formatter.as_array() {
formatter_array = array.clone();
} else {
formatter_array.insert(0, formatter.clone());
}
};
let found_code_actions = !code_actions.is_empty();
formatter_array.splice(
0..0,
code_actions
.into_iter()
.map(|code_action| serde_json::json!({"code_action": code_action})),
);
obj.remove("code_actions_on_format");
if found_code_actions {
obj.insert("formatter".to_string(), Value::Array(formatter_array));
}
Ok(())
}

View File

@@ -0,0 +1,68 @@
use anyhow::Result;
use serde_json::Value;
use crate::patterns::migrate_language_setting;
pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> {
migrate_language_setting(value, restore_code_actions_on_format_inner)
}
fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let code_actions_on_format = obj
.get("code_actions_on_format")
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
fn fmt_path(path: &[&str], key: &str) -> String {
let mut path = path.to_vec();
path.push(key);
path.join(".")
}
let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else {
anyhow::bail!(
r#"The `code_actions_on_format` is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
fmt_path(path, "code_actions_on_format"),
);
};
let Some(formatter) = obj.get("formatter") else {
return Ok(());
};
let formatter_array = if let Some(array) = formatter.as_array() {
array.clone()
} else {
vec![formatter.clone()]
};
let mut code_action_formatters = Vec::new();
for formatter in formatter_array {
let Some(code_action) = formatter.get("code_action") else {
return Ok(());
};
let Some(code_action_name) = code_action.as_str() else {
anyhow::bail!(
r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#,
fmt_path(path, "formatter"),
);
};
code_action_formatters.push(code_action_name.to_string());
}
code_actions_map.extend(
code_action_formatters
.into_iter()
.rev()
.map(|code_action| (code_action, Value::Bool(true))),
);
obj.remove("formatter");
obj.insert(
"code_actions_on_format".into(),
Value::Object(code_actions_map),
);
Ok(())
}

View File

@@ -213,7 +213,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_10_03::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_03,
),
MigrationType::Json(migrations::m_2025_10_10::remove_code_actions_on_format),
MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
];
run_migrations(text, migrations)
}
@@ -367,6 +367,7 @@ mod tests {
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
#[track_caller]
fn assert_migrate_settings(input: &str, output: Option<&str>) {
let migrated = migrate_settings(input).unwrap();
assert_migrated_correctly(migrated, output);
@@ -1341,7 +1342,11 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_basic_array() {
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
&r#"{
"formatter": [
{
@@ -1368,7 +1373,11 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_basic_object() {
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
&r#"{
"formatter": {
"code_actions": {
@@ -1500,7 +1509,11 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
{
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
&r#"{
"formatter": {
"code_actions": {
@@ -1916,297 +1929,91 @@ mod tests {
}
#[test]
fn test_code_actions_on_format_migration_basic() {
fn test_restore_code_actions_on_format() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": true
"formatter": {
"code_action": "foo"
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.organizeImports"
},
{
"code_action": "source.fixAll"
}
]
"code_actions_on_format": {
"foo": true
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_filters_false_values() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"a": true,
"b": false,
"c": true
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "a"
},
{
"code_action": "c"
}
]
}
"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_with_existing_formatter_object() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": "prettier",
"code_actions_on_format": {
"source.organizeImports": true
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.organizeImports"
},
"prettier"
]
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_with_existing_formatter_array() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": ["prettier", {"language_server": "eslint"}],
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": true
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.organizeImports"
},
{
"code_action": "source.fixAll"
},
"prettier",
{
"language_server": "eslint"
}
]
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_in_languages() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"languages": {
"JavaScript": {
"code_actions_on_format": {
"source.fixAll.eslint": true
}
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
}
}
}
}"#
.unindent(),
Some(
&r#"{
"languages": {
"JavaScript": {
"formatter": [
{
"code_action": "source.fixAll.eslint"
}
]
},
"Go": {
"formatter": [
{
"code_action": "source.organizeImports"
}
]
}
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_in_languages_with_existing_formatter() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"languages": {
"JavaScript": {
"formatter": "prettier",
"code_actions_on_format": {
"source.fixAll.eslint": true,
"source.organizeImports": false
}
}
}
}"#
.unindent(),
Some(
&r#"{
"languages": {
"JavaScript": {
"formatter": [
{
"code_action": "source.fixAll.eslint"
},
"prettier"
]
}
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_mixed_global_and_languages() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": "prettier",
"code_actions_on_format": {
"source.fixAll": true
},
"languages": {
"Rust": {
"formatter": "rust-analyzer",
"code_actions_on_format": {
"source.organizeImports": true
}
},
"Python": {
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": false
}
}
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.fixAll"
},
"prettier"
],
"languages": {
"Rust": {
"formatter": [
{
"code_action": "source.organizeImports"
},
"rust-analyzer"
]
},
"Python": {
"formatter": [
{
"code_action": "source.organizeImports"
}
]
}
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_no_migration_when_not_present() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": ["prettier"]
"formatter": [
{ "code_action": "foo" },
"auto"
]
}"#
.unindent(),
None,
);
}
#[test]
fn test_code_actions_on_format_migration_all_false_values() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"a": false,
"b": false
"formatter": {
"code_action": "foo"
},
"formatter": "prettier"
"code_actions_on_format": {
"bar": true,
"baz": false
}
}"#
.unindent(),
Some(
&r#"{
"formatter": "prettier"
"code_actions_on_format": {
"foo": true,
"bar": true,
"baz": false
}
}"#
.unindent(),
),
);
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"formatter": [
{ "code_action": "foo" },
{ "code_action": "qux" },
],
"code_actions_on_format": {
"bar": true,
"baz": false
}
}"#
.unindent(),
Some(
&r#"{
"code_actions_on_format": {
"foo": true,
"qux": true,
"bar": true,
"baz": false
}
}"#
.unindent(),
),

View File

@@ -10,4 +10,5 @@ pub(crate) use settings::{
SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
migrate_language_setting,
};

View File

@@ -108,3 +108,24 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
(#eq? @agent1 "agent")
(#eq? @agent2 "agent")
)"#;
/// Migrate language settings,
/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
/// Fails early if `migrate_fn` returns an error at any point
pub fn migrate_language_setting(
value: &mut serde_json::Value,
migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
migrate_fn(value, &[])?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
migrate_fn(language, &path)?;
}
}
Ok(())
}

View File

@@ -577,7 +577,6 @@ fn get_or_npm_install_builtin_agent(
package_name: SharedString,
entrypoint_path: PathBuf,
minimum_version: Option<semver::Version>,
channel: &'static str,
status_tx: Option<watch::Sender<SharedString>>,
new_version_available: Option<watch::Sender<Option<String>>>,
fs: Arc<dyn Fs>,
@@ -649,12 +648,10 @@ fn get_or_npm_install_builtin_agent(
let dir = dir.clone();
let fs = fs.clone();
async move {
// TODO remove the filter
let latest_version = node_runtime
.npm_package_latest_version(&package_name)
.await
.ok()
.filter(|_| channel == "latest");
.ok();
if let Some(latest_version) = latest_version
&& &latest_version != &file_name.to_string_lossy()
{
@@ -663,7 +660,6 @@ fn get_or_npm_install_builtin_agent(
dir.clone(),
node_runtime,
package_name.clone(),
channel,
)
.await
.log_err();
@@ -687,7 +683,6 @@ fn get_or_npm_install_builtin_agent(
dir.clone(),
node_runtime,
package_name.clone(),
channel,
))
.await?
.into()
@@ -736,14 +731,13 @@ async fn download_latest_version(
dir: PathBuf,
node_runtime: NodeRuntime,
package_name: SharedString,
channel: &'static str,
) -> Result<String> {
log::debug!("downloading latest version of {package_name}");
let tmp_dir = tempfile::tempdir_in(&dir)?;
node_runtime
.npm_install_packages(tmp_dir.path(), &[(&package_name, channel)])
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
.await?;
let version = node_runtime
@@ -886,17 +880,12 @@ impl ExternalAgentServer for LocalGemini {
GEMINI_NAME.into(),
"@google/gemini-cli".into(),
"node_modules/@google/gemini-cli/dist/index.js".into(),
// TODO remove these windows-specific workarounds once v0.9.0 stable is released
if cfg!(windows) {
Some("0.9.0-preview.4".parse().unwrap())
// v0.8.x on Windows has a bug that causes the initialize request to hang forever
Some("0.9.0".parse().unwrap())
} else {
Some("0.2.1".parse().unwrap())
},
if cfg!(windows) {
"0.9.0-preview.4"
} else {
"latest"
},
status_tx,
new_version_available_tx,
fs,
@@ -980,7 +969,6 @@ impl ExternalAgentServer for LocalClaudeCode {
"@zed-industries/claude-code-acp".into(),
"node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
Some("0.5.2".parse().unwrap()),
"latest",
status_tx,
new_version_available_tx,
fs,

View File

@@ -14,12 +14,13 @@ use super::dap_command::{
TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
};
use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use base64::Engine;
use collections::{HashMap, HashSet, IndexMap};
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
use dap::messages::Response;
use dap::requests::{Request, RunInTerminal, StartDebugging};
use dap::transport::TcpTransport;
use dap::{
Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
SteppingGranularity, StoppedEvent, VariableReference,
@@ -47,12 +48,14 @@ use remote::RemoteClient;
use rpc::ErrorExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::net::TcpListener;
use smol::net::{TcpListener, TcpStream};
use std::any::TypeId;
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use std::u64;
use std::{
any::Any,
@@ -63,6 +66,7 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
use url::Url;
use util::command::new_smol_command;
use util::{ResultExt, debug_panic, maybe};
use worktree::Worktree;
@@ -2768,31 +2772,42 @@ impl Session {
let mut console_output = self.console_output(cx);
let task = cx.spawn(async move |this, cx| {
let (dap_port, _child) =
if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
(request.server_port, None)
} else {
let port = {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.context("getting port for DAP")?;
listener.local_addr()?.port()
};
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_port_command(
port,
"localhost".into(),
request.server_port,
)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
(port, Some(child))
};
let forward_ports_process = if remote_client
.read_with(cx, |client, _| client.shares_network_interface())?
{
request.other.insert(
"proxyUri".into(),
format!("127.0.0.1:{}", request.server_port).into(),
);
None
} else {
let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST)
.await
.context("getting port for DAP")?;
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{port}").into());
let mut port_forwards = vec![(port, "localhost".to_owned(), request.server_port)];
if let Some(value) = request.params.get("url")
&& let Some(url) = value.as_str()
&& let Some(url) = Url::parse(url).ok()
&& let Some(frontend_port) = url.port()
{
port_forwards.push((frontend_port, "localhost".to_owned(), frontend_port));
}
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_ports_command(port_forwards)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
Some(child)
};
let mut companion_process = None;
let companion_port =
@@ -2814,14 +2829,17 @@ impl Session {
}
}
};
this.update(cx, |this, cx| {
this.companion_port = Some(companion_port);
let Some(mut child) = companion_process else {
return;
};
if let Some(stderr) = child.stderr.take() {
let mut background_tasks = Vec::new();
if let Some(mut forward_ports_process) = forward_ports_process {
background_tasks.push(cx.spawn(async move |_| {
forward_ports_process.status().await.log_err();
}));
};
if let Some(mut companion_process) = companion_process {
if let Some(stderr) = companion_process.stderr.take() {
let mut console_output = console_output.clone();
this.background_tasks.push(cx.spawn(async move |_, _| {
background_tasks.push(cx.spawn(async move |_| {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
@@ -2835,9 +2853,9 @@ impl Session {
}
}));
}
this.background_tasks.push(cx.spawn({
background_tasks.push(cx.spawn({
let mut console_output = console_output.clone();
async move |_, _| match child.status().await {
async move |_| match companion_process.status().await {
Ok(status) => {
if status.success() {
console_output
@@ -2860,17 +2878,33 @@ impl Session {
.ok();
}
}
}))
})?;
}));
}
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
// TODO pass wslInfo as needed
let companion_address = format!("127.0.0.1:{companion_port}");
let mut companion_started = false;
for _ in 0..10 {
if TcpStream::connect(&companion_address).await.is_ok() {
companion_started = true;
break;
}
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
}
if !companion_started {
console_output
.send("Browser companion failed to start".into())
.await
.ok();
bail!("Browser companion failed to start");
}
let response = http_client
.post_json(
&format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
&format!("http://{companion_address}/launch-and-attach"),
serde_json::to_string(&request)
.context("serializing request")?
.into(),
@@ -2895,6 +2929,11 @@ impl Session {
}
}
this.update(cx, |this, _| {
this.background_tasks.extend(background_tasks);
this.companion_port = Some(companion_port);
})?;
anyhow::Ok(())
});
self.background_tasks.push(cx.spawn(async move |_, _| {
@@ -2926,15 +2965,16 @@ impl Session {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct LaunchBrowserInCompanionParams {
server_port: u16,
params: HashMap<String, serde_json::Value>,
#[serde(flatten)]
other: HashMap<String, serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct KillCompanionBrowserParams {
launch_id: u64,

View File

@@ -1333,6 +1333,32 @@ impl LocalLspStore {
})?;
}
// Formatter for `code_actions_on_format` that runs before
// the rest of the formatters
let mut code_actions_on_format_formatters = None;
let should_run_code_actions_on_format = !matches!(
(trigger, &settings.format_on_save),
(FormatTrigger::Save, &FormatOnSave::Off)
);
if should_run_code_actions_on_format {
let have_code_actions_to_run_on_format = settings
.code_actions_on_format
.values()
.any(|enabled| *enabled);
if have_code_actions_to_run_on_format {
zlog::trace!(logger => "going to run code actions on format");
code_actions_on_format_formatters = Some(
settings
.code_actions_on_format
.iter()
.filter_map(|(action, enabled)| enabled.then_some(action))
.cloned()
.map(Formatter::CodeAction)
.collect::<Vec<_>>(),
);
}
}
let formatters = match (trigger, &settings.format_on_save) {
(FormatTrigger::Save, FormatOnSave::Off) => &[],
(FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
@@ -1340,6 +1366,11 @@ impl LocalLspStore {
}
};
let formatters = code_actions_on_format_formatters
.iter()
.flatten()
.chain(formatters);
for formatter in formatters {
let formatter = if formatter == &Formatter::Auto {
if settings.prettier.allowed {

View File

@@ -5,7 +5,7 @@ use std::{
sync::{Arc, atomic::AtomicUsize},
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use collections::{HashMap, HashSet};
use fs::{Fs, copy_recursive};
use futures::{
@@ -1194,6 +1194,16 @@ impl WorktreeStore {
RelPath::from_proto(&envelope.payload.new_path)?,
);
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
let Some((_, project_id)) = this.downstream_client else {
bail!("no downstream client")
};
let Some(entry) = this.entry_for_id(entry_id, cx) else {
bail!("no such entry");
};
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
bail!("entry is private")
}
let new_worktree = this
.worktree_for_id(new_worktree_id, cx)
.context("no such worktree")?;
@@ -1217,6 +1227,15 @@ impl WorktreeStore {
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let worktree = this.update(&mut cx, |this, cx| {
let Some((_, project_id)) = this.downstream_client else {
bail!("no downstream client")
};
let Some(entry) = this.entry_for_id(entry_id, cx) else {
bail!("no entry")
};
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
bail!("entry is private")
}
this.worktree_for_entry(entry_id, cx)
.context("worktree not found")
})??;
@@ -1237,6 +1256,18 @@ impl WorktreeStore {
let worktree = this
.worktree_for_entry(entry_id, cx)
.context("no such worktree")?;
let Some((_, project_id)) = this.downstream_client else {
bail!("no downstream client")
};
let entry = worktree
.read(cx)
.entry_for_id(entry_id)
.ok_or_else(|| anyhow!("missing entry"))?;
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
bail!("entry is private")
}
let scan_id = worktree.read(cx).scan_id();
anyhow::Ok((
scan_id,

View File

@@ -64,7 +64,7 @@ use workspace::{
DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
SplitDirection, Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyTaskExt},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
};
use worktree::CreatedEntry;
use zed_actions::workspace::OpenWithSystem;
@@ -2677,12 +2677,14 @@ impl ProjectPanel {
for task in paste_tasks {
match task {
PasteTask::Rename(task) => {
if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
if let Some(CreatedEntry::Included(entry)) =
task.await.notify_async_err(cx)
{
last_succeed = Some(entry);
}
}
PasteTask::Copy(task) => {
if let Some(Some(entry)) = task.await.log_err() {
if let Some(Some(entry)) = task.await.notify_async_err(cx) {
last_succeed = Some(entry);
}
}

View File

@@ -1385,14 +1385,21 @@ impl RemoteServerProjects {
cx: &mut Context<Self>,
) {
self.update_settings_file(cx, move |setting, _| {
setting
.wsl_connections
.get_or_insert(Default::default())
.push(settings::WslConnection {
distro_name: SharedString::from(connection_options.distro_name),
user: connection_options.user,
let connections = setting.wsl_connections.get_or_insert(Default::default());
let distro_name = SharedString::from(connection_options.distro_name);
let user = connection_options.user;
if !connections
.iter()
.any(|conn| conn.distro_name == distro_name && conn.user == user)
{
connections.push(settings::WslConnection {
distro_name,
user,
projects: BTreeSet::new(),
})
}
});
}

View File

@@ -836,16 +836,14 @@ impl RemoteClient {
connection.build_command(program, args, env, working_dir, port_forward)
}
pub fn build_forward_port_command(
pub fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Some(connection) = self.remote_connection() else {
return Err(anyhow!("no ssh connection"));
};
connection.build_forward_port_command(local_port, host, remote_port)
connection.build_forward_ports_command(forwards)
}
pub fn upload_directory(
@@ -1116,11 +1114,9 @@ pub(crate) trait RemoteConnection: Send + Sync {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
remote: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn connection_options(&self) -> RemoteConnectionOptions;
fn path_style(&self) -> PathStyle;
@@ -1551,19 +1547,17 @@ mod fake {
})
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
args: std::iter::once("-N".to_owned())
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
format!("{local_port}:{host}:{remote_port}")
}))
.collect(),
env: Default::default(),
})
}

View File

@@ -146,19 +146,20 @@ impl RemoteConnection for SshRemoteConnection {
)
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Self { socket, .. } = self;
let mut args = socket.ssh_args();
args.push("-N".into());
for (local_port, host, remote_port) in forwards {
args.push("-L".into());
args.push(format!("{local_port}:{host}:{remote_port}"));
}
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
args,
env: Default::default(),
})
}

View File

@@ -82,7 +82,7 @@ impl WslRemoteConnection {
this.can_exec = this.detect_can_exec(shell).await?;
this.platform = this.detect_platform(shell).await?;
this.remote_binary_path = Some(
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
this.ensure_server_binary(&delegate, release_channel, version, commit, shell, cx)
.await?,
);
log::debug!("Detected WSL environment: {this:#?}");
@@ -163,6 +163,7 @@ impl WslRemoteConnection {
release_channel: ReleaseChannel,
version: SemanticVersion,
commit: Option<AppCommitSha>,
shell: ShellKind,
cx: &mut AsyncApp,
) -> Result<Arc<RelPath>> {
let version_str = match release_channel {
@@ -184,9 +185,13 @@ impl WslRemoteConnection {
paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
if let Some(parent) = dst_path.parent() {
self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
.await
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
let parent = parent.display(PathStyle::Posix);
if shell == ShellKind::Nushell {
self.run_wsl_command("mkdir", &[&parent]).await
} else {
self.run_wsl_command("mkdir", &["-p", &parent]).await
}
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
}
#[cfg(debug_assertions)]
@@ -485,11 +490,9 @@ impl RemoteConnection for WslRemoteConnection {
})
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
_: u16,
_: String,
_: u16,
_: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Err(anyhow!("WSL shares a network interface with the host"))
}

View File

@@ -103,7 +103,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> {
buffer: Vec::new(),
});
env_logger::Builder::from_default_env()
env_logger::Builder::new()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.target(env_logger::Target::Pipe(target))
.format(|buf, record| {
let mut log_record = LogRecord::new(record);

View File

@@ -318,6 +318,11 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub use_on_type_format: Option<bool>,
/// Which code actions to run on save after the formatter.
/// These are not run if formatting is off.
///
/// Default: {} (or {"source.organizeImports": true} for Go).
pub code_actions_on_format: Option<HashMap<String, bool>>,
/// Whether to perform linked edits of associated ranges, if the language server supports it.
/// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
///

View File

@@ -254,9 +254,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("Fonts"),
SettingsPageItem::SectionHeader("Buffer Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Family",
title: "Font Family",
description: "Font family for editor text",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.buffer_font_family,
@@ -266,7 +266,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Size",
title: "Font Size",
description: "Font size for editor text",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.buffer_font_size,
@@ -276,7 +276,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Weight",
title: "Font Weight",
description: "Font weight for editor text (100-900)",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.buffer_font_weight,
@@ -288,7 +288,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
// todo(settings_ui): This needs custom ui
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Buffer Line Height",
title: "Line Height",
description: "Line height for editor text",
field: Box::new(
SettingField {
@@ -303,7 +303,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Buffer Font Features",
title: "Font Features",
description: "The OpenType features to enable for rendering in text buffers.",
field: Box::new(
SettingField {
@@ -318,7 +318,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Buffer Font Fallbacks",
title: "Font Fallbacks",
description: "The font fallbacks to use for rendering in text buffers.",
field: Box::new(
SettingField {
@@ -331,8 +331,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("UI Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Family",
title: "Font Family",
description: "Font family for UI elements",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_family,
@@ -342,7 +343,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Size",
title: "Font Size",
description: "Font size for UI elements",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_size,
@@ -352,7 +353,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Weight",
title: "Font Weight",
description: "Font weight for UI elements (100-900)",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_weight,
@@ -363,7 +364,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "UI Font Features",
title: "Font Features",
description: "The OpenType features to enable for rendering in UI elements.",
field: Box::new(
SettingField {
@@ -378,7 +379,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "UI Font Fallbacks",
title: "Font Fallbacks",
description: "The font fallbacks to use for rendering in the UI.",
field: Box::new(
SettingField {
@@ -391,8 +392,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("Agent Panel Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "Agent Panel UI Font Size",
title: "UI Font Size",
description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -408,7 +410,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Agent Panel Buffer Font Size",
title: "Buffer Font Size",
description: "Font size for user messages text in the agent panel",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.agent_buffer_font_size,
@@ -419,6 +421,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Cursor"),
SettingsPageItem::SettingItem(SettingItem {
title: "Multi Cursor Modifier",
description: "Modifier key for adding multiple cursors",
@@ -431,7 +434,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Cursor"),
SettingsPageItem::SettingItem(SettingItem {
title: "Cursor Blink",
description: "Whether the cursor blinks in the editor",
@@ -808,9 +810,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Hover"),
SettingsPageItem::SectionHeader("Hover Popover"),
SettingsPageItem::SettingItem(SettingItem {
title: "Hover Popover Enabled",
title: "Enabled",
description: "Show the informational hover box when moving the mouse over symbols in the editor",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.hover_popover_enabled,
@@ -823,7 +825,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
// todo(settings ui): add units to this number input
SettingsPageItem::SettingItem(SettingItem {
title: "Hover Popover Delay",
title: "Delay",
description: "Time to wait in milliseconds before showing the informational hover box",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.hover_popover_delay,
@@ -834,21 +836,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Code Actions & Selection"),
SettingsPageItem::SectionHeader("Drag And Drop Selection"),
SettingsPageItem::SettingItem(SettingItem {
title: "Inline Code Actions",
description: "Show code action button at start of buffer line",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.inline_code_actions,
pick_mut: |settings_content| {
&mut settings_content.editor.inline_code_actions
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Drag And Drop Selection",
title: "Enabled",
description: "Enable drag and drop selection",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -872,7 +862,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Drag And Drop Selection Delay",
title: "Delay",
description: "Delay in milliseconds before drag and drop selection starts",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -1014,6 +1004,18 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Inline Code Actions",
description: "Show code action button at start of buffer line",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.inline_code_actions,
pick_mut: |settings_content| {
&mut settings_content.editor.inline_code_actions
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Scrollbar"),
SettingsPageItem::SettingItem(SettingItem {
title: "Show",
@@ -5414,6 +5416,27 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
metadata: None,
files: USER | LOCAL,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Code Actions On Format",
description: "Additional Code Actions To Run When Formatting",
field: Box::new(
SettingField {
pick: |settings_content| {
language_settings_field(settings_content, |language| {
&language.code_actions_on_format
})
},
pick_mut: |settings_content| {
language_settings_field_mut(settings_content, |language| {
&mut language.code_actions_on_format
})
},
}
.unimplemented(),
),
metadata: None,
files: USER | LOCAL,
}),
SettingsPageItem::SectionHeader("Autoclose"),
SettingsPageItem::SettingItem(SettingItem {
title: "Use Autoclose",

View File

@@ -15,7 +15,7 @@ use heck::ToTitleCase as _;
use project::WorktreeId;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{SettingsContent, SettingsStore};
use settings::{Settings, SettingsContent, SettingsStore};
use std::{
any::{Any, TypeId, type_name},
cell::RefCell,
@@ -466,6 +466,13 @@ pub fn open_settings_editor(
// We have to defer this to get the workspace off the stack.
cx.defer(move |cx| {
let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into();
let default_bounds = size(px(900.), px(750.)); // 4:3 Aspect Ratio
let default_rem_size = 16.0;
let scale_factor = current_rem_size / default_rem_size;
let scaled_bounds: gpui::Size<Pixels> = default_bounds.map(|axis| axis * scale_factor);
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
@@ -478,8 +485,8 @@ pub fn open_settings_editor(
is_movable: true,
kind: gpui::WindowKind::Floating,
window_background: cx.theme().window_background_appearance(),
window_min_size: Some(size(px(900.), px(750.))), // 4:3 Aspect Ratio
window_bounds: Some(WindowBounds::centered(size(px(900.), px(750.)), cx)),
window_min_size: Some(scaled_bounds),
window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
..Default::default()
},
|window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)),
@@ -1695,7 +1702,7 @@ impl SettingsWindow {
};
v_flex()
.w_64()
.w_56()
.p_2p5()
.when(cfg!(target_os = "macos"), |c| c.pt_10())
.h_full()
@@ -2130,7 +2137,7 @@ impl SettingsWindow {
}
return v_flex()
.size_full()
.flex_1()
.pt_6()
.pb_8()
.px_8()

View File

@@ -97,6 +97,7 @@ impl Render for PlatformTitleBar {
})
// this border is to avoid a transparent gap in the rounded corners
.mt(px(-1.))
.mb(px(-1.))
.border(px(1.))
.border_color(titlebar_color),
})

View File

@@ -1,6 +1,6 @@
use gpui::{Corner, Entity, Pixels, Point};
use crate::{ContextMenu, PopoverMenu, prelude::*};
use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*};
use super::PopoverMenuHandle;
@@ -137,36 +137,52 @@ impl RenderOnce for DropdownMenu {
let full_width = self.full_width;
let trigger_size = self.trigger_size;
let button = match self.label {
LabelKind::Text(text) => Button::new(self.id.clone(), text)
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled),
LabelKind::Element(_element) => Button::new(self.id.clone(), "")
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled),
}
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index));
let (text_button, element_button) = match self.label {
LabelKind::Text(text) => (
Some(
Button::new(self.id.clone(), text)
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled)
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
),
None,
),
LabelKind::Element(element) => (
None,
Some(
ButtonLike::new(self.id.clone())
.child(element)
.style(button_style)
.when(self.chevron, |this| {
this.child(
Icon::new(IconName::ChevronUpDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled)
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
),
),
};
PopoverMenu::new((self.id.clone(), "popover"))
.full_width(self.full_width)
.menu(move |_window, _cx| Some(self.menu.clone()))
.trigger(button)
.when_some(text_button, |this, text_button| this.trigger(text_button))
.when_some(element_button, |this, element_button| {
this.trigger(element_button)
})
.attach(match self.attach {
Some(attach) => attach,
None => Corner::BottomRight,

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.209.0"
version = "0.209.2"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -49,8 +49,8 @@ You can configure Zed to format code using `eslint --fix` by running the ESLint
{
"languages": {
"JavaScript": {
"formatter": {
"code_action": "source.fixAll.eslint"
"code_actions_on_format": {
"source.fixAll.eslint": true
}
}
}
@@ -63,8 +63,8 @@ You can also only execute a single ESLint rule when using `fixAll`:
{
"languages": {
"JavaScript": {
"formatter": {
"code_action": "source.fixAll.eslint"
"code_actions_on_format": {
"source.fixAll.eslint": true
}
}
},
@@ -92,8 +92,8 @@ the formatter:
{
"languages": {
"JavaScript": {
"formatter": {
"code_action": "source.fixAll.eslint"
"code_actions_on_format": {
"source.fixAll.eslint": true
}
}
}