From 18436225404504a0048f85caa9d682d69d6b1ea1 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sat, 23 May 2026 18:45:44 +0100 Subject: [PATCH] refactor: prefer Arc to String --- build.rs | 32 +- examples/similar_demo.rs | 6 +- src/api.rs | 14 +- src/api/auth.rs | 16 +- src/api/issues.rs | 113 +++--- src/api/mock.rs | 100 +++-- src/api/repo.rs | 45 +- src/api/user.rs | 16 +- src/component/markdown.rs | 474 +++++++++++----------- src/screen/dashboard/issue_list.rs | 55 ++- src/screen/dashboard/pull_request_view.rs | 14 +- src/screen/setup_wizard/github_step.rs | 146 ++++--- src/util/file.rs | 16 +- src/util/mod.rs | 1 + src/util/str.rs | 20 + 15 files changed, 524 insertions(+), 544 deletions(-) create mode 100644 src/util/str.rs diff --git a/build.rs b/build.rs index 0a9333b..bbf50f6 100644 --- a/build.rs +++ b/build.rs @@ -175,8 +175,10 @@ fn render_github_fixtures(fixture_root: &Path) -> String { if let Some(id) = parse_pull_request_file_tree_fixture_name(&file_name) { let value = read_fixture_value(&entry.path()); - pull_request_file_tree_fixtures - .insert(id, read_pull_request_file_tree_fixture(&value, &entry.path())); + pull_request_file_tree_fixtures.insert( + id, + read_pull_request_file_tree_fixture(&value, &entry.path()), + ); continue; } @@ -232,12 +234,12 @@ fn render_github_fixtures(fixture_root: &Path) -> String { output.push_str(&string_literal(&path)); output.push_str(", "); match reff { - | Some(reff) => { - output.push_str("Some("); - output.push_str(&string_literal(&reff)); - output.push(')'); - } - | None => output.push_str("None"), + | Some(reff) => { + output.push_str("Some("); + output.push_str(&string_literal(&reff)); + output.push(')'); + } + | None => output.push_str("None"), } output.push_str(") => Some("); output.push_str(&string_literal(&content)); @@ -291,8 +293,8 @@ fn render_github_fixtures(fixture_root: &Path) -> String { output.push_str(&string_literal(&id)); output.push_str(", "); match previous_end_cursor.as_deref() { - | Some(after) => output.push_str(&format!("Some({})", string_literal(after))), - | None => output.push_str("None"), + | Some(after) => output.push_str(&format!("Some({})", string_literal(after))), + | None => output.push_str("None"), } output.push_str(") => Some("); output.push_str(&string_literal(&fixture.json)); @@ -427,9 +429,9 @@ fn issue_fixture_state(issue: &serde_json::Value) -> &'static str { } match required_string(issue, &["state"]) { - | "open" => "OPEN", - | "closed" => "CLOSED", - | state => panic!("unsupported pull request state in fixture: {state}"), + | "open" => "OPEN", + | "closed" => "CLOSED", + | state => panic!("unsupported pull request state in fixture: {state}"), } } @@ -498,8 +500,8 @@ fn collect_repo_file_content_fixtures( let owner = parts[0].clone(); let repo = parts[1].clone(); let reff = match parts[2].as_str() { - | "@default" => None, - | value => Some(value.to_owned()), + | "@default" => None, + | value => Some(value.to_owned()), }; let virtual_path = parts[3..].join("/"); let content = fs::read_to_string(&path).unwrap_or_else(|err| { diff --git a/examples/similar_demo.rs b/examples/similar_demo.rs index a3021bf..80d1b71 100644 --- a/examples/similar_demo.rs +++ b/examples/similar_demo.rs @@ -8,9 +8,9 @@ fn main() { for change in diff.iter_all_changes() { let sign = match change.tag() { - ChangeTag::Delete => "-", - ChangeTag::Insert => "+", - ChangeTag::Equal => " ", + | ChangeTag::Delete => "-", + | ChangeTag::Insert => "+", + | ChangeTag::Equal => " ", }; print!("{}{}", sign, change); } diff --git a/src/api.rs b/src/api.rs index 01df1e8..241c2ee 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use reqwest::Method; use serde::{Deserialize, Serialize}; @@ -22,7 +24,7 @@ pub struct QueryContext { #[derive(Clone, Serialize, Deserialize)] pub(crate) struct AuthTokens { - pub(crate) access_token: String, + pub(crate) access_token: Arc, } #[derive(Clone)] @@ -111,9 +113,9 @@ async fn raw_content(res: reqwest::Response) -> Result { Ok(res.bytes().await?) } else { match res.status() { - | reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist), - | reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed), - | _ => Err(Error::MalformedResponse(res.status().to_string())), + | reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist), + | reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed), + | _ => Err(Error::MalformedResponse(res.status().to_string())), } } } @@ -150,7 +152,7 @@ where { let mut body: graphql_client::Response = res.json().await?; match body.data.take() { - | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), - | Some(data) => Ok((body, data)), + | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), + | Some(data) => Ok((body, data)), } } diff --git a/src/api/auth.rs b/src/api/auth.rs index 2431316..5f2fb4d 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use serde::Deserialize; @@ -11,9 +11,9 @@ pub struct CreateDeviceCode; #[derive(Deserialize)] pub(crate) struct DeviceCodeResponse { - pub device_code: String, - pub user_code: String, - pub vertification_uri: Option, + pub device_code: Arc, + pub user_code: Arc, + pub vertification_uri: Option>, pub expires_in: u16, // minimum number of seconds between polling for access token @@ -46,14 +46,14 @@ impl query::QueryFn for CreateDeviceCode { #[derive(Clone)] pub struct RequestAccessToken { - pub device_code: String, + pub device_code: Arc, } #[derive(Deserialize)] pub struct RequestAccessTokenResponse { - pub access_token: String, - pub token_type: String, - pub scope: String, + pub access_token: Arc, + pub token_type: Arc, + pub scope: Arc, } impl query::QueryFn for RequestAccessToken { diff --git a/src/api/issues.rs b/src/api/issues.rs index efa444c..a4ac271 100644 --- a/src/api/issues.rs +++ b/src/api/issues.rs @@ -1,4 +1,4 @@ -use std::ops::Deref; +use std::sync::Arc; use graphql_client::GraphQLQuery; use serde::Deserialize; @@ -21,33 +21,7 @@ type GitObjectID = String; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize)] #[serde(transparent)] #[repr(transparent)] -pub(crate) struct Id(String); - -impl Deref for Id { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From<&str> for Id { - fn from(value: &str) -> Self { - Self(value.to_owned()) - } -} - -impl From for Id { - fn from(value: String) -> Self { - Self(value) - } -} - -impl From for String { - fn from(value: Id) -> Self { - value.0 - } -} +pub(crate) struct Id(pub(crate) Arc); #[derive(Debug, Deserialize)] pub(crate) struct PullRequestPaginatedResponse { @@ -59,32 +33,32 @@ pub(crate) struct PullRequestPaginatedResponse { #[derive(Debug, Deserialize)] pub(crate) struct PullRequest { pub(crate) id: Id, - pub(crate) title: String, + pub(crate) title: Arc, pub(crate) state: PullRequestState, pub(crate) is_draft: bool, - pub(crate) repo_slug: String, + pub(crate) repo_slug: Arc, } #[derive(Debug, Deserialize)] pub(crate) struct DetailedPullRequest { - pub(crate) title: String, + pub(crate) title: Arc, pub(crate) state: PullRequestState, pub(crate) is_draft: bool, - pub(crate) body: String, + pub(crate) body: Arc, pub(crate) created_at: Option>, pub(crate) author: Option, - pub(crate) base_branch_name: String, - pub(crate) base_repo_slug: String, - pub(crate) base_ref: String, - pub(crate) head_branch_name: String, - pub(crate) head_ref: String, - pub(crate) head_repo_slug: String, + pub(crate) base_branch_name: Arc, + pub(crate) base_repo_slug: Arc, + pub(crate) base_ref: Arc, + pub(crate) head_branch_name: Arc, + pub(crate) head_ref: Arc, + pub(crate) head_repo_slug: Arc, } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct PullRequestTimeline { pub(crate) items: Vec, - pub(crate) end_cursor: Option, + pub(crate) end_cursor: Option>, pub(crate) has_next_page: bool, } @@ -245,12 +219,6 @@ pub(crate) struct TimelineActor { pub(crate) avatar_url: Option, } -impl std::fmt::Display for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub(crate) enum PullRequestState { @@ -310,6 +278,24 @@ pub(crate) struct ListPullRequests { pub page: u32, } +impl std::fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl From for Id { + fn from(value: String) -> Self { + Self(value.into()) + } +} + +impl From<&str> for Id { + fn from(value: &str) -> Self { + Self(value.into()) + } +} + impl query::QueryFn for ListPullRequests { type Data = PullRequestPaginatedResponse; type Error = api::Error; @@ -357,13 +343,14 @@ impl query::QueryFn for ListPullRequests { | PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => { Some(PullRequest { id: p.id.into(), - title: p.title, + title: p.title.into(), state: p.state, is_draft: p.is_draft, repo_slug: format!( "{}/{}", p.repository.owner.login, p.repository.name - ), + ) + .into(), }) } | _ => None, @@ -399,7 +386,7 @@ impl query::QueryFn for FetchPullRequest { } let gql = PullRequestQuery::build_query(pull_request_query::Variables { - id: self.id.clone().into(), + id: self.id.to_string(), }); let res = c.github_graphql_request(&gql)?.send().await?; @@ -421,26 +408,26 @@ impl query::QueryFn for FetchPullRequest { })?; Ok(DetailedPullRequest { - title: p.title, + title: p.title.into(), state: p.state, is_draft: p.is_draft, - body: p.body, + body: p.body.into(), author: p.author.map(|it| api::user::Actor { - login: it.login, - avatar_url: it.avatar_url, + login: it.login.into(), + avatar_url: it.avatar_url.into(), }), base_repo_slug: p .base_repository - .map(|it| it.name_with_owner) + .map(|it| it.name_with_owner.into()) .unwrap_or_default(), - base_branch_name: p.base_ref_name, - base_ref: p.base_ref_oid, + base_branch_name: p.base_ref_name.into(), + base_ref: p.base_ref_oid.into(), head_repo_slug: p .head_repository - .map(|it| it.name_with_owner) + .map(|it| it.name_with_owner.into()) .unwrap_or_default(), - head_branch_name: p.head_ref_name, - head_ref: p.head_ref_oid, + head_branch_name: p.head_ref_name.into(), + head_ref: p.head_ref_oid.into(), created_at: Some(created_at), }) } @@ -473,7 +460,7 @@ impl query::QueryFn for FetchPullRequestFileTree { } else { let gql = PullRequestFileTreeQuery::build_query(pull_request_file_tree_query::Variables { - id: self.id.clone().into(), + id: self.id.to_string(), first: self.first, }); @@ -558,7 +545,7 @@ impl query::QueryFn for FetchPullRequestFileTree { pub(crate) struct FetchPullRequestTimeline { pub(crate) id: Id, pub(crate) first: i64, - pub(crate) after: Option, + pub(crate) after: Option>, } impl query::QueryFn for FetchPullRequestTimeline { @@ -841,9 +828,9 @@ impl query::QueryFn for FetchPullRequestTimeline { } else { let gql = PullRequestTimelineQuery::build_query(pull_request_timeline_query::Variables { - id: self.id.clone().into(), + id: self.id.to_string(), first: self.first, - after: self.after.clone(), + after: self.after.as_ref().map(|it| it.to_string()), }); let res = c.github_graphql_request(&gql)?.send().await?; @@ -892,7 +879,7 @@ impl query::QueryFn for FetchPullRequestTimeline { Ok(PullRequestTimeline { items, - end_cursor: timeline.page_info.end_cursor, + end_cursor: timeline.page_info.end_cursor.map(|it| it.into()), has_next_page: timeline.page_info.has_next_page, }) } diff --git a/src/api/mock.rs b/src/api/mock.rs index ceee939..9a8485a 100644 --- a/src/api/mock.rs +++ b/src/api/mock.rs @@ -175,12 +175,12 @@ mod tests { assert_eq!(merged.state, issues::PullRequestState::Merged); assert!(merged.body.contains("| Stage | Owner | Status |")); assert_eq!( - merged.author.as_ref().map(|author| author.login.as_str()), + merged.author.as_ref().map(|author| author.login.as_ref()), Some("rorycraft") ); - assert_eq!(merged.base_branch_name.as_str(), "main"); + assert_eq!(merged.base_branch_name.as_ref(), "main"); assert_eq!( - merged.head_branch_name.as_str(), + merged.head_branch_name.as_ref(), "feat/release-handoff-checklist" ); assert_eq!( @@ -196,12 +196,12 @@ mod tests { documented_failover .author .as_ref() - .map(|author| author.login.as_str()), + .map(|author| author.login.as_ref()), Some("kennethnym") ); - assert_eq!(documented_failover.base_branch_name.as_str(), "main"); + assert_eq!(documented_failover.base_branch_name.as_ref(), "main"); assert_eq!( - documented_failover.head_branch_name.as_str(), + documented_failover.head_branch_name.as_ref(), "docs/manual-failover-steps" ); assert_eq!( @@ -209,17 +209,17 @@ mod tests { Some(chrono::DateTime::parse_from_rfc3339("2026-04-24T06:40:00Z").unwrap()) ); assert!(dashboard_markdown.body.contains("```rust")); - assert_eq!(dashboard_markdown.base_branch_name.as_str(), "main"); + assert_eq!(dashboard_markdown.base_branch_name.as_ref(), "main"); assert_eq!( - dashboard_markdown.head_branch_name.as_str(), + dashboard_markdown.head_branch_name.as_ref(), "feat/cached-issue-pane" ); assert_eq!( - dashboard_markdown.base_ref.as_str(), + dashboard_markdown.base_ref.as_ref(), "5e8745bfcc0c90c226d3c6af84226d6d4a5ec2d1" ); assert_eq!( - dashboard_markdown.head_ref.as_str(), + dashboard_markdown.head_ref.as_ref(), "2bc41de7731b9ef48f7d64ee9f0d5f497dbe0a51" ); assert_eq!( @@ -230,20 +230,20 @@ mod tests { cached_repo_picker .author .as_ref() - .map(|author| author.login.as_str()), + .map(|author| author.login.as_ref()), Some("kennethnym") ); - assert_eq!(cached_repo_picker.base_branch_name.as_str(), "main"); + assert_eq!(cached_repo_picker.base_branch_name.as_ref(), "main"); assert_eq!( - cached_repo_picker.head_branch_name.as_str(), + cached_repo_picker.head_branch_name.as_ref(), "feat/cached-repo-picker" ); assert_eq!( - cached_repo_picker.base_ref.as_str(), + cached_repo_picker.base_ref.as_ref(), "5e8745bfcc0c90c226d3c6af84226d6d4a5ec2d1" ); assert_eq!( - cached_repo_picker.head_ref.as_str(), + cached_repo_picker.head_ref.as_ref(), "13af7d0b48a6ce0b22d48c9b6c1c78dfcd94e6a0" ); assert_eq!( @@ -254,12 +254,12 @@ mod tests { worker_split .author .as_ref() - .map(|author| author.login.as_str()), + .map(|author| author.login.as_ref()), Some("leaferiksen") ); - assert_eq!(worker_split.base_branch_name.as_str(), "main"); + assert_eq!(worker_split.base_branch_name.as_ref(), "main"); assert_eq!( - worker_split.head_branch_name.as_str(), + worker_split.head_branch_name.as_ref(), "feat/worker-context-envelope" ); assert_eq!( @@ -270,12 +270,12 @@ mod tests { spacing_tokens .author .as_ref() - .map(|author| author.login.as_str()), + .map(|author| author.login.as_ref()), Some("mariahops") ); - assert_eq!(spacing_tokens.base_branch_name.as_str(), "main"); + assert_eq!(spacing_tokens.base_branch_name.as_ref(), "main"); assert_eq!( - spacing_tokens.head_branch_name.as_str(), + spacing_tokens.head_branch_name.as_ref(), "chore/dashboard-spacing-scale" ); assert_eq!( @@ -294,13 +294,13 @@ mod tests { let base_query = fetch_file_content( "kennethnym/novem", "src/query.rs", - Some(dashboard_markdown.base_ref.as_str()), + Some(dashboard_markdown.base_ref.as_ref()), ) .expect("base query fixture should exist"); let head_query = fetch_file_content( "kennethnym/novem", "src/query.rs", - Some(dashboard_markdown.head_ref.as_str()), + Some(dashboard_markdown.head_ref.as_ref()), ) .expect("head query fixture should exist"); let base_query = std::str::from_utf8(base_query.as_ref()) @@ -317,13 +317,13 @@ mod tests { let base_repo = fetch_file_content( "kennethnym/novem", "src/api/repo.rs", - Some(cached_repo_picker.base_ref.as_str()), + Some(cached_repo_picker.base_ref.as_ref()), ) .expect("base repo fixture should exist"); let head_repo = fetch_file_content( "kennethnym/novem", "src/api/repo.rs", - Some(cached_repo_picker.head_ref.as_str()), + Some(cached_repo_picker.head_ref.as_ref()), ) .expect("head repo fixture should exist"); let base_repo = @@ -364,15 +364,15 @@ mod tests { for path in file_paths { fetch_file_content( - dashboard_markdown.base_repo_slug.as_str(), + dashboard_markdown.base_repo_slug.as_ref(), path, - Some(dashboard_markdown.base_ref.as_str()), + Some(dashboard_markdown.base_ref.as_ref()), ) .unwrap_or_else(|_| panic!("base fixture should exist for {path}")); fetch_file_content( - dashboard_markdown.head_repo_slug.as_str(), + dashboard_markdown.head_repo_slug.as_ref(), path, - Some(dashboard_markdown.head_ref.as_str()), + Some(dashboard_markdown.head_ref.as_ref()), ) .unwrap_or_else(|_| panic!("head fixture should exist for {path}")); } @@ -440,36 +440,30 @@ mod tests { .expect("third timeline fixture json should parse"); let first_page_nodes = match first_page.node.as_ref() { - | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { - pull_request - .timeline_items - .nodes - .as_ref() - .expect("first timeline fixture page should contain timeline nodes") - } - | _ => panic!("first timeline fixture page should resolve to a pull request node"), + | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request + .timeline_items + .nodes + .as_ref() + .expect("first timeline fixture page should contain timeline nodes"), + | _ => panic!("first timeline fixture page should resolve to a pull request node"), }; let second_page_nodes = match second_page.node.as_ref() { - | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { - pull_request - .timeline_items - .nodes - .as_ref() - .expect("second timeline fixture page should contain timeline nodes") - } - | _ => panic!("second timeline fixture page should resolve to a pull request node"), + | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request + .timeline_items + .nodes + .as_ref() + .expect("second timeline fixture page should contain timeline nodes"), + | _ => panic!("second timeline fixture page should resolve to a pull request node"), }; let third_page_nodes = match third_page.node.as_ref() { - | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { - pull_request - .timeline_items - .nodes - .as_ref() - .expect("third timeline fixture page should contain timeline nodes") - } - | _ => panic!("third timeline fixture page should resolve to a pull request node"), + | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request + .timeline_items + .nodes + .as_ref() + .expect("third timeline fixture page should contain timeline nodes"), + | _ => panic!("third timeline fixture page should resolve to a pull request node"), }; assert_eq!( diff --git a/src/api/repo.rs b/src/api/repo.rs index 7d57ce7..f84d1e7 100644 --- a/src/api/repo.rs +++ b/src/api/repo.rs @@ -1,11 +1,10 @@ -use futures::{FutureExt, TryFutureExt}; +use std::sync::Arc; + use reqwest::Method; use serde::Deserialize; -use tokio::sync::OwnedRwLockReadGuard; use crate::{ - api, - query::{self, Query, fetch_query}, + api, query, util::{self, file}, }; @@ -31,9 +30,9 @@ pub struct Owner { #[derive(Debug, Clone)] pub struct FileRef { - pub repo_slug: String, - pub path: String, - pub reff: Option, + pub repo_slug: Arc, + pub path: Arc, + pub reff: Option>, } #[derive(Clone)] @@ -67,9 +66,9 @@ impl query::QueryFn for List { #[derive(Clone)] pub struct FetchFileContent { - pub repo_slug: String, - pub path: String, - pub reff: Option, + pub repo_slug: Arc, + pub path: Arc, + pub reff: Option>, } impl query::QueryFn for FetchFileContent { @@ -79,8 +78,8 @@ impl query::QueryFn for FetchFileContent { fn key(&self) -> query::Key { match &self.reff { - | Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(), - | None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(), + | Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(), + | None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(), } } @@ -95,11 +94,11 @@ impl query::QueryFn for FetchFileContent { } let path = match &self.reff { - | Some(reff) => format!( - "/repos/{}/contents/{}?ref={}", - self.repo_slug, self.path, reff - ), - | None => format!("/repos/{}/contents/{}", self.repo_slug, self.path), + | Some(reff) => format!( + "/repos/{}/contents/{}?ref={}", + self.repo_slug, self.path, reff + ), + | None => format!("/repos/{}/contents/{}", self.repo_slug, self.path), }; let res = c @@ -137,8 +136,8 @@ impl query::QueryFn for FetchFileDiff { c: &::Context, ) -> Result, api::Error> { let path = match &r.reff { - | Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff), - | None => format!("/repos/{}/contents/{}", r.repo_slug, r.path), + | Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff), + | None => format!("/repos/{}/contents/{}", r.repo_slug, r.path), }; let res = c @@ -160,10 +159,10 @@ impl query::QueryFn for FetchFileDiff { let (old, new) = tokio::join!(fetch_content(&self.base, c), fetch_content(&self.head, c),); match (old, new) { - | (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)), - | _ => Err(api::Error::MalformedResponse( - "failed to fetch content".to_string(), - )), + | (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)), + | _ => Err(api::Error::MalformedResponse( + "failed to fetch content".to_string(), + )), } } } diff --git a/src/api/user.rs b/src/api/user.rs index a80496d..9d9b672 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,4 +1,4 @@ -use std::ops::Deref; +use std::{ops::Deref, sync::Arc}; use reqwest::Method; use serde::{Deserialize, Serialize}; @@ -12,18 +12,18 @@ pub struct Id(u64); #[derive(Debug, Deserialize)] pub struct User { - pub login: String, + pub login: Arc, pub id: Id, - pub avatar_url: String, - pub html_url: String, - pub name: Option, - pub email: Option, + pub avatar_url: Arc, + pub html_url: Arc, + pub name: Option>, + pub email: Option>, } #[derive(Debug, Deserialize)] pub(crate) struct Actor { - pub(crate) login: String, - pub(crate) avatar_url: String, + pub(crate) login: Arc, + pub(crate) avatar_url: Arc, } impl Deref for Id { diff --git a/src/component/markdown.rs b/src/component/markdown.rs index 49f3ce3..d6e0842 100644 --- a/src/component/markdown.rs +++ b/src/component/markdown.rs @@ -1,6 +1,9 @@ // markdown treesitter playground: https://ikatyang.github.io/tree-sitter-markdown/ -use std::{ops::Range, sync::LazyLock}; +use std::{ + ops::Range, + sync::{Arc, LazyLock}, +}; use gpui::{AppContext, ParentElement, Refineable, Styled, div, px, relative, rems}; @@ -86,7 +89,7 @@ const MARKDOWN_KIND_ID_TABLE_CELL: u16 = 235; const MARKDOWN_KIND_ID_TASK_LIST_ITEM_MARKER: u16 = 236; pub(crate) struct MarkdownText { - content: gpui::SharedString, + content: Arc, blocks: Vec, } @@ -100,10 +103,7 @@ enum ContentBlock { }, } -pub(crate) fn new( - content: gpui::SharedString, - cx: &mut gpui::Context, -) -> MarkdownText { +pub(crate) fn new(content: Arc, cx: &mut gpui::Context) -> MarkdownText { let mut view = MarkdownText { content, blocks: Vec::new(), @@ -115,20 +115,20 @@ pub(crate) fn new( impl Styled for ContentBlock { fn style(&mut self) -> &mut gpui::StyleRefinement { match self { - | ContentBlock::Text { style, .. } => style, + | ContentBlock::Text { style, .. } => style, } } } impl MarkdownText { fn on_create(&mut self, cx: &gpui::Context) { - let content = self.content.clone(); + let content = Arc::clone(&self.content); let t = cx.background_spawn(async move { let mut parser = tree_sitter::Parser::new(); parser .set_language(tree_sitter_markdown::language()) .expect("tree-sitter-markdown language should load"); - parser.parse(content.as_str(), None) + parser.parse(content.as_bytes(), None) }); cx.spawn(async |weak, cx| { @@ -179,56 +179,54 @@ impl MarkdownText { } match node.kind_id() { - | MARKDOWN_KIND_ID_EMPHASIS => { + | MARKDOWN_KIND_ID_EMPHASIS => { + highlights.push(( + node_range!(), + gpui::HighlightStyle { + font_style: Some(gpui::FontStyle::Italic), + ..Default::default() + }, + )); + } + | MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push(( + node_range!(), + gpui::HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + )), + + | MARKDOWN_KIND_ID_LINK => { + if cursor.goto_first_child() { highlights.push(( node_range!(), gpui::HighlightStyle { - font_style: Some(gpui::FontStyle::Italic), + color: Some(theme.colors.link.into()), + underline: Some(gpui::UnderlineStyle { + color: Some(theme.colors.link.into()), + thickness: px(1.), + wavy: false, + }), ..Default::default() }, )); - } - | MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push(( - node_range!(), - gpui::HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - )), - | MARKDOWN_KIND_ID_LINK => { - if cursor.goto_first_child() { - highlights.push(( - node_range!(), - gpui::HighlightStyle { - color: Some(theme.colors.link.into()), - underline: Some(gpui::UnderlineStyle { - color: Some(theme.colors.link.into()), - thickness: px(1.), - wavy: false, - }), - ..Default::default() - }, - )); - - if cursor.goto_next_sibling() - && let Ok(src) = cursor.node().utf8_text(content.as_bytes()) - { - links.push(( - node_range!(), - gpui::SharedString::from(String::from(src)), - )); - } else { - // the link src is invalid, use an empty string as a fallback - // link on click handler will ignore empty string - links.push((node_range!(), "".into())) - } + if cursor.goto_next_sibling() + && let Ok(src) = cursor.node().utf8_text(content.as_bytes()) + { + links + .push((node_range!(), gpui::SharedString::from(String::from(src)))); + } else { + // the link src is invalid, use an empty string as a fallback + // link on click handler will ignore empty string + links.push((node_range!(), "".into())) } } + } - | _ => { - // extend here to support more markdown node stylings - } + | _ => { + // extend here to support more markdown node stylings + } }; if !cursor.goto_next_sibling() { @@ -305,23 +303,23 @@ impl MarkdownText { let marker_content = &content[marker_node.byte_range()]; let list_marker_char = match marker_content { - // unordered list item - | "-" | "+" | "*" => Some("•".to_string()), + // unordered list item + | "-" | "+" | "*" => Some("•".to_string()), - | marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => { - let i = list_index.get_or_insert_with(|| { - marker_content - .strip_suffix('.') - .unwrap() - .parse::() - .unwrap() - }); - let j = *i; - *i = j + 1; - Some(format!("{j}.")) - } + | marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => { + let i = list_index.get_or_insert_with(|| { + marker_content + .strip_suffix('.') + .unwrap() + .parse::() + .unwrap() + }); + let j = *i; + *i = j + 1; + Some(format!("{j}.")) + } - | _ => None, + | _ => None, }; let Some(list_marker_char) = list_marker_char else { @@ -333,9 +331,9 @@ impl MarkdownText { let block = if cursor.goto_next_sibling() { let mut b = block_for_node(cursor, content, 0, theme); match b { - | ContentBlock::Text { - ref mut decoration, .. - } => *decoration = Some(list_marker_char.into()), + | ContentBlock::Text { + ref mut decoration, .. + } => *decoration = Some(list_marker_char.into()), } b } else { @@ -374,150 +372,150 @@ impl MarkdownText { } match current_node.kind_id() { - | MARKDOWN_KIND_ID_ATX_HEADING => { - if !cursor.goto_first_child() { - render_fallback_content(&cursor, &self.content, &mut self.blocks); - continue; - } - - let marker_node_kind = cursor.node().kind_id(); - - let block = if cursor.goto_next_sibling() - && cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT - { - // because HEADING_CONTENT node includes the space after the heading marker - // offset by 1 to exclude the space - block_for_node(&mut cursor, &self.content, 1, theme) - } else { - ContentBlock::Text { - decoration: None, - text: gpui::SharedString::new(&self.content[current_node.byte_range()]), - highlights: Vec::new(), - links: Vec::new(), - style: gpui::StyleRefinement::default(), - } - }; - - let mut block = match marker_node_kind { - | MARKDOWN_KIND_ID_ATX_H1_MARKER => block - .text_size(rems(2.25)) - .font_weight(gpui::FontWeight::EXTRA_BOLD) - .mb_6(), - | MARKDOWN_KIND_ID_ATX_H2_MARKER => block - .text_2xl() - .font_weight(gpui::FontWeight::BOLD) - .mt_12() - .mb_4(), - | MARKDOWN_KIND_ID_ATX_H3_MARKER => block - .text_xl() - .font_weight(gpui::FontWeight::SEMIBOLD) - .mt_8() - .mb_3(), - | MARKDOWN_KIND_ID_ATX_H4_MARKER => block - .text_base() - .font_weight(gpui::FontWeight::SEMIBOLD) - .mt_6() - .mb_2(), - | _ => block, - } - .text_color(theme.colors.text); - - if is_first_heading { - is_first_heading = false; - block = block.mt_0(); - } - - cursor.goto_parent(); - - self.blocks.push(block); + | MARKDOWN_KIND_ID_ATX_HEADING => { + if !cursor.goto_first_child() { + render_fallback_content(&cursor, &self.content, &mut self.blocks); + continue; } - | MARKDOWN_KIND_ID_PARAGRAPH => { - let block = block_for_node(&mut cursor, &self.content, 0, theme) - .text_color(theme.colors.text) - .text_sm(); + let marker_node_kind = cursor.node().kind_id(); - self.blocks.push(block); + let block = if cursor.goto_next_sibling() + && cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT + { + // because HEADING_CONTENT node includes the space after the heading marker + // offset by 1 to exclude the space + block_for_node(&mut cursor, &self.content, 1, theme) + } else { + ContentBlock::Text { + decoration: None, + text: gpui::SharedString::new(&self.content[current_node.byte_range()]), + highlights: Vec::new(), + links: Vec::new(), + style: gpui::StyleRefinement::default(), + } + }; + + let mut block = match marker_node_kind { + | MARKDOWN_KIND_ID_ATX_H1_MARKER => block + .text_size(rems(2.25)) + .font_weight(gpui::FontWeight::EXTRA_BOLD) + .mb_6(), + | MARKDOWN_KIND_ID_ATX_H2_MARKER => block + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .mt_12() + .mb_4(), + | MARKDOWN_KIND_ID_ATX_H3_MARKER => block + .text_xl() + .font_weight(gpui::FontWeight::SEMIBOLD) + .mt_8() + .mb_3(), + | MARKDOWN_KIND_ID_ATX_H4_MARKER => block + .text_base() + .font_weight(gpui::FontWeight::SEMIBOLD) + .mt_6() + .mb_2(), + | _ => block, + } + .text_color(theme.colors.text); + + if is_first_heading { + is_first_heading = false; + block = block.mt_0(); } - | MARKDOWN_KIND_ID_TIGHT_LIST => { - let is_rendered = - render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0); - if !is_rendered { - continue; - } + cursor.goto_parent(); + + self.blocks.push(block); + } + + | MARKDOWN_KIND_ID_PARAGRAPH => { + let block = block_for_node(&mut cursor, &self.content, 0, theme) + .text_color(theme.colors.text) + .text_sm(); + + self.blocks.push(block); + } + + | MARKDOWN_KIND_ID_TIGHT_LIST => { + let is_rendered = + render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0); + if !is_rendered { + continue; + } + } + + | MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => { + // expected tree shape: + // fenced_code_block + // ├── info_string? (present if there is a language annotation) + // └── code_fence_content? (present if there is some content between the backticks) + + if !cursor.goto_first_child() { + render_fallback_content(&cursor, &self.content, &mut self.blocks); + continue; } - | MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => { - // expected tree shape: - // fenced_code_block - // ├── info_string? (present if there is a language annotation) - // └── code_fence_content? (present if there is some content between the backticks) - - if !cursor.goto_first_child() { - render_fallback_content(&cursor, &self.content, &mut self.blocks); - continue; - } - - let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING { - // skipping info string (which annotates the code block) - if cursor.goto_next_sibling() { - // this is code_fence_content node - gpui::SharedString::new( - cursor - .node() - .utf8_text(self.content.as_bytes()) - .unwrap_or_default(), - ) - } else { - gpui::SharedString::default() - } - } else { - // assuming the current node is already code_fence_content + let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING { + // skipping info string (which annotates the code block) + if cursor.goto_next_sibling() { + // this is code_fence_content node gpui::SharedString::new( cursor .node() .utf8_text(self.content.as_bytes()) .unwrap_or_default(), ) - }; - - cursor.goto_parent(); - - let block = ContentBlock::Text { - decoration: None, - text: content, - highlights: Vec::new(), - links: Vec::new(), - style: gpui::StyleRefinement::default(), + } else { + gpui::SharedString::default() } - .text_sm() + } else { + // assuming the current node is already code_fence_content + gpui::SharedString::new( + cursor + .node() + .utf8_text(self.content.as_bytes()) + .unwrap_or_default(), + ) + }; + + cursor.goto_parent(); + + let block = ContentBlock::Text { + decoration: None, + text: content, + highlights: Vec::new(), + links: Vec::new(), + style: gpui::StyleRefinement::default(), + } + .text_sm() + .text_color(theme.colors.text) + .line_height(relative(1.2)) + .font_family("Menlo") + .px_3() + .py_2() + .rounded_sm() + .bg(theme.colors.code_bg) + .border_1() + .my_4() + .border_color(theme.colors.code_border); + + self.blocks.push(block); + } + + | _ => { + println!( + "[WARN] formatting not implemenetd for node type {:?}", + current_node.kind() + ); + + let block = block_for_node(&mut cursor, &self.content, 0, theme) .text_color(theme.colors.text) - .line_height(relative(1.2)) - .font_family("Menlo") - .px_3() - .py_2() - .rounded_sm() - .bg(theme.colors.code_bg) - .border_1() - .my_4() - .border_color(theme.colors.code_border); + .text_sm(); - self.blocks.push(block); - } - - | _ => { - println!( - "[WARN] formatting not implemenetd for node type {:?}", - current_node.kind() - ); - - let block = block_for_node(&mut cursor, &self.content, 0, theme) - .text_color(theme.colors.text) - .text_sm(); - - self.blocks.push(block); - } + self.blocks.push(block); + } } if !cursor.goto_next_sibling() { @@ -535,55 +533,55 @@ impl gpui::Render for MarkdownText { ) -> impl gpui::prelude::IntoElement { let children = self.blocks.iter().enumerate().map(|(i, block)| { match block { - | ContentBlock::Text { - decoration, - text, - highlights, - links, - style, - } => { - let styled_text = - gpui::StyledText::new(text.clone()).with_highlights(highlights.clone()); + | ContentBlock::Text { + decoration, + text, + highlights, + links, + style, + } => { + let styled_text = + gpui::StyledText::new(text.clone()).with_highlights(highlights.clone()); - let content = if links.is_empty() { - div().w_full().child(styled_text) - } else { - // if link in block, interactive text is needed - // to handle link clicks - let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip(); + let content = if links.is_empty() { + div().w_full().child(styled_text) + } else { + // if link in block, interactive text is needed + // to handle link clicks + let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip(); - let weak = cx.entity(); - let t = gpui::InteractiveText::new(i, styled_text).on_click( - link_ranges, - move |i, _, cx| { - if let Some(src) = srcs.get(i) { - weak.update(cx, |this, cx| { - this.on_open_link(src, cx); - cx.notify(); - }) - } - }, - ); + let weak = cx.entity(); + let t = gpui::InteractiveText::new(i, styled_text).on_click( + link_ranges, + move |i, _, cx| { + if let Some(src) = srcs.get(i) { + weak.update(cx, |this, cx| { + this.on_open_link(src, cx); + cx.notify(); + }) + } + }, + ); - div().w_full().child(t) - }; + div().w_full().child(t) + }; - let mut div = match decoration { - | Some(d) => div() - .w_full() - .flex() - .flex_row() - .gap_2() - .items_start() - .child(d.clone()) - .child(div().flex_1().min_w_0().child(content)), - | None => div().w_full().child(content), - }; + let mut div = match decoration { + | Some(d) => div() + .w_full() + .flex() + .flex_row() + .gap_2() + .items_start() + .child(d.clone()) + .child(div().flex_1().min_w_0().child(content)), + | None => div().w_full().child(content), + }; - div.style().refine(&style); + div.style().refine(&style); - div - } + div + } } }); diff --git a/src/screen/dashboard/issue_list.rs b/src/screen/dashboard/issue_list.rs index 646e82d..530c561 100644 --- a/src/screen/dashboard/issue_list.rs +++ b/src/screen/dashboard/issue_list.rs @@ -1,5 +1,3 @@ -use std::ops::Deref; - use gpui::{ InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, list, point, prelude::FluentBuilder, px, @@ -13,6 +11,7 @@ use crate::{ text::text, }, query::{self, QueryStatus, read_query, use_query}, + util::str::ToSharedString, }; pub(crate) struct IssueList { @@ -20,7 +19,6 @@ pub(crate) struct IssueList { list_state: gpui::ListState, list_items: Vec, - selected_item: Option<(usize, gpui::SharedString)>, } pub(crate) enum Event { @@ -29,7 +27,7 @@ pub(crate) enum Event { #[derive(gpui::IntoElement, Clone)] pub(crate) struct IssueListItem { - id: gpui::SharedString, + id: api::issues::Id, repo_name: Option, title: gpui::SharedString, description: Option, @@ -51,7 +49,6 @@ pub(crate) fn new(cx: &mut gpui::Context) -> IssueList { list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)), list_items: Vec::new(), - selected_item: None, }; list.on_create(cx); list @@ -66,16 +63,12 @@ impl IssueList { let new_len = res.items.len(); let new_items = res.items.iter().enumerate().map(|(i, it)| IssueListItem { - id: gpui::SharedString::from(it.id.deref()), - repo_name: Some(gpui::SharedString::new(it.repo_slug.as_str())), - title: gpui::SharedString::new(it.title.as_str()), + id: it.id.clone(), + repo_name: Some(it.repo_slug.to_shared_string()), + title: it.title.to_shared_string(), description: None, status: it.state, - is_selected: this - .selected_item - .as_ref() - .map(|(_, id)| id.as_str() == it.id.as_str()) - .unwrap_or(false), + is_selected: false, is_last: i == new_len - 1, is_draft: it.is_draft, }); @@ -95,7 +88,7 @@ impl IssueList { item.is_selected = i == j; } cx.notify(); - cx.emit(Event::ItemSelected(item_id.as_str().into())); + cx.emit(Event::ItemSelected(item_id)); } } @@ -148,8 +141,8 @@ impl gpui::RenderOnce for IssueListItem { } let repo_name_text = match self.repo_name { - | Some(name) => text(name), - | None => text("Unknown repo"), + | Some(name) => text(name), + | None => text("Unknown repo"), } .text_xs() .opacity(0.5); @@ -162,21 +155,21 @@ impl gpui::RenderOnce for IssueListItem { .bg(theme.colors.surface) } else { match self.status { - | api::issues::PullRequestState::Closed => pill( - text("Closed").text_color(theme.colors.danger_on_solid), - font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger_on_solid), - ) - .bg(theme.colors.danger_solid), - | api::issues::PullRequestState::Merged => pill( - text("Merged").text_color(theme.colors.accent_on_solid), - font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.accent_on_solid), - ) - .bg(theme.colors.accent_solid), - | _ => pill( - text("Open").text_color(theme.colors.success_on_solid), - font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success_on_solid), - ) - .bg(theme.colors.success_solid), + | api::issues::PullRequestState::Closed => pill( + text("Closed").text_color(theme.colors.danger_on_solid), + font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger_on_solid), + ) + .bg(theme.colors.danger_solid), + | api::issues::PullRequestState::Merged => pill( + text("Merged").text_color(theme.colors.accent_on_solid), + font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.accent_on_solid), + ) + .bg(theme.colors.accent_solid), + | _ => pill( + text("Open").text_color(theme.colors.success_on_solid), + font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success_on_solid), + ) + .bg(theme.colors.success_solid), } }; diff --git a/src/screen/dashboard/pull_request_view.rs b/src/screen/dashboard/pull_request_view.rs index 306a793..f64c637 100644 --- a/src/screen/dashboard/pull_request_view.rs +++ b/src/screen/dashboard/pull_request_view.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use gpui::{ AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, img, prelude::FluentBuilder, @@ -65,7 +67,7 @@ impl PullRequestView { let maybe_content = { let data = read_query(&query, cx); if let QueryStatus::Loaded(pr) = data { - Some(gpui::SharedString::new(pr.body.as_str())) + Some(Arc::clone(&pr.body)) } else { None } @@ -130,8 +132,8 @@ impl PullRequestView { } let merge_text = pr.author.as_ref().map(|author| { - let base_branch = pr.base_branch_name.as_str(); - let head_branch = pr.head_branch_name.as_str(); + let base_branch = &pr.base_branch_name; + let head_branch = &pr.head_branch_name; let str = format!( "{} requested to merge {} into {}", author.login, head_branch, base_branch @@ -172,6 +174,8 @@ impl PullRequestView { ) }); + let pr_title = gpui::SharedString::new(Arc::clone(&pr.title)); + let metadata_line = div() .flex() @@ -184,7 +188,7 @@ impl PullRequestView { .flex_row() .items_center() .gap_1p5() - .child(img(author.avatar_url.clone()).size_4().rounded_full()) + .child(img(author.avatar_url.as_ref()).size_4().rounded_full()) .child( div() .min_w_0() @@ -222,7 +226,7 @@ impl PullRequestView { .flex() .flex_col() .items_start() - .child(text(pr.title.clone()).w_full().text_xl().mb_1()) + .child(text(pr_title).w_full().text_xl().mb_1()) .child(metadata_line), ) .child(div().flex().flex_col().items_end().gap_1().when_some( diff --git a/src/screen/setup_wizard/github_step.rs b/src/screen/setup_wizard/github_step.rs index 8dee29d..2f2e838 100644 --- a/src/screen/setup_wizard/github_step.rs +++ b/src/screen/setup_wizard/github_step.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use futures_lite::StreamExt; use gpui::{ @@ -16,7 +16,7 @@ use crate::{ }, query::{self, QueryStatus, fetch_query, read_query, use_query}, storage, - util::timeout::set_timeout, + util::{str::ToSharedString, timeout::set_timeout}, }; pub(crate) struct GithubStepView { @@ -83,7 +83,7 @@ impl GithubStepView { _ = weak.update(cx, |this, cx| { this.has_opened_link = true; this.is_opening_link = false; - this.begin_auth_flow(&device_code, cx); + this.begin_auth_flow(device_code, cx); cx.notify(); }); }, @@ -110,7 +110,7 @@ impl GithubStepView { timer.clear(); } else { let _ = this.update(cx, |this, cx| { - this.placeholder_code = this.generate_random_code(cx); + this.generate_random_code(cx); cx.notify(); }); } @@ -127,14 +127,13 @@ impl GithubStepView { cx.open_url(api::auth::DEVICE_LOGIN_FLOW_URL); } - fn generate_random_code(&mut self, cx: &mut gpui::Context) -> String { + fn generate_random_code(&mut self, cx: &mut gpui::Context) { let rng = app::rng(cx); - (0..8) - .map(|_| { - let idx = rng.random_range(0..Self::CHAR_POOL.len()); - Self::CHAR_POOL.chars().nth(idx).unwrap() - }) - .collect() + self.placeholder_code.clear(); + self.placeholder_code.extend((0..8).map(|_| { + let idx = rng.random_range(0..Self::CHAR_POOL.len()); + Self::CHAR_POOL.chars().nth(idx).unwrap() + })); } fn copy_user_code(&mut self, code: &str, cx: &mut gpui::Context) { @@ -155,15 +154,10 @@ impl GithubStepView { cx.notify(); } - fn begin_auth_flow(&mut self, device_code: &str, cx: &mut gpui::Context) { + fn begin_auth_flow(&mut self, device_code: Arc, cx: &mut gpui::Context) { GithubStepView::open_github_auth_page(cx); - let query = use_query( - api::auth::RequestAccessToken { - device_code: device_code.to_owned(), - }, - cx, - ); + let query = use_query(api::auth::RequestAccessToken { device_code }, cx); cx.observe(&query, |this, _, cx| { this.handle_access_token_query_response(cx); @@ -189,60 +183,60 @@ impl GithubStepView { let poll_interval = u64::from(*interval); match read_query(query, cx) { - | QueryStatus::Loaded(data) => { - let auth_tokens = api::AuthTokens { - access_token: data.access_token.clone(), - }; + | QueryStatus::Loaded(data) => { + let auth_tokens = api::AuthTokens { + access_token: data.access_token.clone(), + }; - cx.update_global::, _>(|store, _| { - store.update_query_context(|c| { - c.auth = Some(auth_tokens.clone()); - }); + cx.update_global::, _>(|store, _| { + store.update_query_context(|c| { + c.auth = Some(auth_tokens.clone()); }); + }); - self.user_query = Some(use_query(api::user::Fetch, cx)); + self.user_query = Some(use_query(api::user::Fetch, cx)); + cx.spawn(async move |weak, cx| { + let ent = fetch_query(api::user::Fetch, cx).await; + + let fut = weak + .update(cx, move |_this, cx| { + let Ok(query) = ent else { + return None; + }; + let QueryStatus::Loaded(user) = read_query(&query, cx) else { + return None; + }; + Some(storage::store_auth_tokens(&auth_tokens, user, cx)) + }) + .unwrap_or_default(); + + _ = if let Some(task) = fut { + task.await + } else { + Err(anyhow::Error::msg("")) + }; + }) + .detach(); + } + + | QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => { + if error == "authorization_pending" { cx.spawn(async move |weak, cx| { - let ent = fetch_query(api::user::Fetch, cx).await; - - let fut = weak - .update(cx, move |_this, cx| { - let Ok(query) = ent else { - return None; - }; - let QueryStatus::Loaded(user) = read_query(&query, cx) else { - return None; - }; - Some(storage::store_auth_tokens(&auth_tokens, user, cx)) - }) - .unwrap_or_default(); - - _ = if let Some(task) = fut { - task.await - } else { - Err(anyhow::Error::msg("")) - }; + Timer::after(Duration::from_secs(poll_interval)).await; + if let Ok(Some(query)) = + weak.read_with(cx, |this, _cx| this.request_access_token_query.clone()) + { + let _ = weak.update(cx, |_this, cx| { + query.refetch(cx); + }); + } }) .detach(); } + } - | QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => { - if error == "authorization_pending" { - cx.spawn(async move |weak, cx| { - Timer::after(Duration::from_secs(poll_interval)).await; - if let Ok(Some(query)) = - weak.read_with(cx, |this, _cx| this.request_access_token_query.clone()) - { - let _ = weak.update(cx, |_this, cx| { - query.refetch(cx); - }); - } - }) - .detach(); - } - } - - | _ => {} + | _ => {} } } @@ -263,8 +257,8 @@ impl GithubStepView { let theme = app::current_theme(cx); let (displayed_code, copyable_code) = match create_device_code_query { - | QueryStatus::Loaded(data) => (data.user_code.as_str(), Some(data.user_code.clone())), - | _ => (self.placeholder_code.as_str(), None), + | QueryStatus::Loaded(data) => (data.user_code.as_ref(), Some(data.user_code.clone())), + | _ => (self.placeholder_code.as_str(), None), }; let border_color = theme.colors.border.clone(); @@ -358,16 +352,14 @@ impl gpui::Render for GithubStepView { cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let (can_go_next, header, body) = match self.user_query { - | None => (false, self.header(), self.device_code_area(cx)), - | Some(ref q) => { - let user_query = read_query(q, cx); - match user_query { - | QueryStatus::Loaded(user) => { - (true, connected_header(), connected_body(user, cx)) - } - | _ => (false, self.header(), self.device_code_area(cx)), - } + | None => (false, self.header(), self.device_code_area(cx)), + | Some(ref q) => { + let user_query = read_query(q, cx); + match user_query { + | QueryStatus::Loaded(user) => (true, connected_header(), connected_body(user, cx)), + | _ => (false, self.header(), self.device_code_area(cx)), } + } }; div() @@ -417,7 +409,7 @@ fn connected_header() -> gpui::Div { fn connected_body(user: &api::user::User, cx: &gpui::Context) -> gpui::Div { let theme = app::current_theme(cx); - let display_name = user.name.as_deref().unwrap_or(&user.login).to_owned(); + let display_name = user.name.as_ref().unwrap_or(&user.login).to_shared_string(); div() .flex() @@ -444,7 +436,7 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context) -> .flex_row() .gap_4() .items_center() - .child(img(user.avatar_url.clone()).size_12().rounded_full()) + .child(img(user.avatar_url.as_ref()).size_12().rounded_full()) .child( div() .flex() @@ -455,7 +447,7 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context) -> .text_xl() .leading_tight(), ) - .child(text(user.login.clone()).text_sm().opacity(0.5)), + .child(text(user.login.to_shared_string()).text_sm().opacity(0.5)), ), ) .child( diff --git a/src/util/file.rs b/src/util/file.rs index fa95775..c028939 100644 --- a/src/util/file.rs +++ b/src/util/file.rs @@ -20,10 +20,10 @@ pub(crate) struct LineDiff { pub(crate) fn classify_content(content: &[u8]) -> ContentType { if content.is_empty() { ContentType::Text - } else if content.starts_with(&[0xEF, 0xBB, 0xBF]) // UTF-8 + } else if content.starts_with(&[0xEF, 0xBB, 0xBF]) // UTF-8 || content.starts_with(&[0x00, 0x00, 0xFE, 0xFF]) // UTF-32 BE || content.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) // UTF-32 LE - || content.starts_with(&[0xFE, 0xFF]) // UTF-16 BE + || content.starts_with(&[0xFE, 0xFF]) // UTF-16 BE || content.starts_with(&[0xFF, 0xFE]) { ContentType::Text @@ -34,15 +34,3 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType { } } } - -pub(crate) fn diff_content(old: &[u8], new: &[u8]) -> ContentDiff { - similar::TextDiff::from_lines::<[u8]>(old, new) - .iter_all_changes() - .map(|change| LineDiff { - old_line: change.old_index(), - old_content_range: change.old_range, - new_line: change.new_index(), - new_content_range: change.new_range, - }) - .collect() -} diff --git a/src/util/mod.rs b/src/util/mod.rs index 5a5b402..e7b204b 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod diff; pub(crate) mod file; +pub(crate) mod str; pub(crate) mod timeout; diff --git a/src/util/str.rs b/src/util/str.rs new file mode 100644 index 0000000..3391892 --- /dev/null +++ b/src/util/str.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use crate::api; + +pub(crate) trait ToSharedString { + fn to_shared_string(&self) -> gpui::SharedString; +} + +impl ToSharedString for Arc { + /// converts into gpui SharedString cheaply with no allocation involved. + fn to_shared_string(&self) -> gpui::SharedString { + gpui::SharedString::new(Arc::clone(self)) + } +} + +impl Into for api::issues::Id { + fn into(self) -> gpui::ElementId { + gpui::ElementId::Name(gpui::SharedString::new(Arc::clone(&self.0))) + } +}