mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Compare commits
4 commits
3ed9114559
...
ca0429d13e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca0429d13e | ||
|
|
09165c15dc | ||
|
|
e2e7a6769e | ||
|
|
5d3b9e467e |
7 changed files with 916 additions and 67 deletions
|
|
@ -5,6 +5,7 @@ use crate::commit_view::CommitView;
|
|||
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
||||
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||
use crate::solo_diff_view::SoloDiffView;
|
||||
use crate::{branch_picker, picker_prompt, render_remote_button};
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
|
|
@ -15,10 +16,7 @@ use anyhow::Context as _;
|
|||
use askpass::AskPassDelegate;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::kvp::KeyValueStore;
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior,
|
||||
actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior};
|
||||
use editor::{EditorStyle, RewrapOptions};
|
||||
use file_icons::FileIcons;
|
||||
use futures::StreamExt as _;
|
||||
|
|
@ -85,7 +83,7 @@ use workspace::SERIALIZATION_THROTTLE_TIME;
|
|||
use workspace::{
|
||||
Item, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyTaskExt},
|
||||
};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
|
||||
|
|
@ -1385,63 +1383,22 @@ impl GitPanel {
|
|||
});
|
||||
}
|
||||
|
||||
fn open_file(
|
||||
fn open_solo_diff(
|
||||
&mut self,
|
||||
_: &menu::SecondaryConfirm,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
maybe!({
|
||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
||||
let active_repo = self.active_repository.as_ref()?;
|
||||
let path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
if entry.status.is_deleted() {
|
||||
return None;
|
||||
}
|
||||
let entry = self
|
||||
.entries
|
||||
.get(self.selected_entry?)?
|
||||
.status_entry()?
|
||||
.clone();
|
||||
let repository = self.active_repository.clone()?;
|
||||
|
||||
let open_task = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path_preview(path, None, false, false, true, window, cx)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn_in(window, async move |_, mut cx| {
|
||||
let item = open_task
|
||||
.await
|
||||
.notify_workspace_async_err(workspace, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
if let Some(diff_task) =
|
||||
active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load())
|
||||
{
|
||||
diff_task.await;
|
||||
}
|
||||
|
||||
cx.update(|window, cx| {
|
||||
active_editor.update(cx, |editor, cx| {
|
||||
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
|
||||
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
language::Point::new(0, 0),
|
||||
Direction::Next,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
SoloDiffView::open_or_focus(entry, repository, self.workspace.clone(), window, cx)
|
||||
.detach_and_notify_err(self.workspace.clone(), window, cx);
|
||||
|
||||
Some(())
|
||||
});
|
||||
|
|
@ -5970,7 +5927,7 @@ impl GitPanel {
|
|||
)
|
||||
.separator()
|
||||
.action("Open Diff", menu::Confirm.boxed_clone())
|
||||
.action("Open File", menu::SecondaryConfirm.boxed_clone())
|
||||
.action("Open Diff (File)", menu::SecondaryConfirm.boxed_clone())
|
||||
.when(!is_created, |context_menu| {
|
||||
context_menu
|
||||
.separator()
|
||||
|
|
@ -6249,7 +6206,7 @@ impl GitPanel {
|
|||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.click_count() > 1 || event.modifiers().secondary() {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
this.open_solo_diff(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
this.focus_handle.focus(window, cx);
|
||||
|
|
@ -6699,7 +6656,7 @@ impl Render for GitPanel {
|
|||
.on_action(cx.listener(Self::last_entry))
|
||||
.on_action(cx.listener(Self::close_panel))
|
||||
.on_action(cx.listener(Self::open_diff))
|
||||
.on_action(cx.listener(Self::open_file))
|
||||
.on_action(cx.listener(Self::open_solo_diff))
|
||||
.on_action(cx.listener(Self::focus_changes_list))
|
||||
.on_action(cx.listener(Self::focus_editor))
|
||||
.on_action(cx.listener(Self::expand_commit_editor))
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ pub mod picker_prompt;
|
|||
pub mod project_diff;
|
||||
pub(crate) mod remote_output;
|
||||
pub mod repository_selector;
|
||||
pub mod solo_diff_view;
|
||||
pub mod stash_picker;
|
||||
pub mod text_diff_view;
|
||||
pub mod worktree_names;
|
||||
|
|
|
|||
787
crates/git_ui/src/solo_diff_view.rs
Normal file
787
crates/git_ui/src/solo_diff_view.rs
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
use crate::{git_panel::GitStatusEntry, git_status_icon};
|
||||
use anyhow::{Context as _, Result};
|
||||
use buffer_diff::DiffHunkSecondaryStatus;
|
||||
use editor::{
|
||||
Direction, Editor, EditorEvent, EditorSettings, SplittableEditor, ToggleSplitDiff,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
};
|
||||
use fs::Fs;
|
||||
use git::{
|
||||
Commit, Restore, StageAndNext, StageFile, ToggleStaged, UnstageAndNext, UnstageFile,
|
||||
repository::RepoPath, status::StageStatus,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, Render, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
use language::{Buffer, HighlightedText};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::{
|
||||
Project,
|
||||
git_store::{Repository, RepositoryId},
|
||||
};
|
||||
use settings::{DiffViewStyle, Settings, SettingsStore, update_settings_file};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{
|
||||
Color, DiffStat, Divider, Icon, IconButton, IconButtonShape, IconName, Label, LabelCommon as _,
|
||||
SharedString, Tooltip, prelude::*, vertical_divider,
|
||||
};
|
||||
use util::paths::{PathExt as _, PathStyle};
|
||||
use workspace::{
|
||||
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
item::{ItemEvent, SaveOptions, TabContentParams},
|
||||
notifications::NotifyTaskExt,
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct SoloDiffView {
|
||||
repository: Entity<Repository>,
|
||||
repository_id: RepositoryId,
|
||||
repo_path: RepoPath,
|
||||
buffer: Entity<Buffer>,
|
||||
editor: Entity<SplittableEditor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl SoloDiffView {
|
||||
pub fn open_or_focus(
|
||||
entry: GitStatusEntry,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let Some(workspace_entity) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let existing = workspace_entity
|
||||
.read(cx)
|
||||
.items_of_type::<SoloDiffView>(cx)
|
||||
.find(|item| item.read(cx).matches(&repository, &entry.repo_path, cx));
|
||||
if let Some(existing) = existing {
|
||||
workspace_entity.update(cx, |workspace, cx| {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
});
|
||||
existing.focus_handle(cx).focus(window, cx);
|
||||
return Task::ready(Ok(existing));
|
||||
}
|
||||
|
||||
let Some(project_path) = repository
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow::anyhow!(
|
||||
"could not resolve repository path {:?}",
|
||||
entry.repo_path
|
||||
)));
|
||||
};
|
||||
|
||||
let project = workspace_entity.read(cx).project().clone();
|
||||
let repo_path = entry.repo_path;
|
||||
window.spawn(cx, async move |cx| {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})
|
||||
.await?;
|
||||
let diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
workspace_entity.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.entity();
|
||||
let view = cx.new(|cx| {
|
||||
Self::new(
|
||||
project,
|
||||
repository,
|
||||
repo_path,
|
||||
buffer,
|
||||
diff,
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(Box::new(view.clone()), None, true, window, cx);
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
project: Entity<Project>,
|
||||
repository: Entity<Repository>,
|
||||
repo_path: RepoPath,
|
||||
buffer: Entity<Buffer>,
|
||||
diff: Entity<buffer_diff::BufferDiff>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let repository_id = repository.read(cx).id;
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
|
||||
multibuffer.add_diff(diff, cx);
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer
|
||||
});
|
||||
let editor = cx.new(|cx| {
|
||||
let editor = SplittableEditor::new(
|
||||
EditorSettings::get_global(cx).diff_view_style,
|
||||
multibuffer,
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.rhs_editor().update(cx, |editor, cx| {
|
||||
editor.set_should_serialize(false, cx);
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
language::Point::new(0, 0),
|
||||
Direction::Next,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor
|
||||
});
|
||||
|
||||
let mut previous_diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
||||
let diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
||||
if diff_view_style != previous_diff_view_style {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
if editor.diff_view_style() != diff_view_style {
|
||||
editor.toggle_split(&ToggleSplitDiff, window, cx);
|
||||
}
|
||||
});
|
||||
previous_diff_view_style = diff_view_style;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
repository,
|
||||
repository_id,
|
||||
repo_path,
|
||||
buffer,
|
||||
editor,
|
||||
workspace: workspace.downgrade(),
|
||||
_settings_subscription: settings_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(&self, repository: &Entity<Repository>, repo_path: &RepoPath, cx: &App) -> bool {
|
||||
self.repository_id == repository.read(cx).id && &self.repo_path == repo_path
|
||||
}
|
||||
|
||||
fn button_states(&self, cx: &App) -> SoloDiffButtonStates {
|
||||
let editor = self.editor.read(cx).rhs_editor().read(cx);
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let prev_next = snapshot.diff_hunks().nth(1).is_some();
|
||||
let mut selection = true;
|
||||
|
||||
let mut ranges = editor
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.collect::<Vec<_>>();
|
||||
if !ranges.iter().any(|range| range.start != range.end) {
|
||||
selection = false;
|
||||
let anchor = editor.selections.newest_anchor().head();
|
||||
if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor)
|
||||
&& let Some(range) = snapshot
|
||||
.anchor_in_buffer(excerpt_range.context.start)
|
||||
.zip(snapshot.anchor_in_buffer(excerpt_range.context.end))
|
||||
.map(|(start, end)| start..end)
|
||||
{
|
||||
ranges = vec![range];
|
||||
} else {
|
||||
ranges = Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
let mut stage = false;
|
||||
let mut unstage = false;
|
||||
for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) {
|
||||
match hunk.status.secondary {
|
||||
DiffHunkSecondaryStatus::HasSecondaryHunk
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
|
||||
stage = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => {
|
||||
stage = true;
|
||||
unstage = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||
unstage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stage_status = self
|
||||
.repository
|
||||
.read(cx)
|
||||
.status_for_path(&self.repo_path)
|
||||
.map(|entry| entry.status.staging())
|
||||
.unwrap_or(StageStatus::Unstaged);
|
||||
|
||||
SoloDiffButtonStates {
|
||||
stage,
|
||||
unstage,
|
||||
restore: stage || unstage,
|
||||
prev_next,
|
||||
selection,
|
||||
stage_file: stage_status.has_unstaged(),
|
||||
unstage_file: stage_status.has_staged(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) {
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
});
|
||||
}
|
||||
|
||||
fn change_file_stage(&self, stage: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let repository = self.repository.clone();
|
||||
let repo_path = self.repo_path.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let task = cx.spawn(async move |_, cx| {
|
||||
repository
|
||||
.update(cx, |repository, cx| {
|
||||
if stage {
|
||||
repository.stage_entries(vec![repo_path], cx)
|
||||
} else {
|
||||
repository.unstage_entries(vec![repo_path], cx)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
if stage {
|
||||
"failed to stage file"
|
||||
} else {
|
||||
"failed to unstage file"
|
||||
}
|
||||
})
|
||||
});
|
||||
task.detach_and_notify_err(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for SoloDiffView {}
|
||||
|
||||
impl Focusable for SoloDiffView {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for SoloDiffView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::Diff).color(Color::Muted))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| {
|
||||
Some(
|
||||
file.full_path(cx)
|
||||
.file_name()?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
self.repo_path
|
||||
.as_ref()
|
||||
.display(PathStyle::local())
|
||||
.into_owned()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
|
||||
Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.full_path(cx).compact().to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| {
|
||||
self.repo_path
|
||||
.as_ref()
|
||||
.display(PathStyle::local())
|
||||
.into_owned()
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Solo Diff View Opened")
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.deactivated(window, cx);
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
cx: &'a App,
|
||||
) -> Option<gpui::AnyEntity> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.clone().into())
|
||||
} else {
|
||||
self.editor.act_as_type(type_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &App,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.rhs_editor().update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Arc<dyn Any + Send>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.rhs_editor()
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, cx: &App) -> Option<(Vec<HighlightedText>, Option<gpui::Font>)> {
|
||||
self.editor.breadcrumbs(cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.rhs_editor().update(cx, |editor, cx| {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).rhs_editor().read(cx).can_save(cx)
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
options: SaveOptions,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(options, project, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SoloDiffView {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
self.editor.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SoloDiffStyleToolbar {
|
||||
solo_diff: Option<WeakEntity<SoloDiffView>>,
|
||||
}
|
||||
|
||||
pub struct SoloDiffGitToolbar {
|
||||
solo_diff: Option<WeakEntity<SoloDiffView>>,
|
||||
}
|
||||
|
||||
impl SoloDiffStyleToolbar {
|
||||
pub fn new(_: &mut Context<Self>) -> Self {
|
||||
Self { solo_diff: None }
|
||||
}
|
||||
|
||||
fn solo_diff(&self) -> Option<Entity<SoloDiffView>> {
|
||||
self.solo_diff.as_ref()?.upgrade()
|
||||
}
|
||||
|
||||
fn set_diff_view_style(
|
||||
&mut self,
|
||||
diff_view_style: DiffViewStyle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(solo_diff) = self.solo_diff() else {
|
||||
return;
|
||||
};
|
||||
let workspace = solo_diff.read(cx).workspace.clone();
|
||||
|
||||
update_settings_file(<dyn Fs>::global(cx), cx, move |settings, _| {
|
||||
settings.editor.diff_view_style = Some(diff_view_style);
|
||||
});
|
||||
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
let splittable_editors = {
|
||||
workspace
|
||||
.read(cx)
|
||||
.items(cx)
|
||||
.filter_map(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
|
||||
.filter_map(|item| item.downcast::<SplittableEditor>().ok())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for editor in splittable_editors {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if editor.diff_view_style() != diff_view_style {
|
||||
editor.toggle_split(&ToggleSplitDiff, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for SoloDiffStyleToolbar {}
|
||||
|
||||
impl ToolbarItemView for SoloDiffStyleToolbar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
self.solo_diff = active_pane_item
|
||||
.and_then(|item| item.act_as::<SoloDiffView>(cx))
|
||||
.map(|entity| entity.downgrade());
|
||||
if self.solo_diff.is_some() {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SoloDiffStyleToolbar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(solo_diff) = self.solo_diff() else {
|
||||
return div();
|
||||
};
|
||||
let editor_entity = solo_diff.read(cx).editor.clone();
|
||||
let editor = editor_entity.read(cx);
|
||||
let diff_view_style = editor.diff_view_style();
|
||||
let is_split_set = diff_view_style == DiffViewStyle::Split;
|
||||
let split_icon = if is_split_set && !editor.is_split() {
|
||||
IconName::DiffSplitAuto
|
||||
} else {
|
||||
IconName::DiffSplit
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.h_8()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("solo-diff-unified", IconName::DiffUnified)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(diff_view_style == DiffViewStyle::Unified)
|
||||
.tooltip(Tooltip::text("Unified"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_diff_view_style(DiffViewStyle::Unified, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("solo-diff-split", split_icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(diff_view_style == DiffViewStyle::Split)
|
||||
.tooltip(Tooltip::text("Split"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_diff_view_style(DiffViewStyle::Split, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(vertical_divider())
|
||||
.child(div().w_1())
|
||||
}
|
||||
}
|
||||
|
||||
impl SoloDiffGitToolbar {
|
||||
pub fn new(_: &mut Context<Self>) -> Self {
|
||||
Self { solo_diff: None }
|
||||
}
|
||||
|
||||
fn solo_diff(&self) -> Option<Entity<SoloDiffView>> {
|
||||
self.solo_diff.as_ref()?.upgrade()
|
||||
}
|
||||
|
||||
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(solo_diff) = self.solo_diff() {
|
||||
solo_diff.update(cx, |solo_diff, cx| {
|
||||
solo_diff.dispatch_action(action, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(solo_diff) = self.solo_diff() {
|
||||
solo_diff.update(cx, |solo_diff, cx| {
|
||||
solo_diff.change_file_stage(true, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn unstage_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(solo_diff) = self.solo_diff() {
|
||||
solo_diff.update(cx, |solo_diff, cx| {
|
||||
solo_diff.change_file_stage(false, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for SoloDiffGitToolbar {}
|
||||
|
||||
impl ToolbarItemView for SoloDiffGitToolbar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
self.solo_diff = active_pane_item
|
||||
.and_then(|item| item.act_as::<SoloDiffView>(cx))
|
||||
.map(|entity| entity.downgrade());
|
||||
if self.solo_diff.is_some() {
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SoloDiffButtonStates {
|
||||
stage: bool,
|
||||
unstage: bool,
|
||||
restore: bool,
|
||||
prev_next: bool,
|
||||
selection: bool,
|
||||
stage_file: bool,
|
||||
unstage_file: bool,
|
||||
}
|
||||
|
||||
impl Render for SoloDiffGitToolbar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(solo_diff) = self.solo_diff() else {
|
||||
return div();
|
||||
};
|
||||
let focus_handle = solo_diff.focus_handle(cx);
|
||||
let solo_diff = solo_diff.read(cx);
|
||||
let button_states = solo_diff.button_states(cx);
|
||||
let status_entry = solo_diff
|
||||
.repository
|
||||
.read(cx)
|
||||
.status_for_path(&solo_diff.repo_path);
|
||||
let status = status_entry.as_ref().map(|entry| entry.status);
|
||||
let diff_stat = status_entry.and_then(|entry| entry.diff_stat);
|
||||
|
||||
h_group_xl()
|
||||
.my_neg_1()
|
||||
.py_1()
|
||||
.items_center()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.children(status.map(|status| git_status_icon(status).into_any_element()))
|
||||
.children(diff_stat.map(|stat| {
|
||||
DiffStat::new("solo-diff-stat", stat.added as usize, stat.deleted as usize)
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(
|
||||
h_group_sm()
|
||||
.when(button_states.selection, |el| {
|
||||
el.child(
|
||||
Button::new("stage", "Toggle Staged")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Toggle Staged",
|
||||
&ToggleStaged,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.stage && !button_states.unstage)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&ToggleStaged, window, cx)
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(!button_states.selection, |el| {
|
||||
el.child(
|
||||
Button::new("stage", "Stage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage and go to next hunk",
|
||||
&StageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.stage)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&StageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("unstage", "Unstage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage and go to next hunk",
|
||||
&UnstageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.unstage)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("restore", "Restore")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Restore selected hunk",
|
||||
&Restore,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.restore)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&Restore, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_group_sm()
|
||||
.child(
|
||||
IconButton::new("up", IconName::ArrowUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to previous hunk",
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&GoToPreviousHunk, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("down", IconName::ArrowDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to next hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&GoToHunk, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(vertical_divider())
|
||||
.child(
|
||||
h_group_sm()
|
||||
.child(
|
||||
Button::new("stage-file", "Stage File")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage file",
|
||||
&StageFile,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.stage_file)
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.stage_file(window, cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("unstage-file", "Unstage File")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage file",
|
||||
&UnstageFile,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.unstage_file)
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.unstage_file(window, cx)),
|
||||
),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
Button::new("commit", "Commit")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Commit",
|
||||
&Commit,
|
||||
&focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&Commit, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -336,6 +336,20 @@ impl TestAppContext {
|
|||
self.test_platform.simulate_new_path_selection(select_path);
|
||||
}
|
||||
|
||||
/// Simulates responding to a `prompt_for_paths` ("Open") dialog.
|
||||
pub fn simulate_path_prompt_response(
|
||||
&self,
|
||||
select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||
) {
|
||||
self.test_platform
|
||||
.simulate_path_prompt_response(select_paths);
|
||||
}
|
||||
|
||||
/// Returns true if there's a path selection dialog pending.
|
||||
pub fn did_prompt_for_paths(&self) -> bool {
|
||||
self.test_platform.did_prompt_for_paths()
|
||||
}
|
||||
|
||||
/// Simulates clicking a button in an platform-level alert dialog.
|
||||
#[track_caller]
|
||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||
|
|
@ -1098,3 +1112,54 @@ impl AnyWindowHandle {
|
|||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{PathPromptOptions, TestAppContext};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) {
|
||||
assert!(!cx.did_prompt_for_paths());
|
||||
|
||||
let receiver = cx.update(|cx| {
|
||||
cx.prompt_for_paths(PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
prompt: None,
|
||||
})
|
||||
});
|
||||
assert!(cx.did_prompt_for_paths());
|
||||
|
||||
let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")];
|
||||
cx.simulate_path_prompt_response({
|
||||
let selected = selected.clone();
|
||||
move |options| {
|
||||
assert!(options.multiple);
|
||||
Some(selected)
|
||||
}
|
||||
});
|
||||
assert!(!cx.did_prompt_for_paths());
|
||||
|
||||
let response = receiver.await.unwrap().unwrap();
|
||||
assert_eq!(response, Some(selected));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) {
|
||||
let receiver = cx.update(|cx| {
|
||||
cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
})
|
||||
});
|
||||
|
||||
cx.simulate_path_prompt_response(|_options| None);
|
||||
|
||||
let response = receiver.await.unwrap().unwrap();
|
||||
assert_eq!(response, None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata,
|
||||
Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams,
|
||||
size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
|
|
@ -85,6 +86,10 @@ struct TestPrompt {
|
|||
pub(crate) struct TestPrompts {
|
||||
multiple_choice: VecDeque<TestPrompt>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||
paths: VecDeque<(
|
||||
PathPromptOptions,
|
||||
oneshot::Sender<Result<Option<Vec<PathBuf>>>>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl TestPlatform {
|
||||
|
|
@ -147,6 +152,33 @@ impl TestPlatform {
|
|||
tx.send(Ok(select_path(&path))).ok();
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_path_prompt_response(
|
||||
&self,
|
||||
select_paths: impl FnOnce(&PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||
) {
|
||||
let (options, tx) = self
|
||||
.prompts
|
||||
.borrow_mut()
|
||||
.paths
|
||||
.pop_front()
|
||||
.expect("no pending paths prompt");
|
||||
let selection = select_paths(&options);
|
||||
if let Some(paths) = &selection
|
||||
&& !options.multiple
|
||||
&& paths.len() > 1
|
||||
{
|
||||
panic!(
|
||||
"selected {} paths for a prompt that does not allow multiple selection",
|
||||
paths.len()
|
||||
);
|
||||
}
|
||||
tx.send(Ok(selection)).ok();
|
||||
}
|
||||
|
||||
pub(crate) fn did_prompt_for_paths(&self) -> bool {
|
||||
!self.prompts.borrow().paths.is_empty()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
||||
let prompt = self
|
||||
|
|
@ -348,9 +380,11 @@ impl Platform for TestPlatform {
|
|||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: crate::PathPromptOptions,
|
||||
options: crate::PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||
unimplemented!()
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.prompts.borrow_mut().paths.push_back((options, tx));
|
||||
rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ use futures::{StreamExt, channel::mpsc, select_biased};
|
|||
use git_ui::commit_view::CommitViewToolbar;
|
||||
use git_ui::git_panel::GitPanel;
|
||||
use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar};
|
||||
use git_ui::solo_diff_view::{SoloDiffGitToolbar, SoloDiffStyleToolbar};
|
||||
use gpui::{
|
||||
Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
|
||||
Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
|
||||
|
|
@ -1305,6 +1306,8 @@ fn initialize_pane(
|
|||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let multibuffer_hint = cx.new(|_| MultibufferHint::new());
|
||||
toolbar.add_item(multibuffer_hint, window, cx);
|
||||
let solo_diff_style_toolbar = cx.new(SoloDiffStyleToolbar::new);
|
||||
toolbar.add_item(solo_diff_style_toolbar, window, cx);
|
||||
let breadcrumbs = cx.new(|_| Breadcrumbs::new());
|
||||
toolbar.add_item(breadcrumbs, window, cx);
|
||||
let buffer_search_bar = cx.new(|cx| {
|
||||
|
|
@ -1343,6 +1346,8 @@ fn initialize_pane(
|
|||
toolbar.add_item(project_diff_toolbar, window, cx);
|
||||
let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
|
||||
toolbar.add_item(branch_diff_toolbar, window, cx);
|
||||
let solo_diff_git_toolbar = cx.new(SoloDiffGitToolbar::new);
|
||||
toolbar.add_item(solo_diff_git_toolbar, window, cx);
|
||||
let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
|
||||
toolbar.add_item(commit_view_toolbar, window, cx);
|
||||
let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
|
|
@ -142,9 +142,9 @@ dependencies = [
|
|||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Reference in a new issue