refactor: prefer Arc<str> to String

This commit is contained in:
2026-05-23 18:45:44 +01:00
parent 1ef91cb41e
commit 1843622540
15 changed files with 524 additions and 544 deletions

View File

@@ -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) { if let Some(id) = parse_pull_request_file_tree_fixture_name(&file_name) {
let value = read_fixture_value(&entry.path()); let value = read_fixture_value(&entry.path());
pull_request_file_tree_fixtures pull_request_file_tree_fixtures.insert(
.insert(id, read_pull_request_file_tree_fixture(&value, &entry.path())); id,
read_pull_request_file_tree_fixture(&value, &entry.path()),
);
continue; continue;
} }
@@ -232,12 +234,12 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
output.push_str(&string_literal(&path)); output.push_str(&string_literal(&path));
output.push_str(", "); output.push_str(", ");
match reff { match reff {
| Some(reff) => { | Some(reff) => {
output.push_str("Some("); output.push_str("Some(");
output.push_str(&string_literal(&reff)); output.push_str(&string_literal(&reff));
output.push(')'); output.push(')');
} }
| None => output.push_str("None"), | None => output.push_str("None"),
} }
output.push_str(") => Some("); output.push_str(") => Some(");
output.push_str(&string_literal(&content)); 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(&string_literal(&id));
output.push_str(", "); output.push_str(", ");
match previous_end_cursor.as_deref() { match previous_end_cursor.as_deref() {
| Some(after) => output.push_str(&format!("Some({})", string_literal(after))), | Some(after) => output.push_str(&format!("Some({})", string_literal(after))),
| None => output.push_str("None"), | None => output.push_str("None"),
} }
output.push_str(") => Some("); output.push_str(") => Some(");
output.push_str(&string_literal(&fixture.json)); 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"]) { match required_string(issue, &["state"]) {
| "open" => "OPEN", | "open" => "OPEN",
| "closed" => "CLOSED", | "closed" => "CLOSED",
| state => panic!("unsupported pull request state in fixture: {state}"), | 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 owner = parts[0].clone();
let repo = parts[1].clone(); let repo = parts[1].clone();
let reff = match parts[2].as_str() { let reff = match parts[2].as_str() {
| "@default" => None, | "@default" => None,
| value => Some(value.to_owned()), | value => Some(value.to_owned()),
}; };
let virtual_path = parts[3..].join("/"); let virtual_path = parts[3..].join("/");
let content = fs::read_to_string(&path).unwrap_or_else(|err| { let content = fs::read_to_string(&path).unwrap_or_else(|err| {

View File

@@ -8,9 +8,9 @@ fn main() {
for change in diff.iter_all_changes() { for change in diff.iter_all_changes() {
let sign = match change.tag() { let sign = match change.tag() {
ChangeTag::Delete => "-", | ChangeTag::Delete => "-",
ChangeTag::Insert => "+", | ChangeTag::Insert => "+",
ChangeTag::Equal => " ", | ChangeTag::Equal => " ",
}; };
print!("{}{}", sign, change); print!("{}{}", sign, change);
} }

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -22,7 +24,7 @@ pub struct QueryContext {
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub(crate) struct AuthTokens { pub(crate) struct AuthTokens {
pub(crate) access_token: String, pub(crate) access_token: Arc<str>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -111,9 +113,9 @@ async fn raw_content(res: reqwest::Response) -> Result<bytes::Bytes, Error> {
Ok(res.bytes().await?) Ok(res.bytes().await?)
} else { } else {
match res.status() { match res.status() {
| reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist), | reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist),
| reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed), | reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed),
| _ => Err(Error::MalformedResponse(res.status().to_string())), | _ => Err(Error::MalformedResponse(res.status().to_string())),
} }
} }
} }
@@ -150,7 +152,7 @@ where
{ {
let mut body: graphql_client::Response<T> = res.json().await?; let mut body: graphql_client::Response<T> = res.json().await?;
match body.data.take() { match body.data.take() {
| None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
| Some(data) => Ok((body, data)), | Some(data) => Ok((body, data)),
} }
} }

View File

@@ -1,4 +1,4 @@
use std::collections::HashMap; use std::{collections::HashMap, sync::Arc};
use serde::Deserialize; use serde::Deserialize;
@@ -11,9 +11,9 @@ pub struct CreateDeviceCode;
#[derive(Deserialize)] #[derive(Deserialize)]
pub(crate) struct DeviceCodeResponse { pub(crate) struct DeviceCodeResponse {
pub device_code: String, pub device_code: Arc<str>,
pub user_code: String, pub user_code: Arc<str>,
pub vertification_uri: Option<String>, pub vertification_uri: Option<Arc<str>>,
pub expires_in: u16, pub expires_in: u16,
// minimum number of seconds between polling for access token // minimum number of seconds between polling for access token
@@ -46,14 +46,14 @@ impl query::QueryFn for CreateDeviceCode {
#[derive(Clone)] #[derive(Clone)]
pub struct RequestAccessToken { pub struct RequestAccessToken {
pub device_code: String, pub device_code: Arc<str>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct RequestAccessTokenResponse { pub struct RequestAccessTokenResponse {
pub access_token: String, pub access_token: Arc<str>,
pub token_type: String, pub token_type: Arc<str>,
pub scope: String, pub scope: Arc<str>,
} }
impl query::QueryFn for RequestAccessToken { impl query::QueryFn for RequestAccessToken {

View File

@@ -1,4 +1,4 @@
use std::ops::Deref; use std::sync::Arc;
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use serde::Deserialize; use serde::Deserialize;
@@ -21,33 +21,7 @@ type GitObjectID = String;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct Id(String); pub(crate) struct Id(pub(crate) Arc<str>);
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<String> for Id {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<Id> for String {
fn from(value: Id) -> Self {
value.0
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct PullRequestPaginatedResponse { pub(crate) struct PullRequestPaginatedResponse {
@@ -59,32 +33,32 @@ pub(crate) struct PullRequestPaginatedResponse {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct PullRequest { pub(crate) struct PullRequest {
pub(crate) id: Id, pub(crate) id: Id,
pub(crate) title: String, pub(crate) title: Arc<str>,
pub(crate) state: PullRequestState, pub(crate) state: PullRequestState,
pub(crate) is_draft: bool, pub(crate) is_draft: bool,
pub(crate) repo_slug: String, pub(crate) repo_slug: Arc<str>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct DetailedPullRequest { pub(crate) struct DetailedPullRequest {
pub(crate) title: String, pub(crate) title: Arc<str>,
pub(crate) state: PullRequestState, pub(crate) state: PullRequestState,
pub(crate) is_draft: bool, pub(crate) is_draft: bool,
pub(crate) body: String, pub(crate) body: Arc<str>,
pub(crate) created_at: Option<chrono::DateTime<chrono::FixedOffset>>, pub(crate) created_at: Option<chrono::DateTime<chrono::FixedOffset>>,
pub(crate) author: Option<super::user::Actor>, pub(crate) author: Option<super::user::Actor>,
pub(crate) base_branch_name: String, pub(crate) base_branch_name: Arc<str>,
pub(crate) base_repo_slug: String, pub(crate) base_repo_slug: Arc<str>,
pub(crate) base_ref: String, pub(crate) base_ref: Arc<str>,
pub(crate) head_branch_name: String, pub(crate) head_branch_name: Arc<str>,
pub(crate) head_ref: String, pub(crate) head_ref: Arc<str>,
pub(crate) head_repo_slug: String, pub(crate) head_repo_slug: Arc<str>,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PullRequestTimeline { pub(crate) struct PullRequestTimeline {
pub(crate) items: Vec<PullRequestTimelineItem>, pub(crate) items: Vec<PullRequestTimelineItem>,
pub(crate) end_cursor: Option<String>, pub(crate) end_cursor: Option<Arc<str>>,
pub(crate) has_next_page: bool, pub(crate) has_next_page: bool,
} }
@@ -245,12 +219,6 @@ pub(crate) struct TimelineActor {
pub(crate) avatar_url: Option<String>, pub(crate) avatar_url: Option<String>,
} }
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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum PullRequestState { pub(crate) enum PullRequestState {
@@ -310,6 +278,24 @@ pub(crate) struct ListPullRequests {
pub page: u32, 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<String> 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 { impl query::QueryFn for ListPullRequests {
type Data = PullRequestPaginatedResponse; type Data = PullRequestPaginatedResponse;
type Error = api::Error; type Error = api::Error;
@@ -357,13 +343,14 @@ impl query::QueryFn for ListPullRequests {
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => { | PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
Some(PullRequest { Some(PullRequest {
id: p.id.into(), id: p.id.into(),
title: p.title, title: p.title.into(),
state: p.state, state: p.state,
is_draft: p.is_draft, is_draft: p.is_draft,
repo_slug: format!( repo_slug: format!(
"{}/{}", "{}/{}",
p.repository.owner.login, p.repository.name p.repository.owner.login, p.repository.name
), )
.into(),
}) })
} }
| _ => None, | _ => None,
@@ -399,7 +386,7 @@ impl query::QueryFn for FetchPullRequest {
} }
let gql = PullRequestQuery::build_query(pull_request_query::Variables { 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?; let res = c.github_graphql_request(&gql)?.send().await?;
@@ -421,26 +408,26 @@ impl query::QueryFn for FetchPullRequest {
})?; })?;
Ok(DetailedPullRequest { Ok(DetailedPullRequest {
title: p.title, title: p.title.into(),
state: p.state, state: p.state,
is_draft: p.is_draft, is_draft: p.is_draft,
body: p.body, body: p.body.into(),
author: p.author.map(|it| api::user::Actor { author: p.author.map(|it| api::user::Actor {
login: it.login, login: it.login.into(),
avatar_url: it.avatar_url, avatar_url: it.avatar_url.into(),
}), }),
base_repo_slug: p base_repo_slug: p
.base_repository .base_repository
.map(|it| it.name_with_owner) .map(|it| it.name_with_owner.into())
.unwrap_or_default(), .unwrap_or_default(),
base_branch_name: p.base_ref_name, base_branch_name: p.base_ref_name.into(),
base_ref: p.base_ref_oid, base_ref: p.base_ref_oid.into(),
head_repo_slug: p head_repo_slug: p
.head_repository .head_repository
.map(|it| it.name_with_owner) .map(|it| it.name_with_owner.into())
.unwrap_or_default(), .unwrap_or_default(),
head_branch_name: p.head_ref_name, head_branch_name: p.head_ref_name.into(),
head_ref: p.head_ref_oid, head_ref: p.head_ref_oid.into(),
created_at: Some(created_at), created_at: Some(created_at),
}) })
} }
@@ -473,7 +460,7 @@ impl query::QueryFn for FetchPullRequestFileTree {
} else { } else {
let gql = let gql =
PullRequestFileTreeQuery::build_query(pull_request_file_tree_query::Variables { PullRequestFileTreeQuery::build_query(pull_request_file_tree_query::Variables {
id: self.id.clone().into(), id: self.id.to_string(),
first: self.first, first: self.first,
}); });
@@ -558,7 +545,7 @@ impl query::QueryFn for FetchPullRequestFileTree {
pub(crate) struct FetchPullRequestTimeline { pub(crate) struct FetchPullRequestTimeline {
pub(crate) id: Id, pub(crate) id: Id,
pub(crate) first: i64, pub(crate) first: i64,
pub(crate) after: Option<String>, pub(crate) after: Option<Arc<str>>,
} }
impl query::QueryFn for FetchPullRequestTimeline { impl query::QueryFn for FetchPullRequestTimeline {
@@ -841,9 +828,9 @@ impl query::QueryFn for FetchPullRequestTimeline {
} else { } else {
let gql = let gql =
PullRequestTimelineQuery::build_query(pull_request_timeline_query::Variables { PullRequestTimelineQuery::build_query(pull_request_timeline_query::Variables {
id: self.id.clone().into(), id: self.id.to_string(),
first: self.first, 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?; let res = c.github_graphql_request(&gql)?.send().await?;
@@ -892,7 +879,7 @@ impl query::QueryFn for FetchPullRequestTimeline {
Ok(PullRequestTimeline { Ok(PullRequestTimeline {
items, 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, has_next_page: timeline.page_info.has_next_page,
}) })
} }

View File

@@ -175,12 +175,12 @@ mod tests {
assert_eq!(merged.state, issues::PullRequestState::Merged); assert_eq!(merged.state, issues::PullRequestState::Merged);
assert!(merged.body.contains("| Stage | Owner | Status |")); assert!(merged.body.contains("| Stage | Owner | Status |"));
assert_eq!( assert_eq!(
merged.author.as_ref().map(|author| author.login.as_str()), merged.author.as_ref().map(|author| author.login.as_ref()),
Some("rorycraft") Some("rorycraft")
); );
assert_eq!(merged.base_branch_name.as_str(), "main"); assert_eq!(merged.base_branch_name.as_ref(), "main");
assert_eq!( assert_eq!(
merged.head_branch_name.as_str(), merged.head_branch_name.as_ref(),
"feat/release-handoff-checklist" "feat/release-handoff-checklist"
); );
assert_eq!( assert_eq!(
@@ -196,12 +196,12 @@ mod tests {
documented_failover documented_failover
.author .author
.as_ref() .as_ref()
.map(|author| author.login.as_str()), .map(|author| author.login.as_ref()),
Some("kennethnym") Some("kennethnym")
); );
assert_eq!(documented_failover.base_branch_name.as_str(), "main"); assert_eq!(documented_failover.base_branch_name.as_ref(), "main");
assert_eq!( assert_eq!(
documented_failover.head_branch_name.as_str(), documented_failover.head_branch_name.as_ref(),
"docs/manual-failover-steps" "docs/manual-failover-steps"
); );
assert_eq!( assert_eq!(
@@ -209,17 +209,17 @@ mod tests {
Some(chrono::DateTime::parse_from_rfc3339("2026-04-24T06:40:00Z").unwrap()) Some(chrono::DateTime::parse_from_rfc3339("2026-04-24T06:40:00Z").unwrap())
); );
assert!(dashboard_markdown.body.contains("```rust")); 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!( assert_eq!(
dashboard_markdown.head_branch_name.as_str(), dashboard_markdown.head_branch_name.as_ref(),
"feat/cached-issue-pane" "feat/cached-issue-pane"
); );
assert_eq!( assert_eq!(
dashboard_markdown.base_ref.as_str(), dashboard_markdown.base_ref.as_ref(),
"5e8745bfcc0c90c226d3c6af84226d6d4a5ec2d1" "5e8745bfcc0c90c226d3c6af84226d6d4a5ec2d1"
); );
assert_eq!( assert_eq!(
dashboard_markdown.head_ref.as_str(), dashboard_markdown.head_ref.as_ref(),
"2bc41de7731b9ef48f7d64ee9f0d5f497dbe0a51" "2bc41de7731b9ef48f7d64ee9f0d5f497dbe0a51"
); );
assert_eq!( assert_eq!(
@@ -230,20 +230,20 @@ mod tests {
cached_repo_picker cached_repo_picker
.author .author
.as_ref() .as_ref()
.map(|author| author.login.as_str()), .map(|author| author.login.as_ref()),
Some("kennethnym") 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!( assert_eq!(
cached_repo_picker.head_branch_name.as_str(), cached_repo_picker.head_branch_name.as_ref(),
"feat/cached-repo-picker" "feat/cached-repo-picker"
); );
assert_eq!( assert_eq!(
cached_repo_picker.base_ref.as_str(), cached_repo_picker.base_ref.as_ref(),
"5e8745bfcc0c90c226d3c6af84226d6d4a5ec2d1" "5e8745bfcc0c90c226d3c6af84226d6d4a5ec2d1"
); );
assert_eq!( assert_eq!(
cached_repo_picker.head_ref.as_str(), cached_repo_picker.head_ref.as_ref(),
"13af7d0b48a6ce0b22d48c9b6c1c78dfcd94e6a0" "13af7d0b48a6ce0b22d48c9b6c1c78dfcd94e6a0"
); );
assert_eq!( assert_eq!(
@@ -254,12 +254,12 @@ mod tests {
worker_split worker_split
.author .author
.as_ref() .as_ref()
.map(|author| author.login.as_str()), .map(|author| author.login.as_ref()),
Some("leaferiksen") Some("leaferiksen")
); );
assert_eq!(worker_split.base_branch_name.as_str(), "main"); assert_eq!(worker_split.base_branch_name.as_ref(), "main");
assert_eq!( assert_eq!(
worker_split.head_branch_name.as_str(), worker_split.head_branch_name.as_ref(),
"feat/worker-context-envelope" "feat/worker-context-envelope"
); );
assert_eq!( assert_eq!(
@@ -270,12 +270,12 @@ mod tests {
spacing_tokens spacing_tokens
.author .author
.as_ref() .as_ref()
.map(|author| author.login.as_str()), .map(|author| author.login.as_ref()),
Some("mariahops") Some("mariahops")
); );
assert_eq!(spacing_tokens.base_branch_name.as_str(), "main"); assert_eq!(spacing_tokens.base_branch_name.as_ref(), "main");
assert_eq!( assert_eq!(
spacing_tokens.head_branch_name.as_str(), spacing_tokens.head_branch_name.as_ref(),
"chore/dashboard-spacing-scale" "chore/dashboard-spacing-scale"
); );
assert_eq!( assert_eq!(
@@ -294,13 +294,13 @@ mod tests {
let base_query = fetch_file_content( let base_query = fetch_file_content(
"kennethnym/novem", "kennethnym/novem",
"src/query.rs", "src/query.rs",
Some(dashboard_markdown.base_ref.as_str()), Some(dashboard_markdown.base_ref.as_ref()),
) )
.expect("base query fixture should exist"); .expect("base query fixture should exist");
let head_query = fetch_file_content( let head_query = fetch_file_content(
"kennethnym/novem", "kennethnym/novem",
"src/query.rs", "src/query.rs",
Some(dashboard_markdown.head_ref.as_str()), Some(dashboard_markdown.head_ref.as_ref()),
) )
.expect("head query fixture should exist"); .expect("head query fixture should exist");
let base_query = std::str::from_utf8(base_query.as_ref()) let base_query = std::str::from_utf8(base_query.as_ref())
@@ -317,13 +317,13 @@ mod tests {
let base_repo = fetch_file_content( let base_repo = fetch_file_content(
"kennethnym/novem", "kennethnym/novem",
"src/api/repo.rs", "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"); .expect("base repo fixture should exist");
let head_repo = fetch_file_content( let head_repo = fetch_file_content(
"kennethnym/novem", "kennethnym/novem",
"src/api/repo.rs", "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"); .expect("head repo fixture should exist");
let base_repo = let base_repo =
@@ -364,15 +364,15 @@ mod tests {
for path in file_paths { for path in file_paths {
fetch_file_content( fetch_file_content(
dashboard_markdown.base_repo_slug.as_str(), dashboard_markdown.base_repo_slug.as_ref(),
path, 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}")); .unwrap_or_else(|_| panic!("base fixture should exist for {path}"));
fetch_file_content( fetch_file_content(
dashboard_markdown.head_repo_slug.as_str(), dashboard_markdown.head_repo_slug.as_ref(),
path, 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}")); .unwrap_or_else(|_| panic!("head fixture should exist for {path}"));
} }
@@ -440,36 +440,30 @@ mod tests {
.expect("third timeline fixture json should parse"); .expect("third timeline fixture json should parse");
let first_page_nodes = match first_page.node.as_ref() { let first_page_nodes = match first_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request
pull_request .timeline_items
.timeline_items .nodes
.nodes .as_ref()
.as_ref() .expect("first timeline fixture page should contain timeline nodes"),
.expect("first timeline fixture page should contain timeline nodes") | _ => panic!("first timeline fixture page should resolve to a pull request node"),
}
| _ => panic!("first timeline fixture page should resolve to a pull request node"),
}; };
let second_page_nodes = match second_page.node.as_ref() { let second_page_nodes = match second_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request
pull_request .timeline_items
.timeline_items .nodes
.nodes .as_ref()
.as_ref() .expect("second timeline fixture page should contain timeline nodes"),
.expect("second timeline fixture page should contain timeline nodes") | _ => panic!("second timeline fixture page should resolve to a pull request node"),
}
| _ => panic!("second timeline fixture page should resolve to a pull request node"),
}; };
let third_page_nodes = match third_page.node.as_ref() { let third_page_nodes = match third_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => { | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request
pull_request .timeline_items
.timeline_items .nodes
.nodes .as_ref()
.as_ref() .expect("third timeline fixture page should contain timeline nodes"),
.expect("third timeline fixture page should contain timeline nodes") | _ => panic!("third timeline fixture page should resolve to a pull request node"),
}
| _ => panic!("third timeline fixture page should resolve to a pull request node"),
}; };
assert_eq!( assert_eq!(

View File

@@ -1,11 +1,10 @@
use futures::{FutureExt, TryFutureExt}; use std::sync::Arc;
use reqwest::Method; use reqwest::Method;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::OwnedRwLockReadGuard;
use crate::{ use crate::{
api, api, query,
query::{self, Query, fetch_query},
util::{self, file}, util::{self, file},
}; };
@@ -31,9 +30,9 @@ pub struct Owner {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileRef { pub struct FileRef {
pub repo_slug: String, pub repo_slug: Arc<str>,
pub path: String, pub path: Arc<str>,
pub reff: Option<String>, pub reff: Option<Arc<str>>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -67,9 +66,9 @@ impl query::QueryFn for List {
#[derive(Clone)] #[derive(Clone)]
pub struct FetchFileContent { pub struct FetchFileContent {
pub repo_slug: String, pub repo_slug: Arc<str>,
pub path: String, pub path: Arc<str>,
pub reff: Option<String>, pub reff: Option<Arc<str>>,
} }
impl query::QueryFn for FetchFileContent { impl query::QueryFn for FetchFileContent {
@@ -79,8 +78,8 @@ impl query::QueryFn for FetchFileContent {
fn key(&self) -> query::Key { fn key(&self) -> query::Key {
match &self.reff { match &self.reff {
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(), | Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
| None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).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 { let path = match &self.reff {
| Some(reff) => format!( | Some(reff) => format!(
"/repos/{}/contents/{}?ref={}", "/repos/{}/contents/{}?ref={}",
self.repo_slug, self.path, reff self.repo_slug, self.path, reff
), ),
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path), | None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
}; };
let res = c let res = c
@@ -137,8 +136,8 @@ impl query::QueryFn for FetchFileDiff {
c: &<FetchFileDiff as query::QueryFn>::Context, c: &<FetchFileDiff as query::QueryFn>::Context,
) -> Result<Option<bytes::Bytes>, api::Error> { ) -> Result<Option<bytes::Bytes>, api::Error> {
let path = match &r.reff { let path = match &r.reff {
| Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff), | Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff),
| None => format!("/repos/{}/contents/{}", r.repo_slug, r.path), | None => format!("/repos/{}/contents/{}", r.repo_slug, r.path),
}; };
let res = c 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),); let (old, new) = tokio::join!(fetch_content(&self.base, c), fetch_content(&self.head, c),);
match (old, new) { match (old, new) {
| (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)), | (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)),
| _ => Err(api::Error::MalformedResponse( | _ => Err(api::Error::MalformedResponse(
"failed to fetch content".to_string(), "failed to fetch content".to_string(),
)), )),
} }
} }
} }

View File

@@ -1,4 +1,4 @@
use std::ops::Deref; use std::{ops::Deref, sync::Arc};
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -12,18 +12,18 @@ pub struct Id(u64);
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {
pub login: String, pub login: Arc<str>,
pub id: Id, pub id: Id,
pub avatar_url: String, pub avatar_url: Arc<str>,
pub html_url: String, pub html_url: Arc<str>,
pub name: Option<String>, pub name: Option<Arc<str>>,
pub email: Option<String>, pub email: Option<Arc<str>>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct Actor { pub(crate) struct Actor {
pub(crate) login: String, pub(crate) login: Arc<str>,
pub(crate) avatar_url: String, pub(crate) avatar_url: Arc<str>,
} }
impl Deref for Id { impl Deref for Id {

View File

@@ -1,6 +1,9 @@
// markdown treesitter playground: https://ikatyang.github.io/tree-sitter-markdown/ // 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}; 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; const MARKDOWN_KIND_ID_TASK_LIST_ITEM_MARKER: u16 = 236;
pub(crate) struct MarkdownText { pub(crate) struct MarkdownText {
content: gpui::SharedString, content: Arc<str>,
blocks: Vec<ContentBlock>, blocks: Vec<ContentBlock>,
} }
@@ -100,10 +103,7 @@ enum ContentBlock {
}, },
} }
pub(crate) fn new( pub(crate) fn new(content: Arc<str>, cx: &mut gpui::Context<MarkdownText>) -> MarkdownText {
content: gpui::SharedString,
cx: &mut gpui::Context<MarkdownText>,
) -> MarkdownText {
let mut view = MarkdownText { let mut view = MarkdownText {
content, content,
blocks: Vec::new(), blocks: Vec::new(),
@@ -115,20 +115,20 @@ pub(crate) fn new(
impl Styled for ContentBlock { impl Styled for ContentBlock {
fn style(&mut self) -> &mut gpui::StyleRefinement { fn style(&mut self) -> &mut gpui::StyleRefinement {
match self { match self {
| ContentBlock::Text { style, .. } => style, | ContentBlock::Text { style, .. } => style,
} }
} }
} }
impl MarkdownText { impl MarkdownText {
fn on_create(&mut self, cx: &gpui::Context<Self>) { fn on_create(&mut self, cx: &gpui::Context<Self>) {
let content = self.content.clone(); let content = Arc::clone(&self.content);
let t = cx.background_spawn(async move { let t = cx.background_spawn(async move {
let mut parser = tree_sitter::Parser::new(); let mut parser = tree_sitter::Parser::new();
parser parser
.set_language(tree_sitter_markdown::language()) .set_language(tree_sitter_markdown::language())
.expect("tree-sitter-markdown language should load"); .expect("tree-sitter-markdown language should load");
parser.parse(content.as_str(), None) parser.parse(content.as_bytes(), None)
}); });
cx.spawn(async |weak, cx| { cx.spawn(async |weak, cx| {
@@ -179,56 +179,54 @@ impl MarkdownText {
} }
match node.kind_id() { 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(( highlights.push((
node_range!(), node_range!(),
gpui::HighlightStyle { 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() ..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_next_sibling()
if cursor.goto_first_child() { && let Ok(src) = cursor.node().utf8_text(content.as_bytes())
highlights.push(( {
node_range!(), links
gpui::HighlightStyle { .push((node_range!(), gpui::SharedString::from(String::from(src))));
color: Some(theme.colors.link.into()), } else {
underline: Some(gpui::UnderlineStyle { // the link src is invalid, use an empty string as a fallback
color: Some(theme.colors.link.into()), // link on click handler will ignore empty string
thickness: px(1.), links.push((node_range!(), "".into()))
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()))
}
} }
} }
}
| _ => { | _ => {
// extend here to support more markdown node stylings // extend here to support more markdown node stylings
} }
}; };
if !cursor.goto_next_sibling() { if !cursor.goto_next_sibling() {
@@ -305,23 +303,23 @@ impl MarkdownText {
let marker_content = &content[marker_node.byte_range()]; let marker_content = &content[marker_node.byte_range()];
let list_marker_char = match marker_content { let list_marker_char = match marker_content {
// unordered list item // unordered list item
| "-" | "+" | "*" => Some("".to_string()), | "-" | "+" | "*" => Some("".to_string()),
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => { | marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
let i = list_index.get_or_insert_with(|| { let i = list_index.get_or_insert_with(|| {
marker_content marker_content
.strip_suffix('.') .strip_suffix('.')
.unwrap() .unwrap()
.parse::<usize>() .parse::<usize>()
.unwrap() .unwrap()
}); });
let j = *i; let j = *i;
*i = j + 1; *i = j + 1;
Some(format!("{j}.")) Some(format!("{j}."))
} }
| _ => None, | _ => None,
}; };
let Some(list_marker_char) = list_marker_char else { let Some(list_marker_char) = list_marker_char else {
@@ -333,9 +331,9 @@ impl MarkdownText {
let block = if cursor.goto_next_sibling() { let block = if cursor.goto_next_sibling() {
let mut b = block_for_node(cursor, content, 0, theme); let mut b = block_for_node(cursor, content, 0, theme);
match b { match b {
| ContentBlock::Text { | ContentBlock::Text {
ref mut decoration, .. ref mut decoration, ..
} => *decoration = Some(list_marker_char.into()), } => *decoration = Some(list_marker_char.into()),
} }
b b
} else { } else {
@@ -374,150 +372,150 @@ impl MarkdownText {
} }
match current_node.kind_id() { match current_node.kind_id() {
| MARKDOWN_KIND_ID_ATX_HEADING => { | MARKDOWN_KIND_ID_ATX_HEADING => {
if !cursor.goto_first_child() { if !cursor.goto_first_child() {
render_fallback_content(&cursor, &self.content, &mut self.blocks); render_fallback_content(&cursor, &self.content, &mut self.blocks);
continue; 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_PARAGRAPH => { let marker_node_kind = cursor.node().kind_id();
let block = block_for_node(&mut cursor, &self.content, 0, theme)
.text_color(theme.colors.text)
.text_sm();
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 => { cursor.goto_parent();
let is_rendered =
render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0); self.blocks.push(block);
if !is_rendered { }
continue;
} | 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 => { let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING {
// expected tree shape: // skipping info string (which annotates the code block)
// fenced_code_block if cursor.goto_next_sibling() {
// ├── info_string? (present if there is a language annotation) // this is code_fence_content node
// └── 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
gpui::SharedString::new( gpui::SharedString::new(
cursor cursor
.node() .node()
.utf8_text(self.content.as_bytes()) .utf8_text(self.content.as_bytes())
.unwrap_or_default(), .unwrap_or_default(),
) )
}; } else {
gpui::SharedString::default()
cursor.goto_parent();
let block = ContentBlock::Text {
decoration: None,
text: content,
highlights: Vec::new(),
links: Vec::new(),
style: gpui::StyleRefinement::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) .text_color(theme.colors.text)
.line_height(relative(1.2)) .text_sm();
.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); 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);
}
} }
if !cursor.goto_next_sibling() { if !cursor.goto_next_sibling() {
@@ -535,55 +533,55 @@ impl gpui::Render for MarkdownText {
) -> impl gpui::prelude::IntoElement { ) -> impl gpui::prelude::IntoElement {
let children = self.blocks.iter().enumerate().map(|(i, block)| { let children = self.blocks.iter().enumerate().map(|(i, block)| {
match block { match block {
| ContentBlock::Text { | ContentBlock::Text {
decoration, decoration,
text, text,
highlights, highlights,
links, links,
style, style,
} => { } => {
let styled_text = let styled_text =
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone()); gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
let content = if links.is_empty() { let content = if links.is_empty() {
div().w_full().child(styled_text) div().w_full().child(styled_text)
} else { } else {
// if link in block, interactive text is needed // if link in block, interactive text is needed
// to handle link clicks // to handle link clicks
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip(); let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
let weak = cx.entity(); let weak = cx.entity();
let t = gpui::InteractiveText::new(i, styled_text).on_click( let t = gpui::InteractiveText::new(i, styled_text).on_click(
link_ranges, link_ranges,
move |i, _, cx| { move |i, _, cx| {
if let Some(src) = srcs.get(i) { if let Some(src) = srcs.get(i) {
weak.update(cx, |this, cx| { weak.update(cx, |this, cx| {
this.on_open_link(src, cx); this.on_open_link(src, cx);
cx.notify(); cx.notify();
}) })
} }
}, },
); );
div().w_full().child(t) div().w_full().child(t)
}; };
let mut div = match decoration { let mut div = match decoration {
| Some(d) => div() | Some(d) => div()
.w_full() .w_full()
.flex() .flex()
.flex_row() .flex_row()
.gap_2() .gap_2()
.items_start() .items_start()
.child(d.clone()) .child(d.clone())
.child(div().flex_1().min_w_0().child(content)), .child(div().flex_1().min_w_0().child(content)),
| None => div().w_full().child(content), | None => div().w_full().child(content),
}; };
div.style().refine(&style); div.style().refine(&style);
div div
} }
} }
}); });

View File

@@ -1,5 +1,3 @@
use std::ops::Deref;
use gpui::{ use gpui::{
InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, list, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, list,
point, prelude::FluentBuilder, px, point, prelude::FluentBuilder, px,
@@ -13,6 +11,7 @@ use crate::{
text::text, text::text,
}, },
query::{self, QueryStatus, read_query, use_query}, query::{self, QueryStatus, read_query, use_query},
util::str::ToSharedString,
}; };
pub(crate) struct IssueList { pub(crate) struct IssueList {
@@ -20,7 +19,6 @@ pub(crate) struct IssueList {
list_state: gpui::ListState, list_state: gpui::ListState,
list_items: Vec<IssueListItem>, list_items: Vec<IssueListItem>,
selected_item: Option<(usize, gpui::SharedString)>,
} }
pub(crate) enum Event { pub(crate) enum Event {
@@ -29,7 +27,7 @@ pub(crate) enum Event {
#[derive(gpui::IntoElement, Clone)] #[derive(gpui::IntoElement, Clone)]
pub(crate) struct IssueListItem { pub(crate) struct IssueListItem {
id: gpui::SharedString, id: api::issues::Id,
repo_name: Option<gpui::SharedString>, repo_name: Option<gpui::SharedString>,
title: gpui::SharedString, title: gpui::SharedString,
description: Option<gpui::SharedString>, description: Option<gpui::SharedString>,
@@ -51,7 +49,6 @@ pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)), list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)),
list_items: Vec::new(), list_items: Vec::new(),
selected_item: None,
}; };
list.on_create(cx); list.on_create(cx);
list list
@@ -66,16 +63,12 @@ impl IssueList {
let new_len = res.items.len(); let new_len = res.items.len();
let new_items = res.items.iter().enumerate().map(|(i, it)| IssueListItem { let new_items = res.items.iter().enumerate().map(|(i, it)| IssueListItem {
id: gpui::SharedString::from(it.id.deref()), id: it.id.clone(),
repo_name: Some(gpui::SharedString::new(it.repo_slug.as_str())), repo_name: Some(it.repo_slug.to_shared_string()),
title: gpui::SharedString::new(it.title.as_str()), title: it.title.to_shared_string(),
description: None, description: None,
status: it.state, status: it.state,
is_selected: this is_selected: false,
.selected_item
.as_ref()
.map(|(_, id)| id.as_str() == it.id.as_str())
.unwrap_or(false),
is_last: i == new_len - 1, is_last: i == new_len - 1,
is_draft: it.is_draft, is_draft: it.is_draft,
}); });
@@ -95,7 +88,7 @@ impl IssueList {
item.is_selected = i == j; item.is_selected = i == j;
} }
cx.notify(); 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 { let repo_name_text = match self.repo_name {
| Some(name) => text(name), | Some(name) => text(name),
| None => text("Unknown repo"), | None => text("Unknown repo"),
} }
.text_xs() .text_xs()
.opacity(0.5); .opacity(0.5);
@@ -162,21 +155,21 @@ impl gpui::RenderOnce for IssueListItem {
.bg(theme.colors.surface) .bg(theme.colors.surface)
} else { } else {
match self.status { match self.status {
| api::issues::PullRequestState::Closed => pill( | api::issues::PullRequestState::Closed => pill(
text("Closed").text_color(theme.colors.danger_on_solid), text("Closed").text_color(theme.colors.danger_on_solid),
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger_on_solid), font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger_on_solid),
) )
.bg(theme.colors.danger_solid), .bg(theme.colors.danger_solid),
| api::issues::PullRequestState::Merged => pill( | api::issues::PullRequestState::Merged => pill(
text("Merged").text_color(theme.colors.accent_on_solid), text("Merged").text_color(theme.colors.accent_on_solid),
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.accent_on_solid), font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.accent_on_solid),
) )
.bg(theme.colors.accent_solid), .bg(theme.colors.accent_solid),
| _ => pill( | _ => pill(
text("Open").text_color(theme.colors.success_on_solid), text("Open").text_color(theme.colors.success_on_solid),
font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success_on_solid), font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success_on_solid),
) )
.bg(theme.colors.success_solid), .bg(theme.colors.success_solid),
} }
}; };

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use gpui::{ use gpui::{
AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled,
div, img, prelude::FluentBuilder, div, img, prelude::FluentBuilder,
@@ -65,7 +67,7 @@ impl PullRequestView {
let maybe_content = { let maybe_content = {
let data = read_query(&query, cx); let data = read_query(&query, cx);
if let QueryStatus::Loaded(pr) = data { if let QueryStatus::Loaded(pr) = data {
Some(gpui::SharedString::new(pr.body.as_str())) Some(Arc::clone(&pr.body))
} else { } else {
None None
} }
@@ -130,8 +132,8 @@ impl PullRequestView {
} }
let merge_text = pr.author.as_ref().map(|author| { let merge_text = pr.author.as_ref().map(|author| {
let base_branch = pr.base_branch_name.as_str(); let base_branch = &pr.base_branch_name;
let head_branch = pr.head_branch_name.as_str(); let head_branch = &pr.head_branch_name;
let str = format!( let str = format!(
"{} requested to merge {} into {}", "{} requested to merge {} into {}",
author.login, head_branch, base_branch 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 = let metadata_line =
div() div()
.flex() .flex()
@@ -184,7 +188,7 @@ impl PullRequestView {
.flex_row() .flex_row()
.items_center() .items_center()
.gap_1p5() .gap_1p5()
.child(img(author.avatar_url.clone()).size_4().rounded_full()) .child(img(author.avatar_url.as_ref()).size_4().rounded_full())
.child( .child(
div() div()
.min_w_0() .min_w_0()
@@ -222,7 +226,7 @@ impl PullRequestView {
.flex() .flex()
.flex_col() .flex_col()
.items_start() .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(metadata_line),
) )
.child(div().flex().flex_col().items_end().gap_1().when_some( .child(div().flex().flex_col().items_end().gap_1().when_some(

View File

@@ -1,4 +1,4 @@
use std::time::Duration; use std::{sync::Arc, time::Duration};
use futures_lite::StreamExt; use futures_lite::StreamExt;
use gpui::{ use gpui::{
@@ -16,7 +16,7 @@ use crate::{
}, },
query::{self, QueryStatus, fetch_query, read_query, use_query}, query::{self, QueryStatus, fetch_query, read_query, use_query},
storage, storage,
util::timeout::set_timeout, util::{str::ToSharedString, timeout::set_timeout},
}; };
pub(crate) struct GithubStepView { pub(crate) struct GithubStepView {
@@ -83,7 +83,7 @@ impl GithubStepView {
_ = weak.update(cx, |this, cx| { _ = weak.update(cx, |this, cx| {
this.has_opened_link = true; this.has_opened_link = true;
this.is_opening_link = false; this.is_opening_link = false;
this.begin_auth_flow(&device_code, cx); this.begin_auth_flow(device_code, cx);
cx.notify(); cx.notify();
}); });
}, },
@@ -110,7 +110,7 @@ impl GithubStepView {
timer.clear(); timer.clear();
} else { } else {
let _ = this.update(cx, |this, cx| { let _ = this.update(cx, |this, cx| {
this.placeholder_code = this.generate_random_code(cx); this.generate_random_code(cx);
cx.notify(); cx.notify();
}); });
} }
@@ -127,14 +127,13 @@ impl GithubStepView {
cx.open_url(api::auth::DEVICE_LOGIN_FLOW_URL); cx.open_url(api::auth::DEVICE_LOGIN_FLOW_URL);
} }
fn generate_random_code(&mut self, cx: &mut gpui::Context<Self>) -> String { fn generate_random_code(&mut self, cx: &mut gpui::Context<Self>) {
let rng = app::rng(cx); let rng = app::rng(cx);
(0..8) self.placeholder_code.clear();
.map(|_| { self.placeholder_code.extend((0..8).map(|_| {
let idx = rng.random_range(0..Self::CHAR_POOL.len()); let idx = rng.random_range(0..Self::CHAR_POOL.len());
Self::CHAR_POOL.chars().nth(idx).unwrap() Self::CHAR_POOL.chars().nth(idx).unwrap()
}) }));
.collect()
} }
fn copy_user_code(&mut self, code: &str, cx: &mut gpui::Context<Self>) { fn copy_user_code(&mut self, code: &str, cx: &mut gpui::Context<Self>) {
@@ -155,15 +154,10 @@ impl GithubStepView {
cx.notify(); cx.notify();
} }
fn begin_auth_flow(&mut self, device_code: &str, cx: &mut gpui::Context<Self>) { fn begin_auth_flow(&mut self, device_code: Arc<str>, cx: &mut gpui::Context<Self>) {
GithubStepView::open_github_auth_page(cx); GithubStepView::open_github_auth_page(cx);
let query = use_query( let query = use_query(api::auth::RequestAccessToken { device_code }, cx);
api::auth::RequestAccessToken {
device_code: device_code.to_owned(),
},
cx,
);
cx.observe(&query, |this, _, cx| { cx.observe(&query, |this, _, cx| {
this.handle_access_token_query_response(cx); this.handle_access_token_query_response(cx);
@@ -189,60 +183,60 @@ impl GithubStepView {
let poll_interval = u64::from(*interval); let poll_interval = u64::from(*interval);
match read_query(query, cx) { match read_query(query, cx) {
| QueryStatus::Loaded(data) => { | QueryStatus::Loaded(data) => {
let auth_tokens = api::AuthTokens { let auth_tokens = api::AuthTokens {
access_token: data.access_token.clone(), access_token: data.access_token.clone(),
}; };
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| { cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
store.update_query_context(|c| { store.update_query_context(|c| {
c.auth = Some(auth_tokens.clone()); 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| { cx.spawn(async move |weak, cx| {
let ent = fetch_query(api::user::Fetch, cx).await; Timer::after(Duration::from_secs(poll_interval)).await;
if let Ok(Some(query)) =
let fut = weak weak.read_with(cx, |this, _cx| this.request_access_token_query.clone())
.update(cx, move |_this, cx| { {
let Ok(query) = ent else { let _ = weak.update(cx, |_this, cx| {
return None; query.refetch(cx);
}; });
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(); .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 theme = app::current_theme(cx);
let (displayed_code, copyable_code) = match create_device_code_query { let (displayed_code, copyable_code) = match create_device_code_query {
| QueryStatus::Loaded(data) => (data.user_code.as_str(), Some(data.user_code.clone())), | QueryStatus::Loaded(data) => (data.user_code.as_ref(), Some(data.user_code.clone())),
| _ => (self.placeholder_code.as_str(), None), | _ => (self.placeholder_code.as_str(), None),
}; };
let border_color = theme.colors.border.clone(); let border_color = theme.colors.border.clone();
@@ -358,16 +352,14 @@ impl gpui::Render for GithubStepView {
cx: &mut gpui::Context<Self>, cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement { ) -> impl gpui::IntoElement {
let (can_go_next, header, body) = match self.user_query { let (can_go_next, header, body) = match self.user_query {
| None => (false, self.header(), self.device_code_area(cx)), | None => (false, self.header(), self.device_code_area(cx)),
| Some(ref q) => { | Some(ref q) => {
let user_query = read_query(q, cx); let user_query = read_query(q, cx);
match user_query { match user_query {
| QueryStatus::Loaded(user) => { | QueryStatus::Loaded(user) => (true, connected_header(), connected_body(user, cx)),
(true, connected_header(), connected_body(user, cx)) | _ => (false, self.header(), self.device_code_area(cx)),
}
| _ => (false, self.header(), self.device_code_area(cx)),
}
} }
}
}; };
div() div()
@@ -417,7 +409,7 @@ fn connected_header() -> gpui::Div {
fn connected_body(user: &api::user::User, cx: &gpui::Context<GithubStepView>) -> gpui::Div { fn connected_body(user: &api::user::User, cx: &gpui::Context<GithubStepView>) -> gpui::Div {
let theme = app::current_theme(cx); 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() div()
.flex() .flex()
@@ -444,7 +436,7 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context<GithubStepView>) ->
.flex_row() .flex_row()
.gap_4() .gap_4()
.items_center() .items_center()
.child(img(user.avatar_url.clone()).size_12().rounded_full()) .child(img(user.avatar_url.as_ref()).size_12().rounded_full())
.child( .child(
div() div()
.flex() .flex()
@@ -455,7 +447,7 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context<GithubStepView>) ->
.text_xl() .text_xl()
.leading_tight(), .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( .child(

View File

@@ -20,10 +20,10 @@ pub(crate) struct LineDiff {
pub(crate) fn classify_content(content: &[u8]) -> ContentType { pub(crate) fn classify_content(content: &[u8]) -> ContentType {
if content.is_empty() { if content.is_empty() {
ContentType::Text 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(&[0x00, 0x00, 0xFE, 0xFF]) // UTF-32 BE
|| content.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) // UTF-32 LE || 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]) || content.starts_with(&[0xFF, 0xFE])
{ {
ContentType::Text 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()
}

View File

@@ -1,3 +1,4 @@
pub(crate) mod diff; pub(crate) mod diff;
pub(crate) mod file; pub(crate) mod file;
pub(crate) mod str;
pub(crate) mod timeout; pub(crate) mod timeout;

20
src/util/str.rs Normal file
View File

@@ -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<str> {
/// converts into gpui SharedString cheaply with no allocation involved.
fn to_shared_string(&self) -> gpui::SharedString {
gpui::SharedString::new(Arc::clone(self))
}
}
impl Into<gpui::ElementId> for api::issues::Id {
fn into(self) -> gpui::ElementId {
gpui::ElementId::Name(gpui::SharedString::new(Arc::clone(&self.0)))
}
}