Compare commits
26 Commits
v0.218.2-p
...
v0.209.2-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a808ecb503 | ||
|
|
b69d0ab1be | ||
|
|
eb33d3009c | ||
|
|
caa5d624ea | ||
|
|
de7e0b47ba | ||
|
|
bebf4b0497 | ||
|
|
fe00b6cb53 | ||
|
|
70d197876e | ||
|
|
10c540bf20 | ||
|
|
bb7de4ee04 | ||
|
|
87b9b9f452 | ||
|
|
e17609c938 | ||
|
|
353e936a7a | ||
|
|
1d89ddb776 | ||
|
|
c8903a0010 | ||
|
|
ac002d0a7f | ||
|
|
66112d81b0 | ||
|
|
c3eaa757e8 | ||
|
|
2793dd77ad | ||
|
|
10720d64a3 | ||
|
|
2adb979ed7 | ||
|
|
bec6cd94a4 | ||
|
|
f6c0fa43ef | ||
|
|
62da0dc402 | ||
|
|
154405ff3b | ||
|
|
b558313181 |
20
.github/workflows/community_release_actions.yml
vendored
20
.github/workflows/community_release_actions.yml
vendored
@@ -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
2
Cargo.lock
generated
@@ -21203,7 +21203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.209.0"
|
||||
version = "0.209.2"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(|_| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -3899,6 +3899,9 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if selection_ranges.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = match columnar_state {
|
||||
ColumnarSelectionState::FromMouse { .. } => {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)?;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("[](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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
68
crates/migrator/src/migrations/m_2025_10_16/settings.rs
Normal file
68
crates/migrator/src/migrations/m_2025_10_16/settings.rs
Normal 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(())
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user