refactor: prefer Arc<str> to String
This commit is contained in:
32
build.rs
32
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) {
|
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| {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/api.rs
14
src/api.rs
@@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
100
src/api/mock.rs
100
src/api/mock.rs
@@ -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!(
|
||||||
|
|||||||
@@ -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(),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
20
src/util/str.rs
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user