Compare commits

...

17 Commits

Author SHA1 Message Date
Joseph T. Lyons
63cab3ccc8 v0.203.x stable 2025-09-10 10:08:22 -04:00
Anthony Eid
8aa6a94610 onboarding: Add telemetry to Basics page (#37502)
- Welcome Keymap Changed
- Welcome Theme Changed
- Welcome Theme mode Changed
- Welcome Page Telemetry Diagnostics Toggled
- Welcome Page Telemetry Metrics Toggled
- Welcome Vim Mode Toggled
- Welcome Keymap Changed
- Welcome Sign In Clicked

cc: @katie-z-geer

Release Notes:

- N/A
2025-09-10 00:07:49 -04:00
Conrad Irwin
63860fc5c6 Merge conflicts 2025-09-09 16:17:14 -06:00
Zed Bot
996f9cad68 Bump to 0.203.4 for @ConradIrwin 2025-09-09 22:11:42 +00:00
Agus Zubiaga
21aa3d5cd7 acp: Ensure connection subprocess gets killed on drop (#37858)
It appears that in macOS, the `AcpConnection._wait_task` doesn't always
get dropped when quitting the app. In these cases, the subprocess would
be kept alive because we move the `child` into it.

Instead, we will now explicitly kill it when `AcpConnection` is dropped.
It's ok to do this because when the connection is dropped, the thread is
also dropped, so there's no need to report the exit status to it.

Closes #37741

Release Notes:

- Claude Code: Fix subprocess leak on app quit
2025-09-09 16:08:28 -06:00
Conrad Irwin
c5d36e05f8 Allow unauthenticated commit models to show (#37857)
Closes #37462
Closes #37814

Release Notes:

- Fixed a bug where the commit generation message would not always show
2025-09-09 12:50:22 -06:00
Conrad Irwin
b63c715a53 Only reject agent actions, don't restore checkpoint on revert (#37801)
Updates #37623

Release Notes:

- Changed the behaviour when editing an old message in a native agent
thread.
Prior to this, it would automatically restore the checkpoint (which
could
lead to a surprising amount of work being discarded). Now it will just
reject
any unaccepted agent edits, and you can use the "restore checkpoint"
button
  for the original behavior.
2025-09-08 20:19:10 -06:00
Peter Tripp
d287b8bbf7 zed 0.203.3 2025-09-08 14:33:55 -04:00
Lukas Wirth
bbbfa10fe1 project: Consider all worktrees for activation script search (#37764)
Should fix https://github.com/zed-industries/zed/issues/37734

Release Notes:

- Fixed venv not always activating correctly
2025-09-08 19:38:57 +02:00
Cole Miller
0c1ad95e8c acp: Pass project environment to external agent servers (#37568)
Closes #37469 

Release Notes:

- agent: The project shell environment is now passed to external agent
processes.

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Nia Espera <nia-e@haecceity.cc>
2025-09-08 13:29:46 -04:00
张小白
29b7863403 macos: Fix menu bar flickering (#37707)
Closes #37526

Release Notes:

- Fixed menu bar flickering when using some IMEs on macOS.
2025-09-08 13:21:53 -04:00
Smit Barmase
66fbfd7080 linux: Fix IME preedit text not showing in Terminal on Wayland (#37701)
Closes https://github.com/zed-industries/zed/issues/37268
 
Release Notes:

- Fixed an issue where IME preedit text was not showing in the Terminal
on Wayland.
2025-09-08 13:14:49 -04:00
Ben Kunkle
92912915fa onboarding: Fix font loading frame delay (#37668)
Closes #ISSUE

Fixed an issue where the first frame of the `Editing` page in onboarding
would have a slight delay before rendering the first time it was
navigated to. This was caused by listing the OS fonts on the main
thread, blocking rendering. This PR fixes the issue by adding a new
method to the font family cache to prefill the cache on a background
thread.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
2025-09-08 11:58:41 -05:00
Ben Kunkle
018935de63 onboarding: Improve performance of AI upsell card (#37504)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-08 11:55:43 -05:00
Max Brunsfeld
fbec5e8dd5 Disable foreign keys in sqlite when running migrations (#37572)
Closes #37473

Previously, we enabled foreign keys at all times for our sqlite database
that we use for client-side state.
The problem with this is that In sqlite, `alter table` is somewhat
limited, so for many migrations, you must *recreate* the table: create a
new table called e.g. `workspace__2`, then copy all of the data from
`workspaces` into `workspace__2`, then delete the old `workspaces` table
and rename `workspaces__2` to `workspaces`. The way foreign keys work in
sqlite, when we delete the old table, all of its associated records in
other tables will be deleted due to `on delete cascade` clauses.

Unfortunately, one of the types of associated records that can be
deleted are `editors`, which sometimes store unsaved text. It is very
bad to delete these records, as they are the *only* place that this
unsaved text is stored.

This has already happened multiple times as we have migrated tables as
we develop Zed, but I caused it to happened again in
https://github.com/zed-industries/zed/pull/36714.

The Sqlite docs recommend a multi-step approach to migrations where you:

* disable foreign keys
* start a transaction
* create a new table
* populate the new table with data from the old table
* delete the old table
* rename the new table to the old name
* run a foreign key check
* if it passes, commit the transaction
* enable foreign keys

In this PR, I've adjusted our sqlite migration code path to follow this
pattern more closely. Specifically, we disable foreign key checks before
running migrations, run a foreign key check before committing, and then
enable foreign key checks after the migrations are done.

In addition, I've added a generic query that we run *before* running the
foreign key check that explicitly deletes any rows that have dangling
foreign keys. This way, we avoid failing the migration (and breaking the
app) if a migration deletes data that *does* cause associated records to
need to be deleted.

But now, in the common case where we migrate old data in the new table
and keep the ids, all of the associated data will be preserved.

Release Notes:

- Fixed a bug where workspace state would be lost when upgrading from
Zed 0.201.x. or below.
2025-09-08 09:45:02 -07:00
Kirill Bulatov
bda30bb0eb Fixed LSP binary info not being shown in full (#37682)
Follow-up of https://github.com/zed-industries/zed/pull/37083
Closes https://github.com/zed-industries/zed/issues/37677

Release Notes:

- Fixed LSP binary info not being shown in full
2025-09-06 10:50:04 +03:00
Peter Tripp
aa95dbb670 linux: Restore ctrl-escape to keymap (#37636)
Closes: https://github.com/zed-industries/zed/issues/37628
Follow-up to: https://github.com/zed-industries/zed/pull/36712

Release Notes:

- linux: Fix for ctrl-escape not escaping the tab switcher.
2025-09-05 11:10:08 -04:00
26 changed files with 444 additions and 199 deletions

3
Cargo.lock generated
View File

@@ -15313,6 +15313,7 @@ dependencies = [
"futures 0.3.31",
"indoc",
"libsqlite3-sys",
"log",
"parking_lot",
"smol",
"sqlformat",
@@ -20398,7 +20399,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.203.2"
version = "0.203.4"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -16,6 +16,7 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",

View File

@@ -1640,13 +1640,13 @@ impl AcpThread {
cx.foreground_executor().spawn(send_task)
}
/// Rewinds this thread to before the entry at `index`, removing it and all
/// subsequent entries while reverting any changes made from that point.
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
return Task::ready(Err(anyhow!("not supported")));
};
let Some(message) = self.user_message(&id) else {
/// Restores the git working tree to the state at the given checkpoint (if one exists)
pub fn restore_checkpoint(
&mut self,
id: UserMessageId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some((_, message)) = self.user_message_mut(&id) else {
return Task::ready(Err(anyhow!("message not found")));
};
@@ -1654,15 +1654,30 @@ impl AcpThread {
.checkpoint
.as_ref()
.map(|c| c.git_checkpoint.clone());
let rewind = self.rewind(id.clone(), cx);
let git_store = self.project.read(cx).git_store().clone();
cx.spawn(async move |this, cx| {
cx.spawn(async move |_, cx| {
rewind.await?;
if let Some(checkpoint) = checkpoint {
git_store
.update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))?
.await?;
}
Ok(())
})
}
/// Rewinds this thread to before the entry at `index`, removing it and all
/// subsequent entries while rejecting any action_log changes made from that point.
/// Unlike `restore_checkpoint`, this method does not restore from git.
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
return Task::ready(Err(anyhow!("not supported")));
};
cx.spawn(async move |this, cx| {
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) {
@@ -1670,7 +1685,11 @@ impl AcpThread {
this.entries.truncate(ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range));
}
})
this.action_log()
.update(cx, |action_log, cx| action_log.reject_all_edits(cx))
})?
.await;
Ok(())
})
}
@@ -1727,20 +1746,6 @@ impl AcpThread {
})
}
fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> {
self.entries.iter().find_map(|entry| {
if let AgentThreadEntry::UserMessage(message) = entry {
if message.id.as_ref() == Some(id) {
Some(message)
} else {
None
}
} else {
None
}
})
}
fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> {
self.entries.iter_mut().enumerate().find_map(|(ix, entry)| {
if let AgentThreadEntry::UserMessage(message) = entry {
@@ -2684,7 +2689,7 @@ mod tests {
let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else {
panic!("unexpected entries {:?}", thread.entries)
};
thread.rewind(message.id.clone().unwrap(), cx)
thread.restore_checkpoint(message.id.clone().unwrap(), cx)
})
.await
.unwrap();

View File

@@ -9,6 +9,7 @@ use futures::AsyncBufReadExt as _;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use util::ResultExt as _;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
@@ -29,6 +30,9 @@ pub struct AcpConnection {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
agent_capabilities: acp::AgentCapabilities,
// NB: Don't move this into the wait_task, since we need to ensure the process is
// killed on drop (setting kill_on_drop on the command seems to not always work).
child: smol::process::Child,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
@@ -65,7 +69,6 @@ impl AcpConnection {
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
@@ -102,8 +105,9 @@ impl AcpConnection {
let wait_task = cx.spawn({
let sessions = sessions.clone();
let status_fut = child.status();
async move |cx| {
let status = child.status().await?;
let status = status_fut.await?;
for session in sessions.borrow().values() {
session
@@ -152,6 +156,7 @@ impl AcpConnection {
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
child,
})
}
@@ -160,6 +165,13 @@ impl AcpConnection {
}
}
impl Drop for AcpConnection {
fn drop(&mut self) {
// See the comment on the child field.
self.child.kill().log_err();
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,

View File

@@ -80,8 +80,15 @@ impl AgentServer for ClaudeCode {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings {
settings.command
} else {
@@ -97,6 +104,8 @@ impl AgentServer for ClaudeCode {
})?
.await?
};
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
command
.env

View File

@@ -41,12 +41,19 @@ impl AgentServer for Gemini {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
@@ -67,13 +74,12 @@ impl AgentServer for Gemini {
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
project_env
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(

View File

@@ -913,7 +913,7 @@ impl AcpThreadView {
}
}
ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
self.regenerate(event.entry_index, editor, window, cx);
self.regenerate(event.entry_index, editor.clone(), window, cx);
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
self.cancel_editing(&Default::default(), window, cx);
@@ -1137,7 +1137,7 @@ impl AcpThreadView {
fn regenerate(
&mut self,
entry_ix: usize,
message_editor: &Entity<MessageEditor>,
message_editor: Entity<MessageEditor>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1154,16 +1154,18 @@ impl AcpThreadView {
return;
};
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
let task = cx.spawn(async move |_, cx| {
let contents = contents.await?;
cx.spawn_in(window, async move |this, cx| {
thread
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
.await?;
Ok(contents)
});
self.send_impl(task, window, cx);
let contents =
message_editor.update(cx, |message_editor, cx| message_editor.contents(cx))?;
this.update_in(cx, |this, window, cx| {
this.send_impl(contents, window, cx);
})?;
anyhow::Ok(())
})
.detach();
}
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
@@ -1626,14 +1628,16 @@ impl AcpThreadView {
cx.notify();
}
fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
let Some(thread) = self.thread() else {
return;
};
thread
.update(cx, |thread, cx| thread.rewind(message_id.clone(), cx))
.update(cx, |thread, cx| {
thread.restore_checkpoint(message_id.clone(), cx)
})
.detach_and_log_err(cx);
cx.notify();
}
fn render_entry(
@@ -1703,8 +1707,9 @@ impl AcpThreadView {
.label_size(LabelSize::XSmall)
.icon_color(Color::Muted)
.color(Color::Muted)
.tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
.on_click(cx.listener(move |this, _, _window, cx| {
this.rewind(&message_id, cx);
this.restore_checkpoint(&message_id, cx);
}))
)
.child(Divider::horizontal())
@@ -1775,7 +1780,7 @@ impl AcpThreadView {
let editor = editor.clone();
move |this, _, window, cx| {
this.regenerate(
entry_ix, &editor, window, cx,
entry_ix, editor.clone(), window, cx,
);
}
})).into_any_element()

View File

@@ -86,10 +86,16 @@ impl RenderOnce for AiUpsellCard {
)
.child(plan_definitions.free_plan());
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
);
let grid_bg = h_flex()
.absolute()
.inset_0()
.w_full()
.h(px(240.))
.bg(gpui::pattern_slash(
cx.theme().colors().border.opacity(0.1),
2.,
25.,
));
let gradient_bg = div()
.absolute()

View File

@@ -40,8 +40,7 @@ use gpui::{
use itertools::Itertools;
use language::{Buffer, File};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use multi_buffer::ExcerptInfo;
@@ -1860,13 +1859,17 @@ impl GitPanel {
/// Generates a commit message using an LLM.
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
if !self.can_commit() || DisableAiSettings::get_global(cx).disable_ai {
if !self.can_commit()
|| DisableAiSettings::get_global(cx).disable_ai
|| !agent_settings::AgentSettings::get_global(cx).enabled
{
return;
}
let model = match current_language_model(cx) {
Some(value) => value,
None => return,
let Some(ConfiguredModel { provider, model }) =
LanguageModelRegistry::read_global(cx).commit_message_model()
else {
return;
};
let Some(repo) = self.active_repository.as_ref() else {
@@ -1891,6 +1894,16 @@ impl GitPanel {
this.generate_commit_message_task.take();
});
if let Some(task) = cx.update(|cx| {
if !provider.is_authenticated(cx) {
Some(provider.authenticate(cx))
} else {
None
}
})? {
task.await.log_err();
};
let mut diff_text = match diff.await {
Ok(result) => match result {
Ok(text) => text,
@@ -3080,9 +3093,18 @@ impl GitPanel {
&self,
cx: &Context<Self>,
) -> Option<AnyElement> {
current_language_model(cx).is_some().then(|| {
if self.generate_commit_message_task.is_some() {
return h_flex()
if !agent_settings::AgentSettings::get_global(cx).enabled
|| DisableAiSettings::get_global(cx).disable_ai
|| LanguageModelRegistry::read_global(cx)
.commit_message_model()
.is_none()
{
return None;
}
if self.generate_commit_message_task.is_some() {
return Some(
h_flex()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
@@ -3095,11 +3117,13 @@ impl GitPanel {
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element();
}
.into_any_element(),
);
}
let can_commit = self.can_commit();
let editor_focus_handle = self.commit_editor.focus_handle(cx);
let can_commit = self.can_commit();
let editor_focus_handle = self.commit_editor.focus_handle(cx);
Some(
IconButton::new("generate-commit-message", IconName::AiEdit)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
@@ -3120,8 +3144,8 @@ impl GitPanel {
.on_click(cx.listener(move |this, _event, _window, cx| {
this.generate_commit_message(cx);
}))
.into_any_element()
})
.into_any_element(),
)
}
pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
@@ -4453,20 +4477,6 @@ impl GitPanel {
}
}
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
let is_enabled = agent_settings::AgentSettings::get_global(cx).enabled
&& !DisableAiSettings::get_global(cx).disable_ai;
is_enabled
.then(|| {
let ConfiguredModel { provider, model } =
LanguageModelRegistry::read_global(cx).commit_message_model()?;
provider.is_authenticated(cx).then(|| model)
})
.flatten()
}
impl Render for GitPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let project = self.project.read(cx);

View File

@@ -325,7 +325,7 @@ impl LspLogView {
let server_info = format!(
"* Server: {NAME} (id {ID})
* Binary: {BINARY:#?}
* Binary: {BINARY}
* Registered workspace folders:
{WORKSPACE_FOLDERS}
@@ -335,10 +335,10 @@ impl LspLogView {
* Configuration: {CONFIGURATION}",
NAME = info.name,
ID = info.id,
BINARY = info.binary.as_ref().map_or_else(
|| "Unknown".to_string(),
|bin| bin.path.as_path().to_string_lossy().to_string()
),
BINARY = info
.binary
.as_ref()
.map_or_else(|| "Unknown".to_string(), |binary| format!("{binary:#?}")),
WORKSPACE_FOLDERS = info.workspace_folders.join(", "),
CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
@@ -990,10 +990,16 @@ impl Render for LspLogToolbarItemView {
let server_id = server.server_id;
let rpc_trace_enabled = server.rpc_trace_enabled;
let log_view = log_view.clone();
let label = match server.selected_entry {
LogKind::Rpc => RPC_MESSAGES,
LogKind::Trace => SERVER_TRACE,
LogKind::Logs => SERVER_LOGS,
LogKind::ServerInfo => SERVER_INFO,
};
PopoverMenu::new("LspViewSelector")
.anchor(Corner::TopLeft)
.trigger(
Button::new("language_server_menu_header", server.selected_entry.label())
Button::new("language_server_menu_header", label)
.icon(IconName::ChevronDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),

View File

@@ -68,6 +68,12 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
MODE_NAMES[mode as usize].clone(),
move |_, _, cx| {
write_mode_change(mode, cx);
telemetry::event!(
"Welcome Theme mode Changed",
from = theme_mode,
to = mode
);
},
)
}),
@@ -105,7 +111,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
ThemeMode::Dark => Appearance::Dark,
ThemeMode::System => *system_appearance,
};
let current_theme_name = theme_selection.theme(appearance);
let current_theme_name = SharedString::new(theme_selection.theme(appearance));
let theme_names = match appearance {
Appearance::Light => LIGHT_THEMES,
@@ -149,8 +155,15 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
})
.on_click({
let theme_name = theme.name.clone();
let current_theme_name = current_theme_name.clone();
move |_, _, cx| {
write_theme_change(theme_name.clone(), theme_mode, cx);
telemetry::event!(
"Welcome Theme Changed",
from = current_theme_name,
to = theme_name
);
}
})
.map(|this| {
@@ -239,6 +252,17 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
cx,
move |setting, _| setting.metrics = Some(enabled),
);
// This telemetry event shouldn't fire when it's off. If it does we're be alerted
// and can fix it in a timely manner to respect a user's choice.
telemetry::event!("Welcome Page Telemetry Metrics Toggled",
options = if enabled {
"on"
} else {
"off"
}
);
}},
).tab_index({
*tab_index += 1;
@@ -267,6 +291,16 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
cx,
move |setting, _| setting.diagnostics = Some(enabled),
);
// This telemetry event shouldn't fire when it's off. If it does we're be alerted
// and can fix it in a timely manner to respect a user's choice.
telemetry::event!("Welcome Page Telemetry Diagnostics Toggled",
options = if enabled {
"on"
} else {
"off"
}
);
}
}
).tab_index({
@@ -327,6 +361,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
setting.base_keymap = Some(keymap_base);
});
telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
}
}
@@ -344,13 +380,21 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
{
let fs = <dyn Fs>::global(cx);
move |&selection, _, cx| {
update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
*setting = match selection {
ToggleState::Selected => Some(true),
ToggleState::Unselected => Some(false),
ToggleState::Indeterminate => None,
let vim_mode = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => {
return;
}
};
update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
*setting = Some(vim_mode);
});
telemetry::event!(
"Welcome Vim Mode Toggled",
options = if vim_mode { "on" } else { "off" },
);
}
},
)

View File

@@ -449,28 +449,28 @@ impl FontPickerDelegate {
) -> Self {
let font_family_cache = FontFamilyCache::global(cx);
let fonts: Vec<SharedString> = font_family_cache
.list_font_families(cx)
.into_iter()
.collect();
let fonts = font_family_cache
.try_list_font_families()
.unwrap_or_else(|| vec![current_font.clone()]);
let selected_index = fonts
.iter()
.position(|font| *font == current_font)
.unwrap_or(0);
let filtered_fonts = fonts
.iter()
.enumerate()
.map(|(index, font)| StringMatch {
candidate_id: index,
string: font.to_string(),
positions: Vec::new(),
score: 0.0,
})
.collect();
Self {
fonts: fonts.clone(),
filtered_fonts: fonts
.iter()
.enumerate()
.map(|(index, font)| StringMatch {
candidate_id: index,
string: font.to_string(),
positions: Vec::new(),
score: 0.0,
})
.collect(),
fonts,
filtered_fonts,
selected_index,
current_font,
on_font_changed: Arc::new(on_font_changed),

View File

@@ -242,12 +242,25 @@ struct Onboarding {
impl Onboarding {
fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
workspace: workspace.weak_handle(),
focus_handle: cx.focus_handle(),
selected_page: SelectedPage::Basics,
user_store: workspace.user_store().clone(),
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
let font_family_cache = theme::FontFamilyCache::global(cx);
cx.new(|cx| {
cx.spawn(async move |this, cx| {
font_family_cache.prefetch(cx).await;
this.update(cx, |_, cx| {
cx.notify();
})
})
.detach();
Self {
workspace: workspace.weak_handle(),
focus_handle: cx.focus_handle(),
selected_page: SelectedPage::Basics,
user_store: workspace.user_store().clone(),
_settings_subscription: cx
.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
}
})
}
@@ -476,6 +489,7 @@ impl Onboarding {
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
telemetry::event!("Welcome Sign In Clicked");
window.dispatch_action(SignIn.boxed_clone(), cx);
})
.into_any_element()

View File

@@ -16,11 +16,6 @@ const SEND_LINE: &str = "\n// Send:";
const RECEIVE_LINE: &str = "\n// Receive:";
const MAX_STORED_LOG_ENTRIES: usize = 2000;
const RPC_MESSAGES: &str = "RPC Messages";
const SERVER_LOGS: &str = "Server Logs";
const SERVER_TRACE: &str = "Server Trace";
const SERVER_INFO: &str = "Server Info";
pub fn init(on_headless_host: bool, cx: &mut App) -> Entity<LogStore> {
let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx));
cx.set_global(GlobalLogStore(log_store.clone()));
@@ -216,15 +211,6 @@ impl LogKind {
LanguageServerLogType::Rpc { .. } => Self::Rpc,
}
}
pub fn label(&self) -> &'static str {
match self {
LogKind::Rpc => RPC_MESSAGES,
LogKind::Trace => SERVER_TRACE,
LogKind::Logs => SERVER_LOGS,
LogKind::ServerInfo => SERVER_INFO,
}
}
}
impl LogStore {

View File

@@ -49,14 +49,6 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
let is_via_remote = self.remote_client.is_some();
let project_path_context = self
.active_entry()
.and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
.or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
.map(|worktree_id| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
});
let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
if is_via_remote {
@@ -124,23 +116,42 @@ impl Project {
},
};
let toolchain = project_path_context
let project_path_contexts = self
.active_entry()
.and_then(|entry_id| self.path_for_entry(entry_id, cx))
.into_iter()
.chain(
self.visible_worktrees(cx)
.map(|wt| wt.read(cx).id())
.map(|worktree_id| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
}),
);
let toolchains = project_path_contexts
.filter(|_| detect_venv)
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx))
.collect::<Vec<_>>();
let lang_registry = self.languages.clone();
let fs = self.fs.clone();
cx.spawn(async move |project, cx| {
let activation_script = maybe!(async {
let toolchain = toolchain?.await?;
Some(
lang_registry
for toolchain in toolchains {
let Some(toolchain) = toolchain.await else {
continue;
};
let language = lang_registry
.language_for_name(&toolchain.language_name.0)
.await
.ok()?
.toolchain_lister()?
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
.await,
)
.ok();
let lister = language?.toolchain_lister();
return Some(
lister?
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
.await,
);
}
None
})
.await
.unwrap_or_default();
@@ -268,14 +279,6 @@ impl Project {
cwd: Option<PathBuf>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
let project_path_context = self
.active_entry()
.and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
.or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
.map(|worktree_id| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
});
let path = cwd.map(|p| Arc::from(&*p));
let is_via_remote = self.remote_client.is_some();
@@ -303,9 +306,22 @@ impl Project {
let local_path = if is_via_remote { None } else { path.clone() };
let toolchain = project_path_context
let project_path_contexts = self
.active_entry()
.and_then(|entry_id| self.path_for_entry(entry_id, cx))
.into_iter()
.chain(
self.visible_worktrees(cx)
.map(|wt| wt.read(cx).id())
.map(|worktree_id| ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
}),
);
let toolchains = project_path_contexts
.filter(|_| detect_venv)
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx))
.collect::<Vec<_>>();
let remote_client = self.remote_client.clone();
let shell = match &remote_client {
Some(remote_client) => remote_client
@@ -327,17 +343,22 @@ impl Project {
let fs = self.fs.clone();
cx.spawn(async move |project, cx| {
let activation_script = maybe!(async {
let toolchain = toolchain?.await?;
let language = lang_registry
.language_for_name(&toolchain.language_name.0)
.await
.ok();
let lister = language?.toolchain_lister();
Some(
lister?
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
.await,
)
for toolchain in toolchains {
let Some(toolchain) = toolchain.await else {
continue;
};
let language = lang_registry
.language_for_name(&toolchain.language_name.0)
.await
.ok();
let lister = language?.toolchain_lister();
return Some(
lister?
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
.await,
);
}
None
})
.await
.unwrap_or_default();

View File

@@ -73,6 +73,7 @@ impl SettingsValue<serde_json::Value> {
let fs = <dyn Fs>::global(cx);
let rx = settings_store.update_settings_file_at_path(fs.clone(), path.as_slice(), value);
let path = path.clone();
cx.background_spawn(async move {
rx.await?

View File

@@ -14,6 +14,7 @@ collections.workspace = true
futures.workspace = true
indoc.workspace = true
libsqlite3-sys.workspace = true
log.workspace = true
parking_lot.workspace = true
smol.workspace = true
sqlformat.workspace = true

View File

@@ -59,6 +59,7 @@ impl Connection {
let mut store_completed_migration = self
.exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
let mut did_migrate = false;
for (index, migration) in migrations.iter().enumerate() {
let migration =
sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default());
@@ -70,9 +71,7 @@ impl Connection {
&sqlformat::QueryParams::None,
Default::default(),
);
if completed_migration == migration
|| migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE")
{
if completed_migration == migration {
// Migration already run. Continue
continue;
} else if should_allow_migration_change(index, &completed_migration, &migration)
@@ -91,12 +90,58 @@ impl Connection {
}
self.eager_exec(&migration)?;
did_migrate = true;
store_completed_migration((domain, index, migration))?;
}
if did_migrate {
self.delete_rows_with_orphaned_foreign_key_references()?;
self.exec("PRAGMA foreign_key_check;")?()?;
}
Ok(())
})
}
/// Delete any rows that were orphaned by a migration. This is needed
/// because we disable foreign key constraints during migrations, so
/// that it's possible to re-create a table with the same name, without
/// deleting all associated data.
fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> {
let foreign_key_info: Vec<(String, String, String, String)> = self.select(
r#"
SELECT DISTINCT
schema.name as child_table,
foreign_keys.[from] as child_key,
foreign_keys.[table] as parent_table,
foreign_keys.[to] as parent_key
FROM sqlite_schema schema
JOIN pragma_foreign_key_list(schema.name) foreign_keys
WHERE
schema.type = 'table' AND
schema.name NOT LIKE "sqlite_%"
"#,
)?()?;
if !foreign_key_info.is_empty() {
log::info!(
"Found {} foreign key relationships to check",
foreign_key_info.len()
);
}
for (child_table, child_key, parent_table, parent_key) in foreign_key_info {
self.exec(&format!(
"
DELETE FROM {child_table}
WHERE {child_key} IS NOT NULL and {child_key} NOT IN
(SELECT {parent_key} FROM {parent_table})
"
))?()?;
}
Ok(())
}
}
#[cfg(test)]

View File

@@ -95,6 +95,14 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
let mut migration_result =
anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run"));
let foreign_keys_enabled: bool =
connection.select_row::<i32>("PRAGMA foreign_keys")?()
.unwrap_or(None)
.map(|enabled| enabled != 0)
.unwrap_or(false);
connection.exec("PRAGMA foreign_keys = OFF;")?()?;
for _ in 0..MIGRATION_RETRIES {
migration_result = connection
.with_savepoint("thread_safe_multi_migration", || M::migrate(connection));
@@ -104,6 +112,9 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
}
}
if foreign_keys_enabled {
connection.exec("PRAGMA foreign_keys = ON;")?()?;
}
migration_result
})
.await?;

View File

@@ -2,6 +2,7 @@ use std::{cmp::Ordering, fmt::Debug};
use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary};
/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree).
#[derive(Clone, PartialEq, Eq)]
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
where

View File

@@ -1192,8 +1192,8 @@ impl Element for TerminalElement {
bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top);
let marked_text_cloned: Option<String> = {
let ime_state = self.terminal_view.read(cx);
ime_state.marked_text.clone()
let ime_state = &self.terminal_view.read(cx).ime_state;
ime_state.as_ref().map(|state| state.marked_text.clone())
};
let terminal_input_handler = TerminalInputHandler {
@@ -1421,11 +1421,9 @@ impl InputHandler for TerminalInputHandler {
_window: &mut Window,
cx: &mut App,
) {
if let Some(range) = new_marked_range {
self.terminal_view.update(cx, |view, view_cx| {
view.set_marked_text(new_text.to_string(), range, view_cx);
});
}
self.terminal_view.update(cx, |view, view_cx| {
view.set_marked_text(new_text.to_string(), new_marked_range, view_cx);
});
}
fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) {

View File

@@ -62,6 +62,11 @@ use std::{
time::Duration,
};
struct ImeState {
marked_text: String,
marked_range_utf16: Option<Range<usize>>,
}
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.);
@@ -138,8 +143,7 @@ pub struct TerminalView {
scroll_handle: TerminalScrollHandle,
show_scrollbar: bool,
hide_scrollbar_task: Option<Task<()>>,
marked_text: Option<String>,
marked_range_utf16: Option<Range<usize>>,
ime_state: Option<ImeState>,
_subscriptions: Vec<Subscription>,
_terminal_subscriptions: Vec<Subscription>,
}
@@ -263,8 +267,7 @@ impl TerminalView {
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
cwd_serialized: false,
marked_text: None,
marked_range_utf16: None,
ime_state: None,
_subscriptions: vec![
focus_in,
focus_out,
@@ -323,24 +326,27 @@ impl TerminalView {
pub(crate) fn set_marked_text(
&mut self,
text: String,
range: Range<usize>,
range: Option<Range<usize>>,
cx: &mut Context<Self>,
) {
self.marked_text = Some(text);
self.marked_range_utf16 = Some(range);
self.ime_state = Some(ImeState {
marked_text: text,
marked_range_utf16: range,
});
cx.notify();
}
/// Gets the current marked range (UTF-16).
pub(crate) fn marked_text_range(&self) -> Option<Range<usize>> {
self.marked_range_utf16.clone()
self.ime_state
.as_ref()
.and_then(|state| state.marked_range_utf16.clone())
}
/// Clears the marked (pre-edit) text state.
pub(crate) fn clear_marked_text(&mut self, cx: &mut Context<Self>) {
if self.marked_text.is_some() {
self.marked_text = None;
self.marked_range_utf16 = None;
if self.ime_state.is_some() {
self.ime_state = None;
cx.notify();
}
}

View File

@@ -16,7 +16,7 @@ struct FontFamilyCacheState {
/// so we do it once and then use the cached values each render.
#[derive(Default)]
pub struct FontFamilyCache {
state: RwLock<FontFamilyCacheState>,
state: Arc<RwLock<FontFamilyCacheState>>,
}
#[derive(Default)]
@@ -52,4 +52,44 @@ impl FontFamilyCache {
lock.font_families.clone()
}
/// Returns the list of font families if they have been loaded
pub fn try_list_font_families(&self) -> Option<Vec<SharedString>> {
self.state
.try_read()
.filter(|state| state.loaded_at.is_some())
.map(|state| state.font_families.clone())
}
/// Prefetch all font names in the background
pub async fn prefetch(&self, cx: &gpui::AsyncApp) {
if self
.state
.try_read()
.is_none_or(|state| state.loaded_at.is_some())
{
return;
}
let Ok(text_system) = cx.update(|cx| App::text_system(cx).clone()) else {
return;
};
let state = self.state.clone();
cx.background_executor()
.spawn(async move {
// We take this lock in the background executor to ensure that synchronous calls to `list_font_families` are blocked while we are prefetching,
// while not blocking the main thread and risking deadlocks
let mut lock = state.write();
let all_font_names = text_system
.all_font_names()
.into_iter()
.map(SharedString::from)
.collect();
lock.font_families = all_font_names;
lock.loaded_at = Some(Instant::now());
})
.await;
}
}

View File

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

View File

@@ -1 +1 @@
preview
stable

View File

@@ -1317,15 +1317,31 @@ pub fn handle_keymap_file_changes(
})
.detach();
let mut current_layout_id = cx.keyboard_layout().id().to_string();
cx.on_keyboard_layout_change(move |cx| {
let next_layout_id = cx.keyboard_layout().id();
if next_layout_id != current_layout_id {
current_layout_id = next_layout_id.to_string();
keyboard_layout_tx.unbounded_send(()).ok();
}
})
.detach();
#[cfg(target_os = "windows")]
{
let mut current_layout_id = cx.keyboard_layout().id().to_string();
cx.on_keyboard_layout_change(move |cx| {
let next_layout_id = cx.keyboard_layout().id();
if next_layout_id != current_layout_id {
current_layout_id = next_layout_id.to_string();
keyboard_layout_tx.unbounded_send(()).ok();
}
})
.detach();
}
#[cfg(not(target_os = "windows"))]
{
let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
cx.on_keyboard_layout_change(move |cx| {
let next_mapping = cx.keyboard_mapper().get_key_equivalents();
if current_mapping.as_ref() != next_mapping {
current_mapping = next_mapping.cloned();
keyboard_layout_tx.unbounded_send(()).ok();
}
})
.detach();
}
load_default_keymap(cx);