Add user-visible output for remote operations (#25849)
This PR adds toasts for reporting success and errors from remote git operations. This PR also adds a focus handle to notifications, in anticipation of making them keyboard accessible. Release Notes: - N/A --------- Co-authored-by: julia <julia@zed.dev>
This commit is contained in:
parent
508b9d3b5d
commit
73ac19958a
38 changed files with 713 additions and 192 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -5353,6 +5353,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"text",
|
||||
"time",
|
||||
"unindent",
|
||||
|
@ -5408,7 +5409,9 @@ dependencies = [
|
|||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"linkify",
|
||||
"linkme",
|
||||
"log",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"panel",
|
||||
|
|
1
assets/icons/cloud.svg
Normal file
1
assets/icons/cloud.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>
|
After Width: | Height: | Size: 279 B |
|
@ -32,12 +32,12 @@
|
|||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
|
|
|
@ -141,19 +141,20 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
|
|||
cx,
|
||||
move |cx| {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
cx.new(|_cx| {
|
||||
MessageNotification::new(format!("Updated to {app_name} {}", version))
|
||||
.primary_message("View Release Notes")
|
||||
.primary_on_click(move |window, cx| {
|
||||
if let Some(workspace) = workspace_handle.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(
|
||||
workspace, window, cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new(
|
||||
format!("Updated to {app_name} {}", version),
|
||||
cx,
|
||||
)
|
||||
.primary_message("View Release Notes")
|
||||
.primary_on_click(move |window, cx| {
|
||||
if let Some(workspace) = workspace_handle.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(workspace, window, cx);
|
||||
})
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
|
|
|
@ -82,7 +82,7 @@ impl Render for Breadcrumbs {
|
|||
text_style.color = Color::Muted.color(cx);
|
||||
|
||||
StyledText::new(segment.text.replace('\n', "⏎"))
|
||||
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
|
||||
.with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
|
|
|
@ -22,7 +22,7 @@ use ui::{
|
|||
h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
|
@ -570,11 +570,12 @@ impl NotificationPanel {
|
|||
workspace.dismiss_notification(&id, cx);
|
||||
workspace.show_notification(id, cx, |cx| {
|
||||
let workspace = cx.entity().downgrade();
|
||||
cx.new(|_| NotificationToast {
|
||||
cx.new(|cx| NotificationToast {
|
||||
notification_id,
|
||||
actor,
|
||||
text,
|
||||
workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -771,8 +772,17 @@ pub struct NotificationToast {
|
|||
actor: Option<Arc<User>>,
|
||||
text: String,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for NotificationToast {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceNotification for NotificationToast {}
|
||||
|
||||
impl NotificationToast {
|
||||
fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
|
|
|
@ -995,7 +995,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
|||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
StyledText::new(message.clone()).with_highlights(
|
||||
StyledText::new(message.clone()).with_default_highlights(
|
||||
&cx.window.text_style(),
|
||||
code_ranges
|
||||
.iter()
|
||||
|
|
|
@ -514,7 +514,7 @@ impl CompletionsMenu {
|
|||
);
|
||||
|
||||
let completion_label = StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
.with_default_highlights(&style.text, highlights);
|
||||
let documentation_label = if let Some(
|
||||
CompletionDocumentation::SingleLine(text),
|
||||
) = documentation
|
||||
|
|
|
@ -6781,7 +6781,7 @@ impl Editor {
|
|||
.first_line_preview();
|
||||
|
||||
let styled_text = gpui::StyledText::new(highlighted_edits.text)
|
||||
.with_highlights(&style.text, highlighted_edits.highlights);
|
||||
.with_default_highlights(&style.text, highlighted_edits.highlights);
|
||||
|
||||
let preview = h_flex()
|
||||
.gap_1()
|
||||
|
@ -18007,7 +18007,7 @@ pub fn diagnostic_block_renderer(
|
|||
)
|
||||
.child(buttons(&diagnostic))
|
||||
.child(div().flex().flex_shrink_0().child(
|
||||
StyledText::new(text_without_backticks.clone()).with_highlights(
|
||||
StyledText::new(text_without_backticks.clone()).with_default_highlights(
|
||||
&text_style,
|
||||
code_ranges.iter().map(|range| {
|
||||
(
|
||||
|
|
|
@ -310,7 +310,7 @@ impl SignatureHelpPopover {
|
|||
.child(
|
||||
div().px_4().pb_1().child(
|
||||
StyledText::new(self.label.clone())
|
||||
.with_highlights(&self.style, self.highlights.iter().cloned()),
|
||||
.with_default_highlights(&self.style, self.highlights.iter().cloned()),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
|
|
|
@ -168,11 +168,14 @@ pub(crate) fn suggest(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Cont
|
|||
);
|
||||
|
||||
workspace.show_notification(notification_id, cx, |cx| {
|
||||
cx.new(move |_cx| {
|
||||
MessageNotification::new(format!(
|
||||
"Do you want to install the recommended '{}' extension for '{}' files?",
|
||||
extension_id, file_name_or_extension
|
||||
))
|
||||
cx.new(move |cx| {
|
||||
MessageNotification::new(
|
||||
format!(
|
||||
"Do you want to install the recommended '{}' extension for '{}' files?",
|
||||
extension_id, file_name_or_extension
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.primary_message("Yes, install extension")
|
||||
.primary_icon(IconName::Check)
|
||||
.primary_icon_color(Color::Success)
|
||||
|
|
|
@ -192,7 +192,7 @@ impl Match {
|
|||
}
|
||||
}
|
||||
|
||||
StyledText::new(text).with_highlights(&window.text_style().clone(), highlights)
|
||||
StyledText::new(text).with_default_highlights(&window.text_style().clone(), highlights)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ text.workspace = true
|
|||
time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
|
|
@ -11,6 +11,8 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use std::borrow::Borrow;
|
||||
use std::io::Write as _;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Stdio;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
|
@ -61,6 +63,12 @@ pub enum UpstreamTracking {
|
|||
Tracked(UpstreamTrackingStatus),
|
||||
}
|
||||
|
||||
impl From<UpstreamTrackingStatus> for UpstreamTracking {
|
||||
fn from(status: UpstreamTrackingStatus) -> Self {
|
||||
UpstreamTracking::Tracked(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpstreamTracking {
|
||||
pub fn is_gone(&self) -> bool {
|
||||
matches!(self, UpstreamTracking::Gone)
|
||||
|
@ -74,9 +82,15 @@ impl UpstreamTracking {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<UpstreamTrackingStatus> for UpstreamTracking {
|
||||
fn from(status: UpstreamTrackingStatus) -> Self {
|
||||
UpstreamTracking::Tracked(status)
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteCommandOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
impl RemoteCommandOutput {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.stdout.is_empty() && self.stderr.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,10 +199,10 @@ pub trait GitRepository: Send + Sync {
|
|||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
) -> Result<()>;
|
||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>;
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
||||
fn fetch(&self) -> Result<()>;
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
|
||||
|
@ -611,19 +625,30 @@ impl GitRepository for RealGitRepository {
|
|||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
) -> Result<()> {
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command("git")
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
command
|
||||
.current_dir(&working_directory)
|
||||
.args(["push", "--quiet"])
|
||||
.args(["push"])
|
||||
.args(options.map(|option| match option {
|
||||
PushOptions::SetUpstream => "--set-upstream",
|
||||
PushOptions::Force => "--force-with-lease",
|
||||
}))
|
||||
.arg(remote_name)
|
||||
.arg(format!("{}:{}", branch_name, branch_name))
|
||||
.output()?;
|
||||
.arg(format!("{}:{}", branch_name, branch_name));
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
|
@ -631,19 +656,33 @@ impl GitRepository for RealGitRepository {
|
|||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
Ok(())
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command("git")
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
command
|
||||
.current_dir(&working_directory)
|
||||
.args(["pull", "--quiet"])
|
||||
.args(["pull"])
|
||||
.arg(remote_name)
|
||||
.arg(branch_name)
|
||||
.output()?;
|
||||
.arg(branch_name);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
|
@ -651,17 +690,31 @@ impl GitRepository for RealGitRepository {
|
|||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(());
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<()> {
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
let output = new_std_command("git")
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
command
|
||||
.current_dir(&working_directory)
|
||||
.args(["fetch", "--quiet", "--all"])
|
||||
.output()?;
|
||||
.args(["fetch", "--all"]);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
|
@ -669,7 +722,10 @@ impl GitRepository for RealGitRepository {
|
|||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(());
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -716,6 +772,18 @@ impl GitRepository for RealGitRepository {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-git-askpass")
|
||||
.tempdir()?;
|
||||
let askpass_script = "#!/bin/sh\necho ''";
|
||||
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
|
||||
std::fs::write(&askpass_script_path, askpass_script)?;
|
||||
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
|
||||
Ok((askpass_script_path, temp_dir))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FakeGitRepository {
|
||||
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||
|
@ -899,15 +967,20 @@ impl GitRepository for FakeGitRepository {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
|
||||
fn push(
|
||||
&self,
|
||||
_branch: &str,
|
||||
_remote: &str,
|
||||
_options: Option<PushOptions>,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<()> {
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,9 @@ git.workspace = true
|
|||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
panel.workspace = true
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
|
||||
use crate::repository_selector::RepositorySelectorPopoverMenu;
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
|
@ -12,8 +13,8 @@ use editor::{
|
|||
ShowScrollbar,
|
||||
};
|
||||
use git::repository::{
|
||||
Branch, CommitDetails, CommitSummary, PushOptions, Remote, ResetMode, Upstream,
|
||||
UpstreamTracking, UpstreamTrackingStatus,
|
||||
Branch, CommitDetails, CommitSummary, PushOptions, Remote, RemoteCommandOutput, ResetMode,
|
||||
Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||
};
|
||||
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
|
||||
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||
|
@ -43,6 +44,7 @@ use ui::{
|
|||
PopoverButton, PopoverMenu, Scrollbar, ScrollbarState, Tooltip,
|
||||
};
|
||||
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotificationId},
|
||||
|
@ -283,6 +285,7 @@ impl GitPanel {
|
|||
let commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
|
||||
});
|
||||
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
});
|
||||
|
@ -1330,62 +1333,114 @@ impl GitPanel {
|
|||
};
|
||||
let guard = self.start_remote_operation();
|
||||
let fetch = repo.read(cx).fetch();
|
||||
cx.spawn(|_, _| async move {
|
||||
fetch.await??;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let remote_message = fetch.await?;
|
||||
drop(guard);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
match remote_message {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Fetch, remote_message, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.show_err_toast(e, cx);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
return;
|
||||
};
|
||||
let branch = branch.clone();
|
||||
let guard = self.start_remote_operation();
|
||||
let remote = self.get_current_remote(window, cx);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let remote = remote.await?;
|
||||
let remote = match remote.await {
|
||||
Ok(Some(remote)) => remote,
|
||||
Ok(None) => {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get current remote: {}", e);
|
||||
this.update(&mut cx, |this, cx| this.show_err_toast(e, cx))
|
||||
.ok();
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let Some(repo) = this.active_repository.clone() else {
|
||||
return Err(anyhow::anyhow!("No active repository"));
|
||||
};
|
||||
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
return Err(anyhow::anyhow!("No active branch"));
|
||||
};
|
||||
|
||||
Ok(repo.read(cx).pull(branch.name.clone(), remote.name))
|
||||
})??
|
||||
.await??;
|
||||
let pull = repo.update(&mut cx, |repo, _cx| {
|
||||
repo.pull(branch.name.clone(), remote.name.clone())
|
||||
})?;
|
||||
|
||||
let remote_message = pull.await?;
|
||||
drop(guard);
|
||||
|
||||
this.update(&mut cx, |this, cx| match remote_message {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Pull, remote_message, cx)
|
||||
}
|
||||
Err(err) => this.show_err_toast(err, cx),
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
return;
|
||||
};
|
||||
let branch = branch.clone();
|
||||
let guard = self.start_remote_operation();
|
||||
let options = action.options;
|
||||
let remote = self.get_current_remote(window, cx);
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let remote = remote.await?;
|
||||
let remote = match remote.await {
|
||||
Ok(Some(remote)) => remote,
|
||||
Ok(None) => {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get current remote: {}", e);
|
||||
this.update(&mut cx, |this, cx| this.show_err_toast(e, cx))
|
||||
.ok();
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let Some(repo) = this.active_repository.clone() else {
|
||||
return Err(anyhow::anyhow!("No active repository"));
|
||||
};
|
||||
let push = repo.update(&mut cx, |repo, _cx| {
|
||||
repo.push(branch.name.clone(), remote.name.clone(), options)
|
||||
})?;
|
||||
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
return Err(anyhow::anyhow!("No active branch"));
|
||||
};
|
||||
|
||||
Ok(repo
|
||||
.read(cx)
|
||||
.push(branch.name.clone(), remote.name, options))
|
||||
})??
|
||||
.await??;
|
||||
let remote_output = push.await?;
|
||||
|
||||
drop(guard);
|
||||
|
||||
this.update(&mut cx, |this, cx| match remote_output {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Push(remote), remote_message, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.show_err_toast(e, cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
@ -1395,7 +1450,7 @@ impl GitPanel {
|
|||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl Future<Output = Result<Remote>> {
|
||||
) -> impl Future<Output = Result<Option<Remote>>> {
|
||||
let repo = self.active_repository.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let mut cx = window.to_async(cx);
|
||||
|
@ -1418,7 +1473,7 @@ impl GitPanel {
|
|||
if current_remotes.len() == 0 {
|
||||
return Err(anyhow::anyhow!("No active remote"));
|
||||
} else if current_remotes.len() == 1 {
|
||||
return Ok(current_remotes.pop().unwrap());
|
||||
return Ok(Some(current_remotes.pop().unwrap()));
|
||||
} else {
|
||||
let current_remotes: Vec<_> = current_remotes
|
||||
.into_iter()
|
||||
|
@ -1436,9 +1491,9 @@ impl GitPanel {
|
|||
})?
|
||||
.await?;
|
||||
|
||||
return Ok(Remote {
|
||||
Ok(selection.map(|selection| Remote {
|
||||
name: current_remotes[selection].clone(),
|
||||
});
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1789,16 +1844,40 @@ impl GitPanel {
|
|||
};
|
||||
let notif_id = NotificationId::Named("git-operation-error".into());
|
||||
|
||||
let message = e.to_string();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
|
||||
let mut message = e.to_string().trim().to_string();
|
||||
let toast;
|
||||
if message.matches("Authentication failed").count() >= 1 {
|
||||
message = format!(
|
||||
"{}\n\n{}",
|
||||
message, "Please set your credentials via the CLI"
|
||||
);
|
||||
toast = Toast::new(notif_id, message);
|
||||
} else {
|
||||
toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
|
||||
window.dispatch_action(workspace::OpenLog.boxed_clone(), cx);
|
||||
});
|
||||
}
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(toast, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_spinner(&self) -> Option<impl IntoElement> {
|
||||
fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let notification_id = NotificationId::Named("git-remote-info".into());
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_notification(notification_id.clone(), cx, |cx| {
|
||||
let workspace = cx.weak_entity();
|
||||
cx.new(|cx| RemoteOutputToast::new(action, info, notification_id, workspace, cx))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_spinner(&self) -> Option<impl IntoElement> {
|
||||
(!self.pending_remote_operations.borrow().is_empty()).then(|| {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
|
@ -2274,9 +2353,10 @@ impl GitPanel {
|
|||
let Some(repo) = self.active_repository.clone() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active repo")));
|
||||
};
|
||||
|
||||
let show = repo.read(cx).show(sha);
|
||||
cx.spawn(|_, _| async move { show.await? })
|
||||
repo.update(cx, |repo, cx| {
|
||||
let show = repo.show(sha);
|
||||
cx.spawn(|_, _| async move { show.await? })
|
||||
})
|
||||
}
|
||||
|
||||
fn deploy_entry_context_menu(
|
||||
|
|
|
@ -11,6 +11,7 @@ pub mod git_panel;
|
|||
mod git_panel_settings;
|
||||
pub mod picker_prompt;
|
||||
pub mod project_diff;
|
||||
mod remote_output_toast;
|
||||
pub mod repository_selector;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
|
|
|
@ -26,7 +26,7 @@ pub fn prompt(
|
|||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<usize>> {
|
||||
) -> Task<Result<Option<usize>>> {
|
||||
if options.is_empty() {
|
||||
return Task::ready(Err(anyhow!("No options")));
|
||||
}
|
||||
|
@ -43,7 +43,10 @@ pub fn prompt(
|
|||
})
|
||||
})?;
|
||||
|
||||
rx.await?
|
||||
match rx.await {
|
||||
Ok(selection) => Some(selection).transpose(),
|
||||
Err(_) => anyhow::Ok(None), // User cancelled
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
214
crates/git_ui/src/remote_output_toast.rs
Normal file
214
crates/git_ui/src/remote_output_toast.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use git::repository::{Remote, RemoteCommandOutput};
|
||||
use gpui::{
|
||||
DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
|
||||
StyledText, Task, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use ui::{
|
||||
div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
|
||||
IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
||||
Render, SharedString, Styled, StyledExt, Window,
|
||||
};
|
||||
use workspace::{
|
||||
notifications::{Notification, NotificationId},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
pub enum RemoteAction {
|
||||
Fetch,
|
||||
Pull,
|
||||
Push(Remote),
|
||||
}
|
||||
|
||||
struct InfoFromRemote {
|
||||
name: SharedString,
|
||||
remote_text: SharedString,
|
||||
links: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
pub struct RemoteOutputToast {
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_id: NotificationId,
|
||||
message: SharedString,
|
||||
remote_info: Option<InfoFromRemote>,
|
||||
_dismiss_task: Task<()>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for RemoteOutputToast {
|
||||
fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification for RemoteOutputToast {}
|
||||
|
||||
const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
|
||||
|
||||
impl RemoteOutputToast {
|
||||
pub fn new(
|
||||
action: RemoteAction,
|
||||
output: RemoteCommandOutput,
|
||||
id: NotificationId,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let task = cx.spawn({
|
||||
let workspace = workspace.clone();
|
||||
let id = id.clone();
|
||||
|_, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
|
||||
.await;
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.dismiss_notification(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
let message;
|
||||
let remote;
|
||||
|
||||
match action {
|
||||
RemoteAction::Fetch | RemoteAction::Pull => {
|
||||
if output.is_empty() {
|
||||
message = "Up to date".into();
|
||||
} else {
|
||||
message = output.stderr.into();
|
||||
}
|
||||
remote = None;
|
||||
}
|
||||
|
||||
RemoteAction::Push(remote_ref) => {
|
||||
message = output.stdout.trim().to_string().into();
|
||||
let remote_message = get_remote_lines(&output.stderr);
|
||||
let finder = LinkFinder::new();
|
||||
let links = finder
|
||||
.links(&remote_message)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.collect_vec();
|
||||
|
||||
remote = Some(InfoFromRemote {
|
||||
name: remote_ref.name,
|
||||
remote_text: remote_message.into(),
|
||||
links,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
_workspace: workspace,
|
||||
_id: id,
|
||||
message,
|
||||
remote_info: remote,
|
||||
_dismiss_task: task,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteOutputToast {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
div()
|
||||
.occlude()
|
||||
.w_full()
|
||||
.max_h(vh(0.8, window))
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::GitBranch).color(Color::Default))
|
||||
.child(Label::new("Git")),
|
||||
)
|
||||
.child(h_flex().child(
|
||||
IconButton::new("close", IconName::Close).on_click(
|
||||
cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(Label::new(self.message.clone()).size(LabelSize::Default))
|
||||
.when_some(self.remote_info.as_ref(), |this, remote_info| {
|
||||
this.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(Color::Muted.color(cx))
|
||||
.rounded_lg()
|
||||
.text_sm()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Cloud).color(Color::Default))
|
||||
.child(
|
||||
Label::new(remote_info.name.clone())
|
||||
.size(LabelSize::Default),
|
||||
),
|
||||
)
|
||||
.map(|div| {
|
||||
let styled_text =
|
||||
StyledText::new(remote_info.remote_text.clone())
|
||||
.with_highlights(remote_info.links.iter().map(
|
||||
|link| {
|
||||
(
|
||||
link.clone(),
|
||||
HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.0),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
let this = cx.weak_entity();
|
||||
let text = InteractiveText::new("remote-message", styled_text)
|
||||
.on_click(
|
||||
remote_info.links.clone(),
|
||||
move |ix, _window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(remote_info) = &this.remote_info {
|
||||
cx.open_url(
|
||||
&remote_info.remote_text
|
||||
[remote_info.links[ix].clone()],
|
||||
)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
|
||||
div.child(text)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RemoteOutputToast {}
|
||||
|
||||
fn get_remote_lines(output: &str) -> String {
|
||||
output
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix("remote:"))
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
|
@ -137,6 +137,7 @@ impl IntoElement for SharedString {
|
|||
pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||
layout: TextLayout,
|
||||
}
|
||||
|
||||
|
@ -146,6 +147,7 @@ impl StyledText {
|
|||
StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
delayed_highlights: None,
|
||||
layout: TextLayout::default(),
|
||||
}
|
||||
}
|
||||
|
@ -157,11 +159,39 @@ impl StyledText {
|
|||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
/// as any ranges of text that have had their style customized.
|
||||
pub fn with_highlights(
|
||||
pub fn with_default_highlights(
|
||||
mut self,
|
||||
default_style: &TextStyle,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
self.delayed_highlights.is_none(),
|
||||
"Can't use `with_default_highlights` and `with_highlights`"
|
||||
);
|
||||
let runs = Self::compute_runs(&self.text, default_style, highlights);
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
/// as any ranges of text that have had their style customized.
|
||||
pub fn with_highlights(
|
||||
mut self,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
self.runs.is_none(),
|
||||
"Can't use `with_highlights` and `with_default_highlights`"
|
||||
);
|
||||
self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
|
||||
self
|
||||
}
|
||||
|
||||
fn compute_runs(
|
||||
text: &str,
|
||||
default_style: &TextStyle,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Vec<TextRun> {
|
||||
let mut runs = Vec::new();
|
||||
let mut ix = 0;
|
||||
for (range, highlight) in highlights {
|
||||
|
@ -176,11 +206,10 @@ impl StyledText {
|
|||
);
|
||||
ix = range.end;
|
||||
}
|
||||
if ix < self.text.len() {
|
||||
runs.push(default_style.to_run(self.text.len() - ix));
|
||||
if ix < text.len() {
|
||||
runs.push(default_style.to_run(text.len() - ix));
|
||||
}
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
runs
|
||||
}
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
|
@ -200,15 +229,17 @@ impl Element for StyledText {
|
|||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
|
||||
_id: Option<&GlobalElementId>,
|
||||
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.layout
|
||||
.layout(self.text.clone(), self.runs.take(), window, cx);
|
||||
let runs = self.runs.take().or_else(|| {
|
||||
self.delayed_highlights.take().map(|delayed_highlights| {
|
||||
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
|
||||
})
|
||||
});
|
||||
|
||||
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
|
|
|
@ -590,7 +590,7 @@ impl HighlightedText {
|
|||
|
||||
pub fn to_styled_text(&self, default_style: &TextStyle) -> StyledText {
|
||||
gpui::StyledText::new(self.text.clone())
|
||||
.with_highlights(default_style, self.highlights.iter().cloned())
|
||||
.with_default_highlights(default_style, self.highlights.iter().cloned())
|
||||
}
|
||||
|
||||
/// Returns the first line without leading whitespace unless highlighted
|
||||
|
|
|
@ -370,7 +370,7 @@ fn render_markdown_code_block(
|
|||
cx: &mut RenderContext,
|
||||
) -> AnyElement {
|
||||
let body = if let Some(highlights) = parsed.highlights.as_ref() {
|
||||
StyledText::new(parsed.contents.clone()).with_highlights(
|
||||
StyledText::new(parsed.contents.clone()).with_default_highlights(
|
||||
&cx.buffer_text_style,
|
||||
highlights.iter().filter_map(|(range, highlight_id)| {
|
||||
highlight_id
|
||||
|
@ -468,7 +468,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
|
|||
InteractiveText::new(
|
||||
element_id,
|
||||
StyledText::new(parsed.contents.clone())
|
||||
.with_highlights(&text_style, highlights),
|
||||
.with_default_highlights(&text_style, highlights),
|
||||
)
|
||||
.tooltip({
|
||||
let links = links.clone();
|
||||
|
|
|
@ -359,7 +359,7 @@ pub fn render_item<T>(
|
|||
outline_item.highlight_ranges.iter().cloned(),
|
||||
);
|
||||
|
||||
StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights)
|
||||
StyledText::new(outline_item.text.clone()).with_default_highlights(&text_style, highlights)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
|
|||
use client::ProjectId;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::StreamExt as _;
|
||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode};
|
||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode};
|
||||
use git::{
|
||||
repository::{GitRepository, RepoPath},
|
||||
status::{GitSummary, TrackedSummary},
|
||||
|
@ -265,23 +265,27 @@ impl GitStore {
|
|||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Fetch>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
) -> Result<proto::RemoteMessageResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
repository_handle
|
||||
let remote_output = repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
|
||||
Ok(proto::RemoteMessageResponse {
|
||||
stdout: remote_output.stdout,
|
||||
stderr: remote_output.stderr,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_push(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Push>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
) -> Result<proto::RemoteMessageResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
|
@ -299,19 +303,22 @@ impl GitStore {
|
|||
let branch_name = envelope.payload.branch_name.into();
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
|
||||
repository_handle
|
||||
let remote_output = repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| {
|
||||
repository_handle.push(branch_name, remote_name, options)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
Ok(proto::RemoteMessageResponse {
|
||||
stdout: remote_output.stdout,
|
||||
stderr: remote_output.stderr,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_pull(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Pull>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
) -> Result<proto::RemoteMessageResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
|
@ -320,12 +327,15 @@ impl GitStore {
|
|||
let branch_name = envelope.payload.branch_name.into();
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
|
||||
repository_handle
|
||||
let remote_message = repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| {
|
||||
repository_handle.pull(branch_name, remote_name)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::Ack {})
|
||||
Ok(proto::RemoteMessageResponse {
|
||||
stdout: remote_message.stdout,
|
||||
stderr: remote_message.stderr,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_stage(
|
||||
|
@ -1086,7 +1096,7 @@ impl Repository {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> oneshot::Receiver<Result<()>> {
|
||||
pub fn fetch(&self) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
self.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => git_repository.fetch(),
|
||||
|
@ -1096,7 +1106,7 @@ impl Repository {
|
|||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
let response = client
|
||||
.request(proto::Fetch {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
|
@ -1105,7 +1115,10 @@ impl Repository {
|
|||
.await
|
||||
.context("sending fetch request")?;
|
||||
|
||||
Ok(())
|
||||
Ok(RemoteCommandOutput {
|
||||
stdout: response.stdout,
|
||||
stderr: response.stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1116,7 +1129,7 @@ impl Repository {
|
|||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
options: Option<PushOptions>,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => git_repository.push(&branch, &remote, options),
|
||||
|
@ -1126,7 +1139,7 @@ impl Repository {
|
|||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
let response = client
|
||||
.request(proto::Push {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
|
@ -1141,7 +1154,10 @@ impl Repository {
|
|||
.await
|
||||
.context("sending push request")?;
|
||||
|
||||
Ok(())
|
||||
Ok(RemoteCommandOutput {
|
||||
stdout: response.stdout,
|
||||
stderr: response.stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1151,7 +1167,7 @@ impl Repository {
|
|||
&self,
|
||||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
self.send_job(|git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => git_repository.pull(&branch, &remote),
|
||||
|
@ -1161,7 +1177,7 @@ impl Repository {
|
|||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
let response = client
|
||||
.request(proto::Pull {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
|
@ -1172,8 +1188,10 @@ impl Repository {
|
|||
.await
|
||||
.context("sending pull request")?;
|
||||
|
||||
// TODO: wire through remote
|
||||
Ok(())
|
||||
Ok(RemoteCommandOutput {
|
||||
stdout: response.stdout,
|
||||
stderr: response.stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -252,8 +252,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
|
|||
v_flex()
|
||||
.child(
|
||||
LabelLike::new().child(
|
||||
StyledText::new(label)
|
||||
.with_highlights(&window.text_style().clone(), highlights),
|
||||
StyledText::new(label).with_default_highlights(
|
||||
&window.text_style().clone(),
|
||||
highlights,
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(Label::new(path).color(Color::Muted)),
|
||||
|
|
|
@ -330,7 +330,9 @@ message Envelope {
|
|||
Pull pull = 308;
|
||||
|
||||
ApplyCodeActionKind apply_code_action_kind = 309;
|
||||
ApplyCodeActionKindResponse apply_code_action_kind_response = 310; // current max
|
||||
ApplyCodeActionKindResponse apply_code_action_kind_response = 310;
|
||||
|
||||
RemoteMessageResponse remote_message_response = 311; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
@ -2834,3 +2836,8 @@ message Pull {
|
|||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
}
|
||||
|
||||
message RemoteMessageResponse {
|
||||
string stdout = 1;
|
||||
string stderr = 2;
|
||||
}
|
||||
|
|
|
@ -451,6 +451,7 @@ messages!(
|
|||
(GetRemotes, Background),
|
||||
(GetRemotesResponse, Background),
|
||||
(Pull, Background),
|
||||
(RemoteMessageResponse, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
@ -589,10 +590,10 @@ request_messages!(
|
|||
(GitReset, Ack),
|
||||
(GitCheckoutFiles, Ack),
|
||||
(SetIndexText, Ack),
|
||||
(Push, Ack),
|
||||
(Fetch, Ack),
|
||||
(Push, RemoteMessageResponse),
|
||||
(Fetch, RemoteMessageResponse),
|
||||
(GetRemotes, GetRemotesResponse),
|
||||
(Pull, Ack),
|
||||
(Pull, RemoteMessageResponse),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
|
|
@ -96,7 +96,7 @@ impl RichText {
|
|||
|
||||
InteractiveText::new(
|
||||
id,
|
||||
StyledText::new(self.text.clone()).with_highlights(
|
||||
StyledText::new(self.text.clone()).with_default_highlights(
|
||||
&window.text_style(),
|
||||
self.highlights.iter().map(|(range, highlight)| {
|
||||
(
|
||||
|
|
|
@ -81,7 +81,7 @@ impl Render for TextStory {
|
|||
"Interactive Text",
|
||||
InteractiveText::new(
|
||||
"interactive",
|
||||
StyledText::new("Hello world, how is it going?").with_highlights(
|
||||
StyledText::new("Hello world, how is it going?").with_default_highlights(
|
||||
&window.text_style(),
|
||||
[
|
||||
(
|
||||
|
|
|
@ -237,6 +237,7 @@ pub enum IconName {
|
|||
Menu,
|
||||
MessageBubbles,
|
||||
MessageCircle,
|
||||
Cloud,
|
||||
Mic,
|
||||
MicMute,
|
||||
Microscope,
|
||||
|
|
|
@ -126,6 +126,6 @@ impl RenderOnce for HighlightedLabel {
|
|||
text_style.color = self.base.color.color(cx);
|
||||
|
||||
self.base
|
||||
.child(StyledText::new(self.label).with_highlights(&text_style, highlights))
|
||||
.child(StyledText::new(self.label).with_default_highlights(&text_style, highlights))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ impl ModalLayer {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
let Some(active_modal) = self.active_modal.as_mut() else {
|
||||
self.dismiss_on_focus_lost = false;
|
||||
return false;
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
use crate::{Toast, Workspace};
|
||||
use gpui::{
|
||||
svg, AnyView, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
|
||||
Entity, EventEmitter, PromptLevel, Render, ScrollHandle, Task,
|
||||
Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, Task,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::{any::TypeId, time::Duration};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Notifications {
|
||||
notifications: Vec<(NotificationId, AnyView)>,
|
||||
}
|
||||
|
||||
impl Deref for Notifications {
|
||||
type Target = Vec<(NotificationId, AnyView)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.notifications
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Notifications {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.notifications
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum NotificationId {
|
||||
Unique(TypeId),
|
||||
|
@ -34,9 +54,7 @@ impl NotificationId {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait Notification: EventEmitter<DismissEvent> + Render {}
|
||||
|
||||
impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
|
||||
pub trait Notification: EventEmitter<DismissEvent> + Focusable + Render {}
|
||||
|
||||
impl Workspace {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -89,7 +107,7 @@ impl Workspace {
|
|||
E: std::fmt::Debug + std::fmt::Display,
|
||||
{
|
||||
self.show_notification(workspace_error_notification_id(), cx, |cx| {
|
||||
cx.new(|_| ErrorMessagePrompt::new(format!("Error: {err}")))
|
||||
cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx))
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -97,8 +115,8 @@ impl Workspace {
|
|||
struct PortalError;
|
||||
|
||||
self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
|
||||
cx.new(|_| {
|
||||
ErrorMessagePrompt::new(err.to_string()).with_link_button(
|
||||
cx.new(|cx| {
|
||||
ErrorMessagePrompt::new(err.to_string(), cx).with_link_button(
|
||||
"See docs",
|
||||
"https://zed.dev/docs/linux#i-cant-open-any-files",
|
||||
)
|
||||
|
@ -120,14 +138,16 @@ impl Workspace {
|
|||
pub fn show_toast(&mut self, toast: Toast, cx: &mut Context<Self>) {
|
||||
self.dismiss_notification(&toast.id, cx);
|
||||
self.show_notification(toast.id.clone(), cx, |cx| {
|
||||
cx.new(|_| match toast.on_click.as_ref() {
|
||||
cx.new(|cx| match toast.on_click.as_ref() {
|
||||
Some((click_msg, on_click)) => {
|
||||
let on_click = on_click.clone();
|
||||
simple_message_notification::MessageNotification::new(toast.msg.clone())
|
||||
simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
|
||||
.primary_message(click_msg.clone())
|
||||
.primary_on_click(move |window, cx| on_click(window, cx))
|
||||
}
|
||||
None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
|
||||
None => {
|
||||
simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
|
||||
}
|
||||
})
|
||||
});
|
||||
if toast.autohide {
|
||||
|
@ -171,13 +191,23 @@ impl Workspace {
|
|||
}
|
||||
|
||||
pub struct LanguageServerPrompt {
|
||||
focus_handle: FocusHandle,
|
||||
request: Option<project::LanguageServerPromptRequest>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl Focusable for LanguageServerPrompt {
|
||||
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification for LanguageServerPrompt {}
|
||||
|
||||
impl LanguageServerPrompt {
|
||||
pub fn new(request: project::LanguageServerPromptRequest) -> Self {
|
||||
pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
request: Some(request),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
}
|
||||
|
@ -286,16 +316,18 @@ fn workspace_error_notification_id() -> NotificationId {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct ErrorMessagePrompt {
|
||||
message: SharedString,
|
||||
focus_handle: gpui::FocusHandle,
|
||||
label_and_url_button: Option<(SharedString, SharedString)>,
|
||||
}
|
||||
|
||||
impl ErrorMessagePrompt {
|
||||
pub fn new<S>(message: S) -> Self
|
||||
pub fn new<S>(message: S, cx: &mut App) -> Self
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
Self {
|
||||
message: message.into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
label_and_url_button: None,
|
||||
}
|
||||
}
|
||||
|
@ -364,17 +396,29 @@ impl Render for ErrorMessagePrompt {
|
|||
}
|
||||
}
|
||||
|
||||
impl Focusable for ErrorMessagePrompt {
|
||||
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
|
||||
|
||||
impl Notification for ErrorMessagePrompt {}
|
||||
|
||||
pub mod simple_message_notification {
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
div, AnyElement, DismissEvent, EventEmitter, ParentElement, Render, SharedString, Styled,
|
||||
div, AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
|
||||
SharedString, Styled,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
|
||||
use super::Notification;
|
||||
|
||||
pub struct MessageNotification {
|
||||
focus_handle: FocusHandle,
|
||||
build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
|
||||
primary_message: Option<SharedString>,
|
||||
primary_icon: Option<IconName>,
|
||||
|
@ -390,18 +434,28 @@ pub mod simple_message_notification {
|
|||
title: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Focusable for MessageNotification {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for MessageNotification {}
|
||||
|
||||
impl Notification for MessageNotification {}
|
||||
|
||||
impl MessageNotification {
|
||||
pub fn new<S>(message: S) -> MessageNotification
|
||||
pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
let message = message.into();
|
||||
Self::new_from_builder(move |_, _| Label::new(message.clone()).into_any_element())
|
||||
Self::new_from_builder(cx, move |_, _| {
|
||||
Label::new(message.clone()).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_from_builder<F>(content: F) -> MessageNotification
|
||||
pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
|
||||
where
|
||||
F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
|
||||
{
|
||||
|
@ -419,6 +473,7 @@ pub mod simple_message_notification {
|
|||
more_info_url: None,
|
||||
show_close_button: true,
|
||||
title: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -769,7 +824,7 @@ where
|
|||
move |cx| {
|
||||
cx.new({
|
||||
let message = message.clone();
|
||||
move |_cx| ErrorMessagePrompt::new(message)
|
||||
move |cx| ErrorMessagePrompt::new(message, cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1156,11 +1156,14 @@ impl WorkspaceDb {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::persistence::model::SerializedWorkspace;
|
||||
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
|
||||
use db::open_test_db;
|
||||
use gpui::{self};
|
||||
use gpui;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_id_stability() {
|
||||
|
@ -1556,31 +1559,33 @@ mod tests {
|
|||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
|
||||
db.save_workspace(workspace_4.clone()).await;
|
||||
db.save_workspace(workspace_5.clone()).await;
|
||||
db.save_workspace(workspace_6.clone()).await;
|
||||
|
||||
let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[0].2, Some(10));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
|
||||
assert_eq!(locations[0].2, Some(20));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[1].2, Some(20));
|
||||
assert_eq!(locations[1].2, Some(10));
|
||||
|
||||
let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[0].2, Some(30));
|
||||
let empty_paths: Vec<&str> = Vec::new();
|
||||
assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([]));
|
||||
assert_eq!(locations[1].2, Some(50));
|
||||
assert_eq!(locations[1].3, Some(ssh_project.id.0));
|
||||
assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([]));
|
||||
assert_eq!(locations[0].2, Some(50));
|
||||
assert_eq!(locations[0].3, Some(ssh_project.id.0));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[1].2, Some(30));
|
||||
|
||||
let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 1);
|
||||
|
|
|
@ -47,7 +47,9 @@ use itertools::Itertools;
|
|||
use language::{LanguageRegistry, Rope};
|
||||
pub use modal_layer::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use notifications::{simple_message_notification::MessageNotification, DetachAndPromptErr};
|
||||
use notifications::{
|
||||
simple_message_notification::MessageNotification, DetachAndPromptErr, Notifications,
|
||||
};
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
pub use persistence::{
|
||||
|
@ -815,7 +817,7 @@ pub struct Workspace {
|
|||
status_bar: Entity<StatusBar>,
|
||||
modal_layer: Entity<ModalLayer>,
|
||||
titlebar_item: Option<AnyView>,
|
||||
notifications: Vec<(NotificationId, AnyView)>,
|
||||
notifications: Notifications,
|
||||
project: Entity<Project>,
|
||||
follower_states: HashMap<PeerId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
|
||||
|
@ -920,7 +922,7 @@ impl Workspace {
|
|||
} => this.show_notification(
|
||||
NotificationId::named(notification_id.clone()),
|
||||
cx,
|
||||
|cx| cx.new(|_| MessageNotification::new(message.clone())),
|
||||
|cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)),
|
||||
),
|
||||
|
||||
project::Event::HideToast { notification_id } => {
|
||||
|
@ -937,7 +939,11 @@ impl Workspace {
|
|||
this.show_notification(
|
||||
NotificationId::composite::<LanguageServerPrompt>(id as usize),
|
||||
cx,
|
||||
|cx| cx.new(|_| notifications::LanguageServerPrompt::new(request.clone())),
|
||||
|cx| {
|
||||
cx.new(|cx| {
|
||||
notifications::LanguageServerPrompt::new(request.clone(), cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5223,8 +5229,8 @@ fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncA
|
|||
NotificationId::unique::<DatabaseFailedNotification>(),
|
||||
cx,
|
||||
|cx| {
|
||||
cx.new(|_| {
|
||||
MessageNotification::new("Failed to load the database file.")
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new("Failed to load the database file.", cx)
|
||||
.primary_message("File an Issue")
|
||||
.primary_icon(IconName::Plus)
|
||||
.primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
|
||||
|
|
|
@ -1114,11 +1114,14 @@ fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Contex
|
|||
NotificationId::unique::<OpenLogError>(),
|
||||
cx,
|
||||
|cx| {
|
||||
cx.new(|_| {
|
||||
MessageNotification::new(format!(
|
||||
"Unable to access/open log file at path {:?}",
|
||||
paths::log_file().as_path()
|
||||
))
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new(
|
||||
format!(
|
||||
"Unable to access/open log file at path {:?}",
|
||||
paths::log_file().as_path()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
@ -1323,8 +1326,8 @@ fn show_keymap_file_json_error(
|
|||
let message: SharedString =
|
||||
format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
|
||||
show_app_notification(notification_id, cx, move |cx| {
|
||||
cx.new(|_cx| {
|
||||
MessageNotification::new(message.clone())
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new(message.clone(), cx)
|
||||
.primary_message("Open Keymap File")
|
||||
.primary_on_click(|window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
|
||||
|
@ -1381,8 +1384,8 @@ fn show_markdown_app_notification<F>(
|
|||
let parsed_markdown = parsed_markdown.clone();
|
||||
let primary_button_message = primary_button_message.clone();
|
||||
let primary_button_on_click = primary_button_on_click.clone();
|
||||
cx.new(move |_cx| {
|
||||
MessageNotification::new_from_builder(move |window, cx| {
|
||||
cx.new(move |cx| {
|
||||
MessageNotification::new_from_builder(cx, move |window, cx| {
|
||||
gpui::div()
|
||||
.text_xs()
|
||||
.child(markdown_preview::markdown_renderer::render_parsed_markdown(
|
||||
|
@ -1441,8 +1444,8 @@ pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
|
|||
return;
|
||||
}
|
||||
show_app_notification(id, cx, move |cx| {
|
||||
cx.new(|_cx| {
|
||||
MessageNotification::new(format!("Invalid user settings file\n{error}"))
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
|
||||
.primary_message("Open Settings File")
|
||||
.primary_icon(IconName::Settings)
|
||||
.primary_on_click(|window, cx| {
|
||||
|
@ -1580,7 +1583,7 @@ fn open_local_file(
|
|||
struct NoOpenFolders;
|
||||
|
||||
workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
|
||||
cx.new(|_| MessageNotification::new("This project has no folders open."))
|
||||
cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ impl CompletionDiffElement {
|
|||
font_style: settings.buffer_font.style,
|
||||
..Default::default()
|
||||
};
|
||||
let element = StyledText::new(diff).with_highlights(&text_style, diff_highlights);
|
||||
let element = StyledText::new(diff).with_default_highlights(&text_style, diff_highlights);
|
||||
let text_layout = element.layout().clone();
|
||||
|
||||
CompletionDiffElement {
|
||||
|
|
|
@ -489,8 +489,8 @@ impl Zeta {
|
|||
NotificationId::unique::<ZedUpdateRequiredError>(),
|
||||
cx,
|
||||
|cx| {
|
||||
cx.new(|_| {
|
||||
ErrorMessagePrompt::new(err.to_string())
|
||||
cx.new(|cx| {
|
||||
ErrorMessagePrompt::new(err.to_string(), cx)
|
||||
.with_link_button(
|
||||
"Update Zed",
|
||||
"https://zed.dev/releases",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue