Compare commits
17 Commits
v0.203.2-p
...
v0.203.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63cab3ccc8 | ||
|
|
8aa6a94610 | ||
|
|
63860fc5c6 | ||
|
|
996f9cad68 | ||
|
|
21aa3d5cd7 | ||
|
|
c5d36e05f8 | ||
|
|
b63c715a53 | ||
|
|
d287b8bbf7 | ||
|
|
bbbfa10fe1 | ||
|
|
0c1ad95e8c | ||
|
|
29b7863403 | ||
|
|
66fbfd7080 | ||
|
|
92912915fa | ||
|
|
018935de63 | ||
|
|
fbec5e8dd5 | ||
|
|
bda30bb0eb | ||
|
|
aa95dbb670 |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
preview
|
||||
stable
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user