review
This commit is contained in:
@@ -8,7 +8,6 @@ use signed_state::ProfileStore;
|
||||
use signed_ui::{UserAvatar, middle_truncate};
|
||||
|
||||
/// Open the About dialog showing every field of the announcement event.
|
||||
/// The event is NIP-34 kind 30617, parsed into [`Announcement`].
|
||||
pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) {
|
||||
window.open_dialog(cx, move |dialog, _window, cx| {
|
||||
let announcement = announcement.clone();
|
||||
@@ -23,7 +22,6 @@ pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window,
|
||||
}
|
||||
|
||||
/// The announcement's fields as labeled rows.
|
||||
/// Hex identifiers carry a copy button, multi-value tags one line per value.
|
||||
fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
|
||||
let mut rows: Vec<AnyElement> = Vec::new();
|
||||
|
||||
@@ -161,6 +159,7 @@ fn copy_value(id: &'static str, value: String, cx: &App) -> AnyElement {
|
||||
|
||||
/// One row per maintainer with avatar and display name.
|
||||
/// The display name falls back to a shortened npub.
|
||||
///
|
||||
/// A copy button copies the full pubkey.
|
||||
fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
|
||||
let profile_store = ProfileStore::global(cx);
|
||||
@@ -192,6 +191,7 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
|
||||
}
|
||||
|
||||
/// One row per item of a multi-value tag.
|
||||
///
|
||||
/// The value is truncated to a single line, with a copy button for the full value.
|
||||
fn list(id: &'static str, items: impl IntoIterator<Item = String>, cx: &App) -> AnyElement {
|
||||
v_flex()
|
||||
|
||||
@@ -17,6 +17,7 @@ const TREE_WIDTH: f32 = 240.;
|
||||
/// Files larger than this are not previewed.
|
||||
pub(super) const MAX_PREVIEW_BYTES: usize = 1024 * 1024;
|
||||
/// Preview cache caps, a file count and a text byte count.
|
||||
///
|
||||
/// The oldest previews are evicted beyond the caps.
|
||||
pub(super) const MAX_PREVIEWED_FILES: usize = 32;
|
||||
pub(super) const MAX_PREVIEW_CACHE_BYTES: usize = 8 * 1024 * 1024;
|
||||
@@ -34,9 +35,6 @@ pub(super) enum FileContent {
|
||||
}
|
||||
|
||||
/// A markdown document loaded into a persistent [`TextViewState`].
|
||||
/// The state lives in the view rather than being created per render.
|
||||
/// GPUI drops keyed element state after one absent frame.
|
||||
/// A per-render state would re-parse the whole document on every pane switch.
|
||||
pub(super) struct MarkdownView {
|
||||
/// Source path, `None` means the repository README.
|
||||
pub(super) path: Option<SharedString>,
|
||||
@@ -44,9 +42,6 @@ pub(super) struct MarkdownView {
|
||||
}
|
||||
|
||||
/// A code file loaded into a persistent [`InputState`].
|
||||
/// It renders as a disabled, read-only code editor.
|
||||
/// Syntax highlighting, line numbers and search are included.
|
||||
/// Persistent for the same reason as [`MarkdownView`].
|
||||
pub(super) struct CodeView {
|
||||
/// Source path, relative to the worktree root.
|
||||
pub(super) path: SharedString,
|
||||
@@ -216,8 +211,6 @@ impl RepoDetailView {
|
||||
}
|
||||
|
||||
/// Load `text` into the persistent markdown TextView state.
|
||||
/// The state is created empty and fed via `push_str`, which parses on a background task.
|
||||
/// Switching files never blocks the main thread.
|
||||
pub(super) fn set_markdown(
|
||||
&mut self,
|
||||
path: Option<SharedString>,
|
||||
@@ -230,15 +223,18 @@ impl RepoDetailView {
|
||||
}
|
||||
|
||||
/// The persistent markdown TextView for `path`, where `None` is the README.
|
||||
///
|
||||
/// Shows a spinner while the document is being loaded or parsed.
|
||||
fn markdown_element(&self, path: Option<&str>, _cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(md) = &self.md else {
|
||||
return preview_spinner();
|
||||
};
|
||||
|
||||
let ready = match path {
|
||||
Some(path) => md.path.as_deref() == Some(path),
|
||||
None => md.path.is_none(),
|
||||
};
|
||||
|
||||
if !ready {
|
||||
return preview_spinner();
|
||||
}
|
||||
@@ -252,8 +248,8 @@ impl RepoDetailView {
|
||||
}
|
||||
|
||||
/// Load `text` into the persistent code editor state for `path`.
|
||||
///
|
||||
/// Code editor mode makes the Input render it read-only and highlighted.
|
||||
/// The tree-sitter parse runs on a background task like [`set_markdown`]'s.
|
||||
pub(super) fn set_code(
|
||||
&mut self,
|
||||
path: SharedString,
|
||||
|
||||
@@ -70,8 +70,6 @@ pub(super) fn commit_row(
|
||||
}
|
||||
|
||||
impl RepoDetailView {
|
||||
/// Full-height body of the Commits tab.
|
||||
/// All commits in a virtual list, or a status message while loading or empty.
|
||||
pub(super) fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(list) = self.all_commits.as_ref() else {
|
||||
return if self.loading_all_commits {
|
||||
|
||||
@@ -29,8 +29,6 @@ use super::helpers::{
|
||||
const TREE_WIDTH: f32 = 260.;
|
||||
|
||||
/// Tree and per-file diff body, shared by the commit diff and compare views.
|
||||
/// Owns the changed-files explorer and the virtual list of the selected file's hunks.
|
||||
/// The host feeds it a [`CommitDiff`] via [`DiffPane::set_diff`].
|
||||
pub struct DiffPane {
|
||||
/// Loaded diff, `None` until [`Self::set_diff`] is called.
|
||||
diff: Option<CommitDiff>,
|
||||
@@ -39,7 +37,6 @@ pub struct DiffPane {
|
||||
/// Path of the file whose diff is shown in the detail column.
|
||||
selected_file: Option<SharedString>,
|
||||
/// Rows of the selected file's diff, hunk headers and lines.
|
||||
/// Backing the virtual list in the detail column.
|
||||
rows: Vec<DiffRow>,
|
||||
/// Per-row heights of [`Self::rows`].
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
@@ -90,6 +87,7 @@ impl DiffPane {
|
||||
}
|
||||
|
||||
/// Forget the diff, e.g. when the compared branches changed.
|
||||
///
|
||||
/// Clears the tree, the selection and the diff rows.
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
self.diff = None;
|
||||
@@ -187,8 +185,6 @@ impl DiffPane {
|
||||
}
|
||||
|
||||
/// The diff of one file, with a header showing status and stats.
|
||||
/// The hunks render in a virtual list.
|
||||
/// A large diff is never materialized per frame.
|
||||
fn render_file_diff(&self, file: &FileDiff, view: Entity<Self>, cx: &App) -> AnyElement {
|
||||
let status_label = match file.status {
|
||||
DiffStatus::Added => "A",
|
||||
@@ -316,7 +312,6 @@ impl Render for DiffPane {
|
||||
}
|
||||
|
||||
/// Detail panel showing the diff of one commit.
|
||||
/// A metadata header plus the shared [`DiffPane`] body.
|
||||
pub struct CommitDiffView {
|
||||
focus_handle: FocusHandle,
|
||||
/// Local clone the commit lives in.
|
||||
@@ -324,8 +319,6 @@ pub struct CommitDiffView {
|
||||
/// Display name of the repository the commit belongs to.
|
||||
repo_name: SharedString,
|
||||
/// The commit being shown in the header and tab title.
|
||||
/// Starts as an id-only stub, the history list omits the full metadata.
|
||||
/// [`Self::load`] replaces the stub with the full metadata.
|
||||
commit: FileCommit,
|
||||
/// The diff is being computed on a background task.
|
||||
loading: bool,
|
||||
@@ -369,8 +362,7 @@ impl CommitDiffView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the commit diff and the full commit metadata on a background task.
|
||||
/// Then populate the tree.
|
||||
/// Load the commit diff and the full commit metadata.
|
||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.loading = true;
|
||||
self.error = None;
|
||||
|
||||
@@ -11,9 +11,6 @@ use signed_core::Announcement;
|
||||
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff};
|
||||
use signed_ui::{menu_copy_row, middle_truncate};
|
||||
|
||||
/// A `Send` file-tree node, the build runs on a background thread.
|
||||
/// The main thread converts the seeds into [`TreeItem`]s.
|
||||
/// [`TreeItem`]s hold `Rc` state and cannot cross threads.
|
||||
pub(super) struct TreeItemSeed {
|
||||
/// Path of the node, relative to the worktree root.
|
||||
id: String,
|
||||
@@ -22,10 +19,6 @@ pub(super) struct TreeItemSeed {
|
||||
children: Vec<TreeItemSeed>,
|
||||
}
|
||||
|
||||
/// Convert tree seeds into [`TreeItem`]s.
|
||||
/// Every folder is expanded when `expand_folders` is set.
|
||||
/// The commit diff explorer shows only changed files, typically a handful.
|
||||
/// Its folders start expanded, the worktree explorer's folders collapsed.
|
||||
pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<TreeItem> {
|
||||
fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem {
|
||||
let mut item = TreeItem::new(seed.id, seed.label);
|
||||
@@ -47,10 +40,6 @@ pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<
|
||||
}
|
||||
|
||||
/// Build nested tree items from a flat entry list sorted dirs-first.
|
||||
/// Returns [`TreeItemSeed`]s so the build can run off the main thread.
|
||||
/// A worktree walk can yield tens of thousands of entries.
|
||||
/// Nodes live in an arena, parents are found via a path-to-index map.
|
||||
/// That keeps the build linear in the number of path components.
|
||||
pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec<TreeItemSeed> {
|
||||
// Node indices by full path, so parents resolve in constant time while inserting.
|
||||
let mut index: HashMap<String, usize> = HashMap::new();
|
||||
@@ -96,8 +85,6 @@ pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec<TreeItemSeed> {
|
||||
}
|
||||
|
||||
/// The markdown fence language for a file path, or `None` for plain text.
|
||||
/// Names resolve in `gpui_component`'s highlighter.
|
||||
/// `highlighter::Language::from_name` accepts short aliases like `rs` and `js`.
|
||||
pub(super) fn code_language(path: &str) -> Option<&'static str> {
|
||||
let name = Path::new(path)
|
||||
.file_name()
|
||||
@@ -192,6 +179,7 @@ impl ShareTargets {
|
||||
}
|
||||
|
||||
/// The share dropdown menu, one row per target.
|
||||
///
|
||||
/// Each shows a compact label, the copy button and row click copy the full value.
|
||||
pub(super) fn menu(&self, menu: PopupMenu) -> PopupMenu {
|
||||
menu.min_w(px(340.))
|
||||
@@ -223,8 +211,6 @@ impl ShareTargets {
|
||||
}
|
||||
|
||||
/// Shorten an naddr link to `<url>/naddr1...[last tail chars]`.
|
||||
/// `https://gitworkshop.dev/naddr1...abcd` is an example.
|
||||
/// Only the label is shortened, the copied value stays the full URL.
|
||||
fn truncate_naddr_link(url: &str, tail: usize) -> String {
|
||||
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
||||
return url.to_string();
|
||||
@@ -241,6 +227,7 @@ pub(super) const GUTTER_WIDTH: f32 = 44.;
|
||||
pub(super) const DIFF_ROW_HEIGHT: f32 = 20.;
|
||||
|
||||
/// One row of a virtual diff list, a hunk header or a line of a hunk.
|
||||
///
|
||||
/// Shared by the commit diff and pull request diff viewers.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum DiffRow {
|
||||
@@ -300,6 +287,7 @@ pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> Any
|
||||
}
|
||||
|
||||
/// One diff line, old and new line numbers in the gutters.
|
||||
///
|
||||
/// The content is tinted by kind, addition, deletion or context.
|
||||
pub(super) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
|
||||
let bg = match line.kind {
|
||||
|
||||
@@ -26,9 +26,6 @@ pub struct InitRepoState {
|
||||
}
|
||||
|
||||
/// Open the Init dialog for the local repository at `local_path`.
|
||||
/// The dialog loads the user's default grasp servers, a kind `10317` grasp list.
|
||||
/// It falls back to the shared defaults when the user has none set.
|
||||
/// On success the dialog closes and `view` switches into NIP-34 mode.
|
||||
pub fn open(
|
||||
local_path: PathBuf,
|
||||
view: WeakEntity<RepoDetailView>,
|
||||
@@ -39,23 +36,25 @@ pub fn open(
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let grasp_settings = SettingsStore::global(cx)
|
||||
.read(cx)
|
||||
.settings()
|
||||
.grasp_servers
|
||||
.clone();
|
||||
|
||||
let state = cx.new(|_| InitRepoState::default());
|
||||
let grasp_state = cx.new(|_| GraspServersState::new_default(&grasp_settings));
|
||||
|
||||
let relay_input = cx.new(|cx| InputState::new(window, cx).placeholder("relay.example.com"));
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).default_value(default_name));
|
||||
let desc_input = cx.new(|cx| {
|
||||
TextareaState::new(window, cx)
|
||||
.auto_grow(3, 5)
|
||||
.placeholder("Short description")
|
||||
});
|
||||
let relay_input = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder("wss://relay.example.com or relay.example.com")
|
||||
});
|
||||
let state = cx.new(|_| InitRepoState::default());
|
||||
let grasp_settings = SettingsStore::global(cx)
|
||||
.read(cx)
|
||||
.settings()
|
||||
.grasp_servers
|
||||
.clone();
|
||||
let grasp_state = cx.new(|_| GraspServersState::new_default(&grasp_settings));
|
||||
|
||||
// Load the user's grasp servers.
|
||||
load_user_grasp_servers(grasp_state.clone(), window, cx);
|
||||
|
||||
window.open_dialog(cx, move |dialog, _window, _cx| {
|
||||
@@ -153,6 +152,7 @@ pub fn open(
|
||||
}
|
||||
|
||||
/// Run the init flow.
|
||||
///
|
||||
/// Closes the dialog and switches the repository into NIP-34 mode on success.
|
||||
fn init_repository(
|
||||
local_path: PathBuf,
|
||||
|
||||
@@ -25,8 +25,6 @@ use utils::relative_time;
|
||||
use super::issue_detail::IssueDetailView;
|
||||
|
||||
/// Height of one issue row in the virtual list.
|
||||
/// `py_2` padding, a 32px `h_8` title line and a 24px `h_6` meta line.
|
||||
/// Plus the 1px bottom border.
|
||||
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
||||
|
||||
/// Status filter of the issues list, chosen via the header's filter buttons.
|
||||
@@ -37,7 +35,6 @@ enum IssueFilter {
|
||||
/// Issues whose resolved status is [`RepoStatus::Open`].
|
||||
Open,
|
||||
/// Issues whose resolved status is [`RepoStatus::Closed`].
|
||||
/// [`RepoStatus::Applied`] counts too, both are done states.
|
||||
Closed,
|
||||
}
|
||||
|
||||
@@ -67,9 +64,6 @@ pub struct IssuesView {
|
||||
/// The filtered issue count [`Self::item_sizes`] was built for.
|
||||
issue_len: usize,
|
||||
/// Indices into the store's `issues` matching [`Self::filter`].
|
||||
/// The virtual list renders this slice.
|
||||
/// Rebuilt only when the store version or the filter changes.
|
||||
/// Keyed by [`Self::cache_key`].
|
||||
visible_issues: Vec<usize>,
|
||||
/// Header counts `(total, open, closed)`, rebuilt with [`Self::visible_issues`].
|
||||
counts: (usize, usize, usize),
|
||||
@@ -121,8 +115,6 @@ impl IssuesView {
|
||||
});
|
||||
}
|
||||
|
||||
/// Render one row of the issue list.
|
||||
/// `ix` is the row index, `issue_ix` the index in the store's `issues`.
|
||||
fn render_row(&self, ix: usize, issue_ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let issue = &self.store.read(cx).issues[issue_ix];
|
||||
let title = activity_subject(issue);
|
||||
@@ -186,7 +178,6 @@ impl IssuesView {
|
||||
|
||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
// Counts of the last list rebuild.
|
||||
// `render` rebuilds first when the store version or filter changed, so never stale.
|
||||
let (total, open, closed) = self.counts;
|
||||
|
||||
h_flex()
|
||||
@@ -245,7 +236,6 @@ impl IssuesView {
|
||||
}
|
||||
|
||||
/// Open the new issue dialog, a title and a content input.
|
||||
/// Confirming submits through [`RepoStore::open_issue`].
|
||||
pub(super) fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title"));
|
||||
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue..."));
|
||||
@@ -335,8 +325,8 @@ impl Render for IssuesView {
|
||||
let filter = self.filter;
|
||||
|
||||
// Rows and counts are rebuilt only when the store refreshed or filter changed.
|
||||
// Other renders reuse the cache.
|
||||
let version = self.store.read(cx).version();
|
||||
|
||||
if self.cache_key != Some((version, filter)) {
|
||||
let store = self.store.read(cx);
|
||||
let mut counts = (0usize, 0usize, 0usize);
|
||||
|
||||
@@ -2419,7 +2419,7 @@ fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedSt
|
||||
let owner = announcement.owner;
|
||||
let user = nip05
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_hex()));
|
||||
.unwrap_or_else(|| owner.to_bech32().unwrap());
|
||||
|
||||
let mut url = format!("nostr://{user}");
|
||||
if let Some(hint) = announcement.relays.first().and_then(RelayUrl::domain) {
|
||||
|
||||
@@ -14,7 +14,7 @@ use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::combobox::{
|
||||
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
|
||||
};
|
||||
use gpui_component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||
use gpui_component::input::{Input, InputState, Textarea, TextareaState};
|
||||
use gpui_component::menu::{DropdownMenu, PopupMenu, PopupMenuItem};
|
||||
use gpui_component::scroll::Scrollbar;
|
||||
use gpui_component::searchable_list::SearchableVec;
|
||||
@@ -36,14 +36,6 @@ use super::commits::{COMMIT_ROW_HEIGHT, commit_row};
|
||||
use super::diff::{CommitDiffView, DiffPane};
|
||||
|
||||
/// The new pull request panel of a repository.
|
||||
/// The compare side comes from a local checkout or an announced fork.
|
||||
/// A local checkout lists its own branches in both selectors.
|
||||
/// Git ops and the tip push run in the checkout.
|
||||
/// An announced fork imports its branches into the target's GitCache mirror.
|
||||
/// The base selector lists the mirror's `refs/remotes/origin/*` branches.
|
||||
/// All git ops run in the mirror.
|
||||
/// The Files and Commits tabs are built from `merge-base..compare` of the chosen refs.
|
||||
/// The patch series published with the PR comes from the same range at submit time.
|
||||
pub struct NewPullRequestView {
|
||||
focus_handle: FocusHandle,
|
||||
/// Dock area the panel lives in, commit diffs are opened there.
|
||||
@@ -53,14 +45,10 @@ pub struct NewPullRequestView {
|
||||
/// Display name of the repository, for the panel title.
|
||||
repo_name: SharedString,
|
||||
/// The user's local checkout.
|
||||
/// Both branches live there in checkout mode and the tip is pushed from there.
|
||||
/// `None` until a folder is picked.
|
||||
repo_path: Option<PathBuf>,
|
||||
/// Branches of the checkout, backing both selectors in checkout mode.
|
||||
branches: Vec<SharedString>,
|
||||
/// Fork-backed compare state.
|
||||
/// `Some` switches the panel into fork mode.
|
||||
/// The checkout above is kept so the user can switch back.
|
||||
fork: Option<ForkCompare>,
|
||||
/// Selected base branch, the PR target, stored as a short name.
|
||||
base: SharedString,
|
||||
@@ -96,8 +84,6 @@ pub struct NewPullRequestView {
|
||||
}
|
||||
|
||||
/// A fork-backed compare.
|
||||
/// The fork's heads are imported into the target mirror under `refs/fork/<namespace>/*`.
|
||||
/// The mirror's own `refs/remotes/origin/*` refs track the base branches.
|
||||
struct ForkCompare {
|
||||
/// Fork announcement the compare branch is imported from.
|
||||
announcement: Announcement,
|
||||
@@ -129,10 +115,6 @@ fn fork_namespace(announcement: &Announcement) -> String {
|
||||
}
|
||||
|
||||
/// The announced forks of `base` a New PR compare can be built from.
|
||||
/// Related by `u` tag or shared EUC, excluding the base itself.
|
||||
/// Announcements without `clone` URLs are unfetchable and excluded.
|
||||
/// Own forks, announced by `user`, come first.
|
||||
/// Newest first as `RepoListStore` keeps them, order is preserved within each group.
|
||||
fn fork_candidates<'a>(
|
||||
announcements: &'a [Announcement],
|
||||
base: &RepoAddr,
|
||||
@@ -154,6 +136,7 @@ fn fork_candidates<'a>(
|
||||
}
|
||||
|
||||
/// The display name of an announcement.
|
||||
///
|
||||
/// Its human-readable name, falling back to the repository id.
|
||||
fn fork_display_name(announcement: &Announcement) -> SharedString {
|
||||
announcement
|
||||
@@ -182,6 +165,7 @@ fn truncate_label(label: &str) -> SharedString {
|
||||
}
|
||||
|
||||
/// The compare-source menu entry of one local checkout folder.
|
||||
///
|
||||
/// Applies the folder directly, no picker.
|
||||
fn checkout_source_item(
|
||||
view: WeakEntity<NewPullRequestView>,
|
||||
@@ -229,6 +213,7 @@ fn choose_folder_source_item(view: WeakEntity<NewPullRequestView>) -> PopupMenuI
|
||||
}
|
||||
|
||||
/// The compare-source menu entry of one announced fork.
|
||||
///
|
||||
/// Imports its branches into the target's mirror and switches the panel to fork mode.
|
||||
fn fork_source_item(
|
||||
view: WeakEntity<NewPullRequestView>,
|
||||
@@ -310,7 +295,7 @@ impl NewPullRequestView {
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
||||
let description = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe..."));
|
||||
|
||||
let base_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| {
|
||||
let base_select = cx.new(|cx| {
|
||||
ComboboxState::new(
|
||||
SearchableVec::new(Vec::<SharedString>::new()),
|
||||
Vec::new(),
|
||||
@@ -320,7 +305,7 @@ impl NewPullRequestView {
|
||||
.searchable(true)
|
||||
});
|
||||
|
||||
let compare_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| {
|
||||
let compare_select = cx.new(|cx| {
|
||||
ComboboxState::new(
|
||||
SearchableVec::new(Vec::<SharedString>::new()),
|
||||
Vec::new(),
|
||||
@@ -331,10 +316,6 @@ impl NewPullRequestView {
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
// Re-evaluate the Create button's enabled state as the title changes.
|
||||
cx.subscribe(&subject, |_this, _state, _event: &InputEvent, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
cx.subscribe_in(&base_select, window, |this, _state, event, window, cx| {
|
||||
if let ComboboxEvent::Change(values) = event
|
||||
&& let Some(name) = values.first()
|
||||
@@ -405,6 +386,7 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// The path git ops run against.
|
||||
///
|
||||
/// The target's mirror in fork mode, the user's checkout otherwise.
|
||||
fn work_path(&self) -> Option<PathBuf> {
|
||||
match &self.fork {
|
||||
@@ -415,6 +397,7 @@ impl NewPullRequestView {
|
||||
|
||||
/// The full ref the selected base branch resolves to.
|
||||
/// The mirror's remote-tracking ref in fork mode.
|
||||
///
|
||||
/// The plain branch name in checkout mode, git resolves it through `refs/heads`.
|
||||
fn base_ref(&self) -> String {
|
||||
match &self.fork {
|
||||
@@ -425,6 +408,7 @@ impl NewPullRequestView {
|
||||
|
||||
/// The full ref the selected compare branch resolves to.
|
||||
/// The imported `refs/fork/<namespace>` ref in fork mode.
|
||||
///
|
||||
/// The plain branch name in checkout mode.
|
||||
fn compare_ref(&self) -> String {
|
||||
match &self.fork {
|
||||
@@ -434,9 +418,6 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Prompt for a local checkout.
|
||||
/// On success populate the branch selectors and load the compare.
|
||||
/// Defaults are the announced HEAD branch for the base.
|
||||
/// The checkout's current branch is the default for the compare.
|
||||
fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: false,
|
||||
@@ -452,6 +433,7 @@ impl NewPullRequestView {
|
||||
Ok(Ok(Some(mut paths))) => paths.pop(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let Some(path) = picked else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -466,6 +448,7 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Apply `path` as the local checkout, no picker.
|
||||
///
|
||||
/// Branches and current branch are read off the UI thread, then applied.
|
||||
fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
@@ -495,9 +478,6 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Apply a picked checkout, filling the selectors and loading the compare.
|
||||
/// Leaves fork mode.
|
||||
/// A fork applied earlier keeps its import in the mirror, harmless.
|
||||
/// The panel switches back to the checkout.
|
||||
fn apply_checkout(
|
||||
&mut self,
|
||||
path: String,
|
||||
@@ -506,6 +486,7 @@ impl NewPullRequestView {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.fork = None;
|
||||
|
||||
let Some((branches, current)) = info else {
|
||||
self.error = Some("The chosen folder is not a git repository".into());
|
||||
self.repo_path = None;
|
||||
@@ -516,6 +497,7 @@ impl NewPullRequestView {
|
||||
cx.notify();
|
||||
return;
|
||||
};
|
||||
|
||||
if branches.is_empty() {
|
||||
self.error = Some("The repository has no branches yet".into());
|
||||
self.repo_path = None;
|
||||
@@ -528,12 +510,14 @@ impl NewPullRequestView {
|
||||
// Falling back to `main`, then the first branch.
|
||||
// The checkout's current branch is the compare side default.
|
||||
let announced = self.store.read(cx).head.clone();
|
||||
|
||||
let base = announced
|
||||
.as_ref()
|
||||
.filter(|branch| branches.contains(branch))
|
||||
.cloned()
|
||||
.or_else(|| branches.iter().find(|branch| *branch == "main").cloned())
|
||||
.unwrap_or_else(|| branches[0].clone());
|
||||
|
||||
let compare = current
|
||||
.filter(|branch| branches.contains(branch))
|
||||
.unwrap_or_else(|| base.clone());
|
||||
@@ -545,19 +529,23 @@ impl NewPullRequestView {
|
||||
// Remember this folder as a checkout of the target repository.
|
||||
// The next panel pre-fills it.
|
||||
let addr = self.store.read(cx).addr().clone();
|
||||
CheckoutsStore::global(cx).update(cx, |store, cx| {
|
||||
let checkout_store = CheckoutsStore::global(cx);
|
||||
checkout_store.update(cx, |store, cx| {
|
||||
store.record(PathBuf::from(&path), addr, cx);
|
||||
});
|
||||
|
||||
let branches = self.branches.clone();
|
||||
let base = SharedString::from(base.clone());
|
||||
let compare = SharedString::from(compare.clone());
|
||||
|
||||
self.base = base.clone();
|
||||
self.compare = compare.clone();
|
||||
|
||||
self.base_select.update(cx, |state, cx| {
|
||||
state.set_items(SearchableVec::from(branches.clone()), window, cx);
|
||||
state.set_selected_values(&[base], window, cx);
|
||||
});
|
||||
|
||||
self.compare_select.update(cx, |state, cx| {
|
||||
state.set_items(SearchableVec::from(branches), window, cx);
|
||||
state.set_selected_values(&[compare], window, cx);
|
||||
@@ -567,6 +555,7 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// The base repository of the panel, its address and announced EUC.
|
||||
///
|
||||
/// Used to find fork candidates.
|
||||
fn base_repo(&self, cx: &App) -> (RepoAddr, Option<String>) {
|
||||
let store = self.store.read(cx);
|
||||
@@ -575,6 +564,7 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Announced forks of the target repository a compare can use, own first.
|
||||
///
|
||||
/// Re-read whenever the picker opens.
|
||||
fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
|
||||
let (base, euc) = self.base_repo(cx);
|
||||
@@ -587,11 +577,6 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Compare against an announced fork.
|
||||
/// The target's GitCache mirror is ensured, then the fork's heads land under `refs/fork/…`.
|
||||
/// The base selector lists `refs/remotes/origin/*`, the compare the import.
|
||||
/// Then the compare loads.
|
||||
/// Picking the fork already applied refreshes it, re-import and reload.
|
||||
/// The branch selection is kept.
|
||||
fn choose_fork(
|
||||
&mut self,
|
||||
announcement: Announcement,
|
||||
@@ -728,6 +713,7 @@ impl NewPullRequestView {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.loading = false;
|
||||
|
||||
let (base_branches, compare_branches) = match result {
|
||||
Ok(branches) => branches,
|
||||
Err(error) => {
|
||||
@@ -738,11 +724,13 @@ impl NewPullRequestView {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if compare_branches.is_empty() {
|
||||
self.error = Some("The fork has no branches to compare".into());
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
if base_branches.is_empty() {
|
||||
self.error =
|
||||
Some("Could not list the target repository's branches; try again later".into());
|
||||
@@ -752,6 +740,7 @@ impl NewPullRequestView {
|
||||
|
||||
let base_branches: Vec<SharedString> =
|
||||
base_branches.into_iter().map(SharedString::from).collect();
|
||||
|
||||
let compare_branches: Vec<SharedString> = compare_branches
|
||||
.into_iter()
|
||||
.map(SharedString::from)
|
||||
@@ -764,8 +753,10 @@ impl NewPullRequestView {
|
||||
let announced = self.store.read(cx).head.clone();
|
||||
let contains =
|
||||
|name: &str, list: &[SharedString]| list.iter().any(|branch| branch.as_ref() == name);
|
||||
|
||||
let keep_base = keep_base.filter(|name| contains(name, &base_branches));
|
||||
let keep_compare = keep_compare.filter(|name| contains(name, &compare_branches));
|
||||
|
||||
let base = keep_base
|
||||
.or_else(|| {
|
||||
announced
|
||||
@@ -780,6 +771,7 @@ impl NewPullRequestView {
|
||||
.cloned()
|
||||
})
|
||||
.unwrap_or_else(|| base_branches[0].clone());
|
||||
|
||||
let compare = keep_compare
|
||||
.or_else(|| {
|
||||
compare_branches
|
||||
@@ -794,13 +786,16 @@ impl NewPullRequestView {
|
||||
namespace,
|
||||
mirror_path,
|
||||
});
|
||||
|
||||
self.error = None;
|
||||
self.base = base.clone();
|
||||
self.compare = compare.clone();
|
||||
|
||||
self.base_select.update(cx, |state, cx| {
|
||||
state.set_items(SearchableVec::from(base_branches), window, cx);
|
||||
state.set_selected_values(&[base], window, cx);
|
||||
});
|
||||
|
||||
self.compare_select.update(cx, |state, cx| {
|
||||
state.set_items(SearchableVec::from(compare_branches), window, cx);
|
||||
state.set_selected_values(&[compare], window, cx);
|
||||
@@ -810,15 +805,14 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Recompute `merge_base..compare` of the selected branches on a background task.
|
||||
/// Computes the merge base, the commit list and the diff.
|
||||
/// Runs against the work path, the checkout or the mirror in fork mode.
|
||||
/// Full refs keep base `main` and fork `main` distinct.
|
||||
fn reload_compare(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo_path) = self.work_path() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let base = self.base_ref();
|
||||
let compare = self.compare_ref();
|
||||
|
||||
// Short names for the error copy, the full refs go to git.
|
||||
let base_name = self.base.to_string();
|
||||
let compare_name = self.compare.to_string();
|
||||
@@ -826,6 +820,7 @@ impl NewPullRequestView {
|
||||
self.loading = true;
|
||||
self.error = None;
|
||||
self.compare_generation += 1;
|
||||
|
||||
let generation = self.compare_generation;
|
||||
cx.notify();
|
||||
|
||||
@@ -871,11 +866,11 @@ impl NewPullRequestView {
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
// A stale result, branches changed mid-flight, must not clobber a newer compare.
|
||||
// The newer task clears the flag.
|
||||
if generation != this.compare_generation {
|
||||
return;
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
match result {
|
||||
Ok((merge_base, commits, diff)) => {
|
||||
this.merge_base = Some(merge_base);
|
||||
@@ -891,18 +886,17 @@ impl NewPullRequestView {
|
||||
this.error = Some(error.to_string().into());
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
}
|
||||
|
||||
/// Publish the pull request.
|
||||
/// Generate the patch series on a background task and hand it to the store.
|
||||
/// Close the panel once the publish is underway.
|
||||
/// Errors surface in the pull request list.
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.submitting || self.loading {
|
||||
return;
|
||||
@@ -969,6 +963,7 @@ impl NewPullRequestView {
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.submitting = false;
|
||||
|
||||
store.update(cx, |store, cx| {
|
||||
store.open_pull_request(
|
||||
(!subject.is_empty()).then_some(subject),
|
||||
@@ -981,6 +976,7 @@ impl NewPullRequestView {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Close the panel once the publish is underway.
|
||||
cx.defer_in(window, {
|
||||
let dock_area = dock_area.clone();
|
||||
@@ -993,6 +989,7 @@ impl NewPullRequestView {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
@@ -1026,8 +1023,6 @@ impl NewPullRequestView {
|
||||
});
|
||||
}
|
||||
|
||||
/// The compare bar, base and compare selectors.
|
||||
/// Plus the source picker, local checkout or announced fork, and the Create button.
|
||||
fn render_compare_bar(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let has_source = self.has_source();
|
||||
let can_submit = has_source
|
||||
@@ -1174,22 +1169,18 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
/// Build the compare-source menu.
|
||||
/// The local checkout entries first, then the announced forks, own forks first.
|
||||
/// Picking the fork already applied re-fetches it.
|
||||
/// Rebuilt every time the menu opens, so the candidates stay current.
|
||||
fn source_menu(
|
||||
&self,
|
||||
cx: &Context<Self>,
|
||||
) -> impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static {
|
||||
let view = cx.entity().downgrade();
|
||||
// Associated local checkouts of the target repository, freshest first.
|
||||
// The applied one is checked.
|
||||
// The picker prompt stays available underneath for arbitrary folders.
|
||||
let addr = self.store.read(cx).addr().clone();
|
||||
let associated = CheckoutsStore::global(cx).read(cx).associations_of(&addr);
|
||||
|
||||
let active_path = (self.fork.is_none())
|
||||
.then(|| self.repo_path.clone())
|
||||
.flatten();
|
||||
|
||||
let candidates = self.fork_candidates(cx);
|
||||
let user = Backend::global(cx).read(cx).current_user();
|
||||
let active_fork = self.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||
@@ -1338,8 +1329,6 @@ impl NewPullRequestView {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Commits tab, `merge_base..compare` in a virtual list.
|
||||
/// Clicking a row opens the commit's diff in a new panel.
|
||||
fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(commits) = self.commits.as_ref() else {
|
||||
return placeholder("No commits", cx);
|
||||
@@ -1417,8 +1406,6 @@ fn count_badge(count: usize, cx: &App) -> impl IntoElement {
|
||||
}
|
||||
|
||||
/// The trigger of a branch selector.
|
||||
/// Shows the icon, the current selection or placeholder, and the caret.
|
||||
/// `Combobox` replaces its default trigger entirely.
|
||||
fn render_ref_trigger(
|
||||
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
|
||||
icon: CustomIconName,
|
||||
|
||||
@@ -7,8 +7,7 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
||||
ScrollStrategy, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative,
|
||||
size,
|
||||
ScrollStrategy, SharedString, Size, Task, WeakEntity, Window, div, px, relative, size,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::clipboard::Clipboard;
|
||||
@@ -42,8 +41,7 @@ use super::helpers::{
|
||||
const TREE_WIDTH: f32 = 260.;
|
||||
|
||||
/// Height of one commit row in the commits tab's virtual list.
|
||||
/// A single text line plus the 1px bottom border.
|
||||
const PR_COMMIT_ROW_HEIGHT: f32 = 37.;
|
||||
const ROW_HEIGHT: f32 = 37.;
|
||||
|
||||
/// Detail panel of a single pull request.
|
||||
pub struct PullRequestDetailView {
|
||||
@@ -60,8 +58,6 @@ pub struct PullRequestDetailView {
|
||||
/// Display name of the repository, for panels opened from here.
|
||||
repo_name: SharedString,
|
||||
/// Local clone the PR's git changes come from.
|
||||
/// `None` when the diff is parsed from the nostr patch set.
|
||||
/// No commit diff viewer in that case.
|
||||
worktree: Option<PathBuf>,
|
||||
/// Root PR's content, shown as plain text.
|
||||
description: SharedString,
|
||||
@@ -91,14 +87,9 @@ pub struct PullRequestDetailView {
|
||||
/// Virtual list state of the commits tab.
|
||||
commit_scroll_handle: VirtualListScrollHandle,
|
||||
/// Comment bodies as shared strings, keyed by comment event ID.
|
||||
/// Re-renders don't clone full contents again.
|
||||
/// Events are immutable, so the cache never needs invalidation.
|
||||
contents: HashMap<EventId, SharedString>,
|
||||
/// In-flight tasks, finished tasks are pruned on every push.
|
||||
/// The vec stays bounded by the number of concurrent loads.
|
||||
tasks: Vec<Task<Result<(), anyhow::Error>>>,
|
||||
/// Subscriptions keeping the view live as the store refreshes.
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl PullRequestDetailView {
|
||||
@@ -109,26 +100,12 @@ impl PullRequestDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let repo_name = store.read(cx).name();
|
||||
let tree_state = cx.new(|cx| TreeState::new(cx));
|
||||
|
||||
let comment_input =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment..."));
|
||||
|
||||
// Same display name as the repo detail panel's title.
|
||||
let repo_name = store
|
||||
.read(cx)
|
||||
.announcement
|
||||
.as_ref()
|
||||
.map(|announcement| {
|
||||
announcement
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Re-render when the store refreshes, new comments or status changes.
|
||||
let subscriptions = vec![cx.observe(&store, |_this, _store, cx| cx.notify())];
|
||||
|
||||
// Defer loading until the window is ready, like the commit diff view.
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.load(window, cx);
|
||||
@@ -158,16 +135,10 @@ impl PullRequestDetailView {
|
||||
commit_scroll_handle: VirtualListScrollHandle::new(),
|
||||
contents: HashMap::new(),
|
||||
tasks: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the PR events from the store.
|
||||
/// File changes and the commit list are computed on a background task.
|
||||
/// The tree is populated from the result.
|
||||
/// The changes come from the PR's patch set, NIP-34 `e`-linked patch events, when present.
|
||||
/// Otherwise from the git repository, `c`, `clone` and `merge-base` tags.
|
||||
/// Diffing the `merge-base..tip` range.
|
||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.loading = true;
|
||||
self.error = None;
|
||||
@@ -177,6 +148,7 @@ impl PullRequestDetailView {
|
||||
|
||||
let (description, patch, current_commit, merge_base, clone_urls, addr, has_patch_link) = {
|
||||
let store = self.store.read(cx);
|
||||
|
||||
let Some(root) = store
|
||||
.pull_requests
|
||||
.iter()
|
||||
@@ -187,19 +159,24 @@ impl PullRequestDetailView {
|
||||
cx.notify();
|
||||
return;
|
||||
};
|
||||
|
||||
let update = latest_update(store.pull_requests.iter(), root);
|
||||
|
||||
let tip = update
|
||||
.and_then(current_commit_of)
|
||||
.or_else(|| current_commit_of(root));
|
||||
|
||||
let base = update
|
||||
.and_then(merge_base_of)
|
||||
.or_else(|| merge_base_of(root));
|
||||
|
||||
let clone_urls = clone_urls_of(root).or_else(|| {
|
||||
store
|
||||
.announcement
|
||||
.as_ref()
|
||||
.map(|a| a.clone.iter().map(ToString::to_string).collect())
|
||||
});
|
||||
|
||||
(
|
||||
root.content.clone(),
|
||||
pull_request_patch(root, store.patches.iter()),
|
||||
@@ -210,16 +187,17 @@ impl PullRequestDetailView {
|
||||
root.tags.event_ids().next().is_some(),
|
||||
)
|
||||
};
|
||||
|
||||
self.description = description.into();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
// Parse the nostr patch set first.
|
||||
let nostr_diff = cx
|
||||
.background_spawn({
|
||||
let patch = patch.clone();
|
||||
async move { patch_diffs(&patch) }
|
||||
})
|
||||
.await;
|
||||
|
||||
let nostr_commits = cx
|
||||
.background_spawn({
|
||||
let patch = patch.clone();
|
||||
@@ -233,6 +211,7 @@ impl PullRequestDetailView {
|
||||
Ok(diff) => has_patch_link || !diff.files.is_empty(),
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
let git = if use_nostr {
|
||||
None
|
||||
} else {
|
||||
@@ -241,19 +220,22 @@ impl PullRequestDetailView {
|
||||
let clone_urls = clone_urls.clone();
|
||||
let base = merge_base.clone();
|
||||
let tip = current_commit.clone();
|
||||
|
||||
Some(
|
||||
cx.background_spawn(async move {
|
||||
let repo = cache.ensure_clone(&addr, &clone_urls)?;
|
||||
|
||||
let workdir = repo
|
||||
.workdir()
|
||||
.ok_or_else(|| anyhow::anyhow!("repository has no worktree"))?
|
||||
.to_path_buf();
|
||||
|
||||
let tip =
|
||||
tip.ok_or_else(|| anyhow::anyhow!("pull request has no tip commit"))?;
|
||||
|
||||
let base = match base {
|
||||
Some(base) => base,
|
||||
// No `merge-base` tag.
|
||||
// Use the merge base of the tip and the default branch.
|
||||
// No `merge-base` tag. Use the merge base of the tip and the default branch.
|
||||
None => {
|
||||
let head = repo
|
||||
.head_id()
|
||||
@@ -262,9 +244,11 @@ impl PullRequestDetailView {
|
||||
repo.merge_base(tip_id, head)?.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let diff = signed_git::worktree_commit_range_diff(&workdir, &base, &tip)?;
|
||||
let commits =
|
||||
signed_git::worktree_commit_range_commits(&workdir, &base, &tip)?;
|
||||
|
||||
Ok::<_, anyhow::Error>((diff, commits, workdir))
|
||||
})
|
||||
.await,
|
||||
@@ -281,9 +265,9 @@ impl PullRequestDetailView {
|
||||
this.loading = false;
|
||||
this.worktree = worktree;
|
||||
this.current_commit = current_commit.map(SharedString::from);
|
||||
this.commit_item_sizes =
|
||||
Rc::new(vec![size(px(0.), px(PR_COMMIT_ROW_HEIGHT)); commits.len()]);
|
||||
this.commit_item_sizes = Rc::new(vec![size(px(0.), px(ROW_HEIGHT)); commits.len()]);
|
||||
this.commits = commits;
|
||||
|
||||
match diff {
|
||||
Ok(diff) => {
|
||||
let mut paths: Vec<PathBuf> = diff
|
||||
@@ -312,6 +296,7 @@ impl PullRequestDetailView {
|
||||
this.error = Some(error.to_string().into());
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
@@ -386,7 +371,6 @@ impl PullRequestDetailView {
|
||||
})
|
||||
}
|
||||
|
||||
/// Left column showing the changed-files tree.
|
||||
fn render_tree_column(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let tree_state = self.tree_state.clone();
|
||||
let view = cx.entity().downgrade();
|
||||
@@ -416,7 +400,6 @@ impl PullRequestDetailView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Right column, header of the selected file plus its diff.
|
||||
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.loading {
|
||||
return v_flex()
|
||||
@@ -426,12 +409,15 @@ impl PullRequestDetailView {
|
||||
.child(Spinner::new().small())
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
if let Some(error) = self.error.clone() {
|
||||
return placeholder(&error, cx);
|
||||
}
|
||||
|
||||
let Some(diff) = self.diff.as_ref() else {
|
||||
return placeholder("Failed to load diff", cx);
|
||||
};
|
||||
|
||||
let Some(path) = self.selected_file.clone() else {
|
||||
return if diff.files.is_empty() {
|
||||
placeholder("No files changed in this pull request", cx)
|
||||
@@ -439,15 +425,13 @@ impl PullRequestDetailView {
|
||||
placeholder("Select a file", cx)
|
||||
};
|
||||
};
|
||||
|
||||
let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else {
|
||||
return placeholder("File not found", cx);
|
||||
};
|
||||
self.render_file_diff(file, cx.entity(), cx)
|
||||
}
|
||||
|
||||
/// The diff of one file, with a header showing status and stats.
|
||||
/// The hunks render in a virtual list.
|
||||
/// A large diff is never materialized per frame.
|
||||
fn render_file_diff(&self, file: &FileDiff, view: Entity<Self>, cx: &App) -> AnyElement {
|
||||
let status_label = match file.status {
|
||||
signed_git::DiffStatus::Added => "A",
|
||||
@@ -456,6 +440,7 @@ impl PullRequestDetailView {
|
||||
signed_git::DiffStatus::Renamed => "R",
|
||||
signed_git::DiffStatus::Copied => "C",
|
||||
};
|
||||
|
||||
let status_color = match file.status {
|
||||
signed_git::DiffStatus::Added => cx.theme().success,
|
||||
signed_git::DiffStatus::Modified => cx.theme().info,
|
||||
@@ -464,6 +449,7 @@ impl PullRequestDetailView {
|
||||
cx.theme().muted_foreground
|
||||
}
|
||||
};
|
||||
|
||||
let title = match &file.old_path {
|
||||
Some(old) => format!("{old} → {}", file.path),
|
||||
None => file.path.clone(),
|
||||
@@ -476,6 +462,7 @@ impl PullRequestDetailView {
|
||||
} else {
|
||||
let sizes = self.item_sizes.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
@@ -564,7 +551,6 @@ impl PullRequestDetailView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Underline tab bar with the Discussion, Files and Commits tabs.
|
||||
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let active = self.active_tab;
|
||||
let files_count = self.diff.as_ref().map(|diff| diff.files.len());
|
||||
@@ -610,8 +596,6 @@ impl PullRequestDetailView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Discussion tab, author, description and comments like the issue panel.
|
||||
/// The comment form sits at the end, a sidebar on the right.
|
||||
fn render_discussion(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.loading {
|
||||
return v_flex()
|
||||
@@ -776,8 +760,6 @@ impl PullRequestDetailView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Files tab, the changed-files tree on the left.
|
||||
/// The diff of the selected file on the right.
|
||||
fn render_files_tab(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
h_flex()
|
||||
.flex_1()
|
||||
@@ -790,6 +772,7 @@ impl PullRequestDetailView {
|
||||
}
|
||||
|
||||
/// Full-height Commits tab.
|
||||
///
|
||||
/// Every commit of the patch series, or a status message while loading or empty.
|
||||
fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.loading {
|
||||
@@ -839,6 +822,7 @@ impl PullRequestDetailView {
|
||||
}
|
||||
|
||||
/// One row of the commits tab, id, summary, author and time.
|
||||
///
|
||||
/// Clicking a row opens the commit's diff in the bottom dock.
|
||||
fn render_commit_row(
|
||||
&self,
|
||||
@@ -852,7 +836,7 @@ impl PullRequestDetailView {
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.px_4()
|
||||
.h(px(PR_COMMIT_ROW_HEIGHT))
|
||||
.h(px(ROW_HEIGHT))
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.text_sm()
|
||||
@@ -893,7 +877,6 @@ impl PullRequestDetailView {
|
||||
}
|
||||
|
||||
/// One comment card, same design as the issue panel.
|
||||
/// Header row holds the avatar, author, commented and age, content below.
|
||||
fn render_comments(&mut self, id: &EventId, cx: &mut Context<Self>) -> AnyElement {
|
||||
let store = self.store.read(cx);
|
||||
let comments: Vec<&Event> = store.comments_of(id).collect();
|
||||
@@ -1104,8 +1087,6 @@ impl PullRequestDetailView {
|
||||
}
|
||||
|
||||
/// Open the update pull request dialog.
|
||||
/// The patch input supplies the new revision.
|
||||
/// Confirming calls [`RepoStore::update_pull_request`].
|
||||
fn open_update_pull_request_dialog(
|
||||
store: Entity<RepoStore>,
|
||||
root: Event,
|
||||
@@ -1188,6 +1169,7 @@ fn current_commit_of(event: &Event) -> Option<String> {
|
||||
}
|
||||
|
||||
/// The `merge-base` tag of a PR event, as hex.
|
||||
///
|
||||
/// The most recent common ancestor with the target branch.
|
||||
fn merge_base_of(event: &Event) -> Option<String> {
|
||||
event
|
||||
@@ -1200,6 +1182,7 @@ fn merge_base_of(event: &Event) -> Option<String> {
|
||||
}
|
||||
|
||||
/// The `clone` tag of a PR event.
|
||||
///
|
||||
/// URLs where the proposed branch can be fetched, or `None` if the PR has none.
|
||||
fn clone_urls_of(event: &Event) -> Option<Vec<String>> {
|
||||
event
|
||||
@@ -1223,9 +1206,6 @@ fn branch_name_of(event: &Event) -> Option<String> {
|
||||
}
|
||||
|
||||
/// The latest PR update, kind 1619, revising `root`.
|
||||
/// Found via its NIP-22 `E` tag pointing at the root PR event.
|
||||
/// Only updates by the PR author count.
|
||||
/// The tip of a PR is only mutable by its author, NIP-34.
|
||||
fn latest_update<'a>(events: impl Iterator<Item = &'a Event>, root: &Event) -> Option<&'a Event> {
|
||||
let root_hex = root.id.to_hex();
|
||||
events
|
||||
@@ -1240,6 +1220,7 @@ fn latest_update<'a>(events: impl Iterator<Item = &'a Event>, root: &Event) -> O
|
||||
}
|
||||
|
||||
/// One-line commit metadata for the commits list.
|
||||
///
|
||||
/// Author and relative time, whichever is available.
|
||||
fn commit_meta(commit: &FileCommit) -> String {
|
||||
let author = commit.author.trim();
|
||||
|
||||
@@ -26,8 +26,7 @@ use super::pull_request_detail::PullRequestDetailView;
|
||||
use super::send_patch::open_send_patch_panel;
|
||||
|
||||
/// Height of one pull request row in the virtual list.
|
||||
/// Same layout as an issue row.
|
||||
const PR_ROW_HEIGHT: f32 = 73.;
|
||||
const ROW_HEIGHT: f32 = 73.;
|
||||
|
||||
/// Status filter of the pull request list, chosen via the header's filter buttons.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -70,17 +69,10 @@ pub struct PullRequestsView {
|
||||
/// Per-row heights of the virtual list.
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
/// The filtered pull request count [`Self::item_sizes`] was built for.
|
||||
/// Rebuilt on change.
|
||||
pr_len: usize,
|
||||
/// Indices into the store's `pull_requests` matching [`Self::filter`].
|
||||
/// Root PR events only, updates are revisions of the root.
|
||||
/// The virtual list renders this slice.
|
||||
/// Rebuilt only when the store version or the filter changes.
|
||||
/// Keyed by [`Self::cache_key`].
|
||||
visible_prs: Vec<usize>,
|
||||
/// Header counts `(total, open, closed, draft, merged)`.
|
||||
/// Root pull requests only, revisions are not separate PRs.
|
||||
/// Rebuilt with [`Self::visible_prs`].
|
||||
counts: (usize, usize, usize, usize, usize),
|
||||
/// Store version and filter the cached rows/counts were built from.
|
||||
cache_key: Option<(u64, PullRequestFilter)>,
|
||||
@@ -139,6 +131,7 @@ impl PullRequestsView {
|
||||
}
|
||||
|
||||
/// Render one row of the pull request list.
|
||||
///
|
||||
/// `ix` is the row index, `pr_ix` the index in the store's `pull_requests`.
|
||||
fn render_row(&self, ix: usize, pr_ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let pr = &self.store.read(cx).pull_requests[pr_ix];
|
||||
@@ -205,7 +198,6 @@ impl PullRequestsView {
|
||||
|
||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
// Counts of the last list rebuild.
|
||||
// `render` rebuilds first when the store version or filter changed, so never stale.
|
||||
let (total, open, closed, draft, merged) = self.counts;
|
||||
|
||||
h_flex()
|
||||
@@ -341,8 +333,8 @@ impl Render for PullRequestsView {
|
||||
let filter = self.filter;
|
||||
|
||||
// Rows and counts are rebuilt only when the store refreshed or filter changed.
|
||||
// Other renders reuse the cache.
|
||||
let version = self.store.read(cx).version();
|
||||
|
||||
if self.cache_key != Some((version, filter)) {
|
||||
let store = self.store.read(cx);
|
||||
let mut counts = (0usize, 0usize, 0usize, 0usize, 0usize);
|
||||
@@ -351,24 +343,24 @@ impl Render for PullRequestsView {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, pr)| {
|
||||
// Kind-30620 patches are revisions of a root PR, NIP-34.
|
||||
// They are not separate pull requests.
|
||||
// Count root events only, or the counts inflate with every revision.
|
||||
// Revisions also default to `Open` in `status_of`.
|
||||
if pr.kind != Kind::GitPullRequest {
|
||||
return None;
|
||||
}
|
||||
|
||||
let status = store.status_of(pr);
|
||||
counts.0 += 1;
|
||||
|
||||
match status {
|
||||
RepoStatus::Open => counts.1 += 1,
|
||||
RepoStatus::Closed => counts.2 += 1,
|
||||
RepoStatus::Draft => counts.3 += 1,
|
||||
RepoStatus::Applied => counts.4 += 1,
|
||||
}
|
||||
|
||||
filter.matches(status).then_some(ix)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.counts = counts;
|
||||
self.cache_key = Some((version, filter));
|
||||
}
|
||||
@@ -379,7 +371,7 @@ impl Render for PullRequestsView {
|
||||
// Rebuild it whenever the filtered pull request count changes.
|
||||
if count != self.pr_len {
|
||||
self.pr_len = count;
|
||||
self.item_sizes = Rc::new(vec![size(px(0.), px(PR_ROW_HEIGHT)); count]);
|
||||
self.item_sizes = Rc::new(vec![size(px(0.), px(ROW_HEIGHT)); count]);
|
||||
}
|
||||
|
||||
let sizes = self.item_sizes.clone();
|
||||
|
||||
@@ -2,10 +2,10 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
|
||||
Subscription, WeakEntity, Window, div, px,
|
||||
WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_base::{Button as BaseButton, StyledExt};
|
||||
use gpui_component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||
use gpui_component::input::{Input, InputState, Textarea, TextareaState};
|
||||
use gpui_component::scroll::ScrollableElement;
|
||||
use gpui_component::spinner::Spinner;
|
||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
||||
@@ -29,7 +29,6 @@ pub struct SendPatchView {
|
||||
submitting: bool,
|
||||
/// Error of the last submit attempt, it keeps the panel open.
|
||||
error: Option<SharedString>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl SendPatchView {
|
||||
@@ -41,22 +40,14 @@ impl SendPatchView {
|
||||
) -> Self {
|
||||
let repo_name = store.read(cx).name();
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
||||
|
||||
let description = cx
|
||||
.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change (optional)"));
|
||||
|
||||
let patch = cx.new(|cx| {
|
||||
TextareaState::new(window, cx).placeholder("diff --git a/file.txt b/file.txt\nindex 1234567..abcdefg 100644\n--- a/file.txt\n+++ b/file.txt")
|
||||
});
|
||||
|
||||
// Re-evaluate the Send button's enabled state as the inputs change.
|
||||
let subscriptions = vec![
|
||||
cx.subscribe(&subject, |_this, _state, _event: &InputEvent, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
cx.subscribe(&patch, |_this, _state, _event: &InputEvent, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
@@ -67,25 +58,23 @@ impl SendPatchView {
|
||||
patch,
|
||||
submitting: false,
|
||||
error: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish the pull request from the pasted patch.
|
||||
/// The store validates synchronously, patch shape, per-part size and sign-in.
|
||||
/// On failure the panel stays open with the error inline.
|
||||
/// On success it closes.
|
||||
/// Async publish failures surface in the pull request list's banner.
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.submitting {
|
||||
return;
|
||||
}
|
||||
|
||||
let subject = self.subject.read(cx).value().to_string();
|
||||
let description = self.description.read(cx).value().to_string();
|
||||
let patch = self.patch.read(cx).value().to_string();
|
||||
|
||||
if patch.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let store = self.store.clone();
|
||||
let dock_area = self.dock_area.clone();
|
||||
let entity = cx.entity().clone();
|
||||
@@ -95,7 +84,6 @@ impl SendPatchView {
|
||||
cx.notify();
|
||||
|
||||
// Errors the store detects before publishing.
|
||||
// Returned synchronously through `last_error`.
|
||||
let sync_error = store.update(cx, |store, cx| {
|
||||
store.open_pull_request(
|
||||
(!subject.is_empty()).then_some(subject),
|
||||
@@ -129,6 +117,7 @@ impl SendPatchView {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ enum RepoFilter {
|
||||
|
||||
impl RepoFilter {
|
||||
/// Indices into the store's `announcements` this filter includes, in display order.
|
||||
///
|
||||
/// Narrowed to repositories whose name or id contains `query`.
|
||||
/// An empty query matches everything.
|
||||
fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
|
||||
let announcements = &store.announcements;
|
||||
let mut indices: Vec<usize> = (0..announcements.len()).collect();
|
||||
@@ -96,7 +96,6 @@ pub struct RepoListView {
|
||||
/// Number of rows [`Self::item_sizes`] was built for, the filtered repo count.
|
||||
repo_len: usize,
|
||||
/// Indices matching [`Self::filter`] into the store's `announcements`.
|
||||
/// The virtual list renders this slice in display order.
|
||||
visible: Vec<usize>,
|
||||
/// Search box filtering repositories by name.
|
||||
search: Entity<InputState>,
|
||||
@@ -150,6 +149,7 @@ impl RepoListView {
|
||||
}
|
||||
|
||||
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the store.
|
||||
///
|
||||
/// Uses the store contents, [`Self::filter`] and the search query.
|
||||
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
||||
let filter = self.filter;
|
||||
|
||||
@@ -24,9 +24,6 @@ pub struct CreateRepoState {
|
||||
}
|
||||
|
||||
/// Open the Create Repository dialog.
|
||||
/// Loads the user's default grasp servers, a kind `10317` grasp list.
|
||||
/// Falls back to the shared defaults when none are set.
|
||||
/// On success the dialog closes and the new repository opens in the dock.
|
||||
pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
||||
let settings = SettingsStore::global(cx);
|
||||
let default_folder = settings
|
||||
@@ -160,8 +157,6 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
|
||||
}
|
||||
|
||||
/// Pick the repository's storage folder with the platform's native folder picker.
|
||||
/// Show the result in the disabled folder input.
|
||||
/// The settings remember the picked folder as the default next time.
|
||||
fn choose_folder(folder_input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
|
||||
let handle = window.window_handle();
|
||||
let folder_input = folder_input.clone();
|
||||
@@ -197,6 +192,7 @@ fn choose_folder(folder_input: &Entity<InputState>, window: &mut Window, cx: &mu
|
||||
}
|
||||
|
||||
/// Run the create-repository flow.
|
||||
///
|
||||
/// Opens the new working copy and the repository panel on success.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_repository(
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct GraspServersState {
|
||||
|
||||
impl GraspServersState {
|
||||
/// Defaults used until the user's grasp list loads, which replaces them when non-empty.
|
||||
///
|
||||
/// Persisted settings supply the defaults, an empty list falls back to the built-ins.
|
||||
pub fn new_default(settings: &GraspServersSettings) -> Self {
|
||||
let urls: Vec<String> = if settings.default_servers.is_empty() {
|
||||
@@ -46,8 +47,6 @@ impl GraspServersState {
|
||||
}
|
||||
|
||||
/// The Grasp servers form field shared by the publish dialogs.
|
||||
/// An expandable toggle, the configured servers each removable, and an add-relay input.
|
||||
/// Shows a loading hint while the user's kind `10317` grasp list is fetched.
|
||||
pub fn grasp_servers_field(
|
||||
state: &Entity<GraspServersState>,
|
||||
relay_input: &Entity<InputState>,
|
||||
@@ -223,6 +222,7 @@ fn add_relay(
|
||||
}
|
||||
|
||||
/// Load the user's grasp list of kind `10317` from the local database.
|
||||
///
|
||||
/// It replaces the defaults when it lists any servers.
|
||||
pub fn load_user_grasp_servers(
|
||||
state: Entity<GraspServersState>,
|
||||
|
||||
@@ -2,7 +2,6 @@ use gpui::{App, Window, px};
|
||||
use gpui_component::WindowExt;
|
||||
|
||||
/// Open the Import Identity dialog.
|
||||
/// Currently a placeholder, the dialog only shows a title.
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
window.open_dialog(cx, move |dialog, _window, _cx| {
|
||||
dialog.title("Import identity").width(px(400.))
|
||||
|
||||
@@ -15,8 +15,6 @@ pub struct OnboardingState {
|
||||
}
|
||||
|
||||
/// Open the Onboarding dialog for creating a new identity.
|
||||
/// The caller creates the input and state entities and passes them in.
|
||||
/// This function only builds the dialog UI and wires up the continue-button handler.
|
||||
pub fn open(
|
||||
name_input: Entity<InputState>,
|
||||
pass_input: Entity<InputState>,
|
||||
|
||||
@@ -18,8 +18,6 @@ pub struct PassphraseState {
|
||||
}
|
||||
|
||||
/// Open the dialog asking for the passphrase that protects the stored identity.
|
||||
/// The identity is NIP-49 encrypted, for example `ncryptsec1...`.
|
||||
/// Called when the backend emits [`signed_state::BackendEvent::PassphraseRequired`].
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
let pass_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
@@ -98,8 +96,6 @@ pub fn open(window: &mut Window, cx: &mut App) {
|
||||
}
|
||||
|
||||
/// Submit the passphrase to the backend.
|
||||
/// On success the dialog closes.
|
||||
/// On failure the error is rendered inline and the dialog stays open.
|
||||
fn unlock(
|
||||
pass_input: &Entity<InputState>,
|
||||
state: &Entity<PassphraseState>,
|
||||
|
||||
@@ -49,7 +49,6 @@ fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
|
||||
}
|
||||
|
||||
/// Stateful controls of the settings dialog, created once when it opens.
|
||||
/// Their values survive re-renders of the dialog content.
|
||||
struct SettingsControls {
|
||||
appearance: Entity<SelectState<Vec<SelectOption>>>,
|
||||
light_theme: Entity<SelectState<Vec<SelectOption>>>,
|
||||
|
||||
@@ -46,8 +46,6 @@ impl Workspace {
|
||||
let mut subscriptions = vec![];
|
||||
|
||||
// A bottom or right dock whose last panel was dragged away is removed entirely.
|
||||
// The emptied region would otherwise linger as a bare strip.
|
||||
// The removal is deferred, the event arrives while the area is mid-update.
|
||||
let dock_for_pruning = dock.clone();
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&dock,
|
||||
@@ -78,7 +76,6 @@ impl Workspace {
|
||||
let backend = Backend::global(cx);
|
||||
|
||||
// Ask for the passphrase when the stored identity is NIP-49 encrypted.
|
||||
// Subscribed via the window, since opening a dialog needs a window.
|
||||
let passphrase_subscription =
|
||||
window.subscribe(&backend, cx, |_backend, event, window, cx| {
|
||||
if matches!(event, BackendEvent::PassphraseRequired) {
|
||||
@@ -87,7 +84,6 @@ impl Workspace {
|
||||
});
|
||||
|
||||
// The event may have fired before this window existed.
|
||||
// The backend is initialized before the first window opens.
|
||||
// Fall back to the backend state in that case.
|
||||
if backend.read(cx).passphrase_required() {
|
||||
passphrase_dialog::open(window, cx);
|
||||
|
||||
Reference in New Issue
Block a user