feat: syntax highlighting for diff view
This commit is contained in:
10
src/api.rs
10
src/api.rs
@@ -116,9 +116,9 @@ async fn raw_content(res: reqwest::Response) -> Result<bytes::Bytes, Error> {
|
||||
Ok(res.bytes().await?)
|
||||
} else {
|
||||
match res.status() {
|
||||
| reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist),
|
||||
| reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed),
|
||||
| _ => Err(Error::MalformedResponse(res.status().to_string())),
|
||||
| reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist),
|
||||
| reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed),
|
||||
| _ => Err(Error::MalformedResponse(res.status().to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ where
|
||||
{
|
||||
let mut body: graphql_client::Response<T> = res.json().await?;
|
||||
match body.data.take() {
|
||||
| None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
|
||||
| Some(data) => Ok((body, data)),
|
||||
| None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
|
||||
| Some(data) => Ok((body, data)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,8 +318,8 @@ impl query::QueryFn for ListPullRequests {
|
||||
}
|
||||
|
||||
let query_string = match self.filter {
|
||||
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
||||
| None => "is:pr archived:false sort:updated-desc".into(),
|
||||
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
||||
| None => "is:pr archived:false sort:updated-desc".into(),
|
||||
};
|
||||
|
||||
let gql =
|
||||
@@ -341,20 +341,20 @@ impl query::QueryFn for ListPullRequests {
|
||||
.flatten()
|
||||
.filter_map(|edge| {
|
||||
edge.node.and_then(|n| match n {
|
||||
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
||||
Some(PullRequest {
|
||||
id: p.id.into(),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
repo_slug: format!(
|
||||
"{}/{}",
|
||||
p.repository.owner.login, p.repository.name
|
||||
)
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
| _ => None,
|
||||
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
||||
Some(PullRequest {
|
||||
id: p.id.into(),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
repo_slug: format!(
|
||||
"{}/{}",
|
||||
p.repository.owner.login, p.repository.name
|
||||
)
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
| _ => None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -399,43 +399,43 @@ impl query::QueryFn for FetchPullRequest {
|
||||
"missing 'node' field on PullRequestQuery response".into(),
|
||||
))
|
||||
.and_then(|n| match n {
|
||||
| PullRequestQueryNode::PullRequest(p) => {
|
||||
let created_at =
|
||||
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
||||
api::Error::MalformedResponse(format!(
|
||||
"invalid pull request createdAt {:?}: {err}",
|
||||
p.created_at
|
||||
))
|
||||
})?;
|
||||
| PullRequestQueryNode::PullRequest(p) => {
|
||||
let created_at =
|
||||
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
||||
api::Error::MalformedResponse(format!(
|
||||
"invalid pull request createdAt {:?}: {err}",
|
||||
p.created_at
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(DetailedPullRequest {
|
||||
id: Id(p.id.into()),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
body: p.body.into(),
|
||||
author: p.author.map(|it| api::user::Actor {
|
||||
login: it.login.into(),
|
||||
avatar_url: it.avatar_url.into(),
|
||||
}),
|
||||
base_repo_slug: p
|
||||
.base_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
base_branch_name: p.base_ref_name.into(),
|
||||
base_ref: p.base_ref_oid.into(),
|
||||
head_repo_slug: p
|
||||
.head_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
head_branch_name: p.head_ref_name.into(),
|
||||
head_ref: p.head_ref_oid.into(),
|
||||
created_at: Some(created_at),
|
||||
})
|
||||
}
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestQuery".into(),
|
||||
)),
|
||||
Ok(DetailedPullRequest {
|
||||
id: Id(p.id.into()),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
body: p.body.into(),
|
||||
author: p.author.map(|it| api::user::Actor {
|
||||
login: it.login.into(),
|
||||
avatar_url: it.avatar_url.into(),
|
||||
}),
|
||||
base_repo_slug: p
|
||||
.base_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
base_branch_name: p.base_ref_name.into(),
|
||||
base_ref: p.base_ref_oid.into(),
|
||||
head_repo_slug: p
|
||||
.head_repository
|
||||
.map(|it| it.name_with_owner.into())
|
||||
.unwrap_or_default(),
|
||||
head_branch_name: p.head_ref_name.into(),
|
||||
head_ref: p.head_ref_oid.into(),
|
||||
created_at: Some(created_at),
|
||||
})
|
||||
}
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestQuery".into(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -494,12 +494,12 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
||||
"missing 'node' field on PullRequestFileTreeQuery response".into(),
|
||||
))
|
||||
.and_then(|node| match node {
|
||||
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
||||
pull_request,
|
||||
) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestFileTreeQuery".into(),
|
||||
)),
|
||||
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
||||
pull_request,
|
||||
) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestFileTreeQuery".into(),
|
||||
)),
|
||||
})?;
|
||||
|
||||
Ok(pull_request
|
||||
@@ -513,23 +513,25 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
||||
edge.node.map(|node| ChangedFile {
|
||||
cursor,
|
||||
change_type: match node.change_type {
|
||||
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added,
|
||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||
ChangeType::Modified
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
||||
ChangeType::Deleted
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
||||
ChangeType::Renamed
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
||||
ChangeType::Copied
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
||||
ChangeType::Changed
|
||||
}
|
||||
| _ => ChangeType::Changed,
|
||||
| pull_request_file_tree_query::PatchStatus::ADDED => {
|
||||
ChangeType::Added
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||
ChangeType::Modified
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
||||
ChangeType::Deleted
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
||||
ChangeType::Renamed
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
||||
ChangeType::Copied
|
||||
}
|
||||
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
||||
ChangeType::Changed
|
||||
}
|
||||
| _ => ChangeType::Changed,
|
||||
},
|
||||
additions: node.additions,
|
||||
deletions: node.deletions,
|
||||
@@ -575,11 +577,11 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
||||
|
||||
TimelineActor {
|
||||
kind: match on {
|
||||
| actorFieldsOn::Bot => "Bot",
|
||||
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
||||
| actorFieldsOn::Mannequin => "Mannequin",
|
||||
| actorFieldsOn::Organization => "Organization",
|
||||
| actorFieldsOn::User => "User",
|
||||
| actorFieldsOn::Bot => "Bot",
|
||||
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
||||
| actorFieldsOn::Mannequin => "Mannequin",
|
||||
| actorFieldsOn::Organization => "Organization",
|
||||
| actorFieldsOn::User => "User",
|
||||
}
|
||||
.into(),
|
||||
name: login,
|
||||
@@ -589,62 +591,62 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
||||
|
||||
fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
|
||||
match actor {
|
||||
| assigneeFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Organization(actor) => TimelineActor {
|
||||
kind: "Organization".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::Organization(actor) => TimelineActor {
|
||||
kind: "Organization".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| assigneeFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
|
||||
match actor {
|
||||
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Team(actor) => TimelineActor {
|
||||
kind: "Team".into(),
|
||||
name: actor.name,
|
||||
avatar_url: None,
|
||||
},
|
||||
| requestedReviewerFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
||||
kind: "Bot".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
||||
kind: "Mannequin".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
| requestedReviewerFields::Team(actor) => TimelineActor {
|
||||
kind: "Team".into(),
|
||||
name: actor.name,
|
||||
avatar_url: None,
|
||||
},
|
||||
| requestedReviewerFields::User(actor) => TimelineActor {
|
||||
kind: "User".into(),
|
||||
name: actor.login,
|
||||
avatar_url: Some(actor.avatar_url),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_review_state(state: PullRequestReviewState) -> String {
|
||||
match state {
|
||||
| PullRequestReviewState::PENDING => "PENDING",
|
||||
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
||||
| PullRequestReviewState::APPROVED => "APPROVED",
|
||||
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
||||
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
||||
| _ => "OTHER",
|
||||
| PullRequestReviewState::PENDING => "PENDING",
|
||||
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
||||
| PullRequestReviewState::APPROVED => "APPROVED",
|
||||
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
||||
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
||||
| _ => "OTHER",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -864,10 +866,10 @@ impl query::QueryFn for FetchPullRequestTimeline {
|
||||
"missing 'node' field on PullRequestTimelineQuery response".into(),
|
||||
))
|
||||
.and_then(|node| match node {
|
||||
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestTimelineQuery".into(),
|
||||
)),
|
||||
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"unexpected node type on PullRequestTimelineQuery".into(),
|
||||
)),
|
||||
})?;
|
||||
|
||||
let timeline = pull_request.timeline_items;
|
||||
|
||||
@@ -463,30 +463,36 @@ mod tests {
|
||||
.expect("third timeline fixture json should parse");
|
||||
|
||||
let first_page_nodes = match first_page.node.as_ref() {
|
||||
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request
|
||||
.timeline_items
|
||||
.nodes
|
||||
.as_ref()
|
||||
.expect("first timeline fixture page should contain timeline nodes"),
|
||||
| _ => panic!("first timeline fixture page should resolve to a pull request node"),
|
||||
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
|
||||
pull_request
|
||||
.timeline_items
|
||||
.nodes
|
||||
.as_ref()
|
||||
.expect("first timeline fixture page should contain timeline nodes")
|
||||
}
|
||||
| _ => panic!("first timeline fixture page should resolve to a pull request node"),
|
||||
};
|
||||
|
||||
let second_page_nodes = match second_page.node.as_ref() {
|
||||
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request
|
||||
.timeline_items
|
||||
.nodes
|
||||
.as_ref()
|
||||
.expect("second timeline fixture page should contain timeline nodes"),
|
||||
| _ => panic!("second timeline fixture page should resolve to a pull request node"),
|
||||
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
|
||||
pull_request
|
||||
.timeline_items
|
||||
.nodes
|
||||
.as_ref()
|
||||
.expect("second timeline fixture page should contain timeline nodes")
|
||||
}
|
||||
| _ => panic!("second timeline fixture page should resolve to a pull request node"),
|
||||
};
|
||||
|
||||
let third_page_nodes = match third_page.node.as_ref() {
|
||||
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request
|
||||
.timeline_items
|
||||
.nodes
|
||||
.as_ref()
|
||||
.expect("third timeline fixture page should contain timeline nodes"),
|
||||
| _ => panic!("third timeline fixture page should resolve to a pull request node"),
|
||||
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
|
||||
pull_request
|
||||
.timeline_items
|
||||
.nodes
|
||||
.as_ref()
|
||||
.expect("third timeline fixture page should contain timeline nodes")
|
||||
}
|
||||
| _ => panic!("third timeline fixture page should resolve to a pull request node"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::{num::NonZeroUsize, rc::Rc, sync::Arc};
|
||||
use std::{num::NonZeroUsize, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
IntoElement, ParentElement, Refineable, Styled, div, list, prelude::FluentBuilder, px, rems,
|
||||
};
|
||||
use gpui::{IntoElement, ParentElement, Refineable, Styled, div, list, prelude::FluentBuilder, px};
|
||||
|
||||
use crate::app;
|
||||
|
||||
#[derive(gpui::IntoElement, Clone)]
|
||||
#[derive(gpui::IntoElement)]
|
||||
pub(crate) struct CodeLine {
|
||||
line_number: Option<NonZeroUsize>,
|
||||
content: Option<gpui::SharedString>,
|
||||
content: Option<gpui::AnyElement>,
|
||||
diff_marker: CodeLineMarker,
|
||||
gutter_width: gpui::Pixels,
|
||||
style: gpui::StyleRefinement,
|
||||
@@ -45,18 +43,29 @@ pub(crate) fn code_line(
|
||||
) -> CodeLine {
|
||||
CodeLine {
|
||||
line_number: line_index.map(|i| unsafe { NonZeroUsize::new_unchecked(i + 1) }),
|
||||
content,
|
||||
content: content.map(|it| it.into_any_element()),
|
||||
diff_marker: marker,
|
||||
gutter_width: px(0.),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeViewContent {
|
||||
pub(crate) fn new(lines: Vec<CodeLine>) -> Self {
|
||||
Self {
|
||||
lines: lines.into(),
|
||||
}
|
||||
pub(crate) fn code_line_with_highlights(
|
||||
line_index: Option<usize>,
|
||||
content: Option<gpui::SharedString>,
|
||||
highlights: impl IntoIterator<Item = (std::ops::Range<usize>, gpui::HighlightStyle)>,
|
||||
marker: CodeLineMarker,
|
||||
) -> CodeLine {
|
||||
CodeLine {
|
||||
line_number: line_index.map(|i| unsafe { NonZeroUsize::new_unchecked(i + 1) }),
|
||||
content: content.map(|it| {
|
||||
gpui::StyledText::new(it)
|
||||
.with_highlights(highlights)
|
||||
.into_any_element()
|
||||
}),
|
||||
diff_marker: marker,
|
||||
gutter_width: px(0.),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,16 +98,7 @@ impl gpui::RenderOnce for CodeView {
|
||||
|
||||
println!("gutter width {}", gutter_width);
|
||||
|
||||
list(self.state.0, move |i, _window, _app| {
|
||||
let line = self.content.lines[i].clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_start()
|
||||
.w_full()
|
||||
.child(line.gutter_width(gutter_width))
|
||||
.into_any_element()
|
||||
})
|
||||
list(self.state.0, move |i, _window, _app| todo!())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
use gpui::{IntoElement, ParentElement, Styled, div, list, px};
|
||||
use gpui::{HighlightStyle, IntoElement, ParentElement, Styled, div, list, px};
|
||||
|
||||
use crate::{
|
||||
component::code_view::{self, CodeLine, code_line},
|
||||
component::code_view::{self, CodeLine, code_line, code_line_with_highlights},
|
||||
util::{self, str::ToSharedString},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,13 @@ pub(crate) struct DiffView {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiffViewState(gpui::ListState);
|
||||
pub(crate) struct DiffViewState(Rc<RefCell<DiffViewStateInner>>);
|
||||
|
||||
struct DiffViewStateInner {
|
||||
list_state: gpui::ListState,
|
||||
old_side_highlights: Option<util::syntax_highlight::HighlightedContent>,
|
||||
new_side_highlights: Option<util::syntax_highlight::HighlightedContent>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiffViewContent {
|
||||
@@ -23,6 +29,7 @@ pub(crate) struct DiffViewContent {
|
||||
|
||||
#[derive(Clone, gpui::IntoElement)]
|
||||
struct DiffRow {
|
||||
state: DiffViewState,
|
||||
line: util::diff::DiffLine,
|
||||
old_side_gutter_width: gpui::Pixels,
|
||||
new_side_gutter_width: gpui::Pixels,
|
||||
@@ -40,22 +47,36 @@ impl From<Arc<util::diff::ContentDiff>> for DiffViewContent {
|
||||
|
||||
impl DiffViewState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)))
|
||||
Self(Rc::new(RefCell::new(DiffViewStateInner {
|
||||
list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)),
|
||||
old_side_highlights: None,
|
||||
new_side_highlights: None,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn reset(&mut self, line_count: usize) {
|
||||
self.0.reset(line_count);
|
||||
self.0.borrow().list_state.reset(line_count);
|
||||
}
|
||||
|
||||
pub(crate) fn set_old_side_highlights(
|
||||
&mut self,
|
||||
highlights: util::syntax_highlight::HighlightedContent,
|
||||
) {
|
||||
self.0.borrow_mut().old_side_highlights = Some(highlights);
|
||||
}
|
||||
|
||||
pub(crate) fn set_new_side_highlights(
|
||||
&mut self,
|
||||
highlights: util::syntax_highlight::HighlightedContent,
|
||||
) {
|
||||
self.0.borrow_mut().new_side_highlights = Some(highlights);
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::RenderOnce for DiffView {
|
||||
fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
let (old_digits, new_digits) = self
|
||||
.content
|
||||
.diff
|
||||
.last()
|
||||
.map(|l| (l.old_line.to_string().len(), l.new_line.to_string().len()))
|
||||
.unwrap_or((1, 1));
|
||||
let old_digits = self.content.diff.old_line_count.to_string().len();
|
||||
let new_digits = self.content.diff.new_line_count.to_string().len();
|
||||
|
||||
let text_style = window.text_style();
|
||||
let font_size = text_style.font_size.to_pixels(window.rem_size());
|
||||
@@ -69,8 +90,11 @@ impl gpui::RenderOnce for DiffView {
|
||||
let old_side_gutter_width = ch * old_digits;
|
||||
let new_side_gutter_width = ch * new_digits;
|
||||
|
||||
list(self.state.0, move |i, _, cx| {
|
||||
let list_state = self.state.0.borrow().list_state.clone();
|
||||
|
||||
list(list_state, move |i, _, _| {
|
||||
DiffRow {
|
||||
state: self.state.clone(),
|
||||
line: self.content.diff.get(i).clone(),
|
||||
old_side_gutter_width,
|
||||
new_side_gutter_width,
|
||||
@@ -84,35 +108,69 @@ impl gpui::RenderOnce for DiffView {
|
||||
|
||||
impl DiffRow {
|
||||
fn old_code_line(&self) -> CodeLine {
|
||||
code_line(
|
||||
Some(self.line.old_line),
|
||||
self.line
|
||||
.old_content
|
||||
let state = self.state.0.borrow();
|
||||
|
||||
let content = self
|
||||
.line
|
||||
.old_content
|
||||
.as_ref()
|
||||
.map(|it| it.to_shared_string());
|
||||
|
||||
let marker = match self.line.op {
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
// inserting on new side, so placeholder on old side
|
||||
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
||||
// old side replaced, so delete
|
||||
| util::diff::Op::Replace | util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||
};
|
||||
|
||||
match self.line.old_line.and_then(|line| {
|
||||
state
|
||||
.old_side_highlights
|
||||
.as_ref()
|
||||
.map(|it| it.to_shared_string()),
|
||||
match self.line.op {
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
|
||||
| util::diff::Op::Replace | util::diff::Op::Delete => {
|
||||
code_view::CodeLineMarker::Deleted
|
||||
}
|
||||
},
|
||||
)
|
||||
.map(|it| it.highlights_at_line(line))
|
||||
}) {
|
||||
| Some(highlights) => code_line_with_highlights(
|
||||
self.line.new_line,
|
||||
content,
|
||||
highlights.iter().cloned(),
|
||||
marker,
|
||||
),
|
||||
|
||||
| None => code_line(self.line.new_line, content, marker),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_code_line(&self) -> CodeLine {
|
||||
code_line(
|
||||
Some(self.line.new_line),
|
||||
self.line
|
||||
.new_content
|
||||
let state = self.state.0.borrow();
|
||||
|
||||
let content = self
|
||||
.line
|
||||
.new_content
|
||||
.as_ref()
|
||||
.map(|it| it.to_shared_string());
|
||||
|
||||
let marker = match self.line.op {
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
|
||||
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||
};
|
||||
|
||||
match self.line.new_line.and_then(|line| {
|
||||
state
|
||||
.new_side_highlights
|
||||
.as_ref()
|
||||
.map(|it| it.to_shared_string()),
|
||||
match self.line.op {
|
||||
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
|
||||
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
|
||||
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted,
|
||||
},
|
||||
)
|
||||
.map(|it| it.highlights_at_line(line))
|
||||
}) {
|
||||
| Some(highlights) => code_line_with_highlights(
|
||||
self.line.new_line,
|
||||
content,
|
||||
highlights.iter().cloned(),
|
||||
marker,
|
||||
),
|
||||
|
||||
| None => code_line(self.line.new_line, content, marker),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ pub(crate) fn new(content: Arc<str>, cx: &mut gpui::Context<MarkdownText>) -> Ma
|
||||
impl Styled for ContentBlock {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
match self {
|
||||
| ContentBlock::Text { style, .. } => style,
|
||||
| ContentBlock::Text { style, .. } => style,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,54 +179,56 @@ impl MarkdownText {
|
||||
}
|
||||
|
||||
match node.kind_id() {
|
||||
| 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() {
|
||||
| MARKDOWN_KIND_ID_EMPHASIS => {
|
||||
highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
color: Some(theme.colors.link.into()),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
color: Some(theme.colors.link.into()),
|
||||
thickness: px(1.),
|
||||
wavy: false,
|
||||
}),
|
||||
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()
|
||||
},
|
||||
)),
|
||||
|
||||
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()))
|
||||
| MARKDOWN_KIND_ID_LINK => {
|
||||
if cursor.goto_first_child() {
|
||||
highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
color: Some(theme.colors.link.into()),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
color: Some(theme.colors.link.into()),
|
||||
thickness: px(1.),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
|
||||
if cursor.goto_next_sibling()
|
||||
&& let Ok(src) = cursor.node().utf8_text(content.as_bytes())
|
||||
{
|
||||
links.push((
|
||||
node_range!(),
|
||||
gpui::SharedString::from(String::from(src)),
|
||||
));
|
||||
} else {
|
||||
// the link src is invalid, use an empty string as a fallback
|
||||
// link on click handler will ignore empty string
|
||||
links.push((node_range!(), "".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
| _ => {
|
||||
// extend here to support more markdown node stylings
|
||||
}
|
||||
| _ => {
|
||||
// extend here to support more markdown node stylings
|
||||
}
|
||||
};
|
||||
|
||||
if !cursor.goto_next_sibling() {
|
||||
@@ -303,23 +305,23 @@ impl MarkdownText {
|
||||
let marker_content = &content[marker_node.byte_range()];
|
||||
|
||||
let list_marker_char = match marker_content {
|
||||
// unordered list item
|
||||
| "-" | "+" | "*" => Some("•".to_string()),
|
||||
// unordered list item
|
||||
| "-" | "+" | "*" => Some("•".to_string()),
|
||||
|
||||
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
|
||||
let i = list_index.get_or_insert_with(|| {
|
||||
marker_content
|
||||
.strip_suffix('.')
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.unwrap()
|
||||
});
|
||||
let j = *i;
|
||||
*i = j + 1;
|
||||
Some(format!("{j}."))
|
||||
}
|
||||
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
|
||||
let i = list_index.get_or_insert_with(|| {
|
||||
marker_content
|
||||
.strip_suffix('.')
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.unwrap()
|
||||
});
|
||||
let j = *i;
|
||||
*i = j + 1;
|
||||
Some(format!("{j}."))
|
||||
}
|
||||
|
||||
| _ => None,
|
||||
| _ => None,
|
||||
};
|
||||
|
||||
let Some(list_marker_char) = list_marker_char else {
|
||||
@@ -331,9 +333,9 @@ impl MarkdownText {
|
||||
let block = if cursor.goto_next_sibling() {
|
||||
let mut b = block_for_node(cursor, content, 0, theme);
|
||||
match b {
|
||||
| ContentBlock::Text {
|
||||
ref mut decoration, ..
|
||||
} => *decoration = Some(list_marker_char.into()),
|
||||
| ContentBlock::Text {
|
||||
ref mut decoration, ..
|
||||
} => *decoration = Some(list_marker_char.into()),
|
||||
}
|
||||
b
|
||||
} else {
|
||||
@@ -372,150 +374,150 @@ impl MarkdownText {
|
||||
}
|
||||
|
||||
match current_node.kind_id() {
|
||||
| MARKDOWN_KIND_ID_ATX_HEADING => {
|
||||
if !cursor.goto_first_child() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
continue;
|
||||
}
|
||||
|
||||
let marker_node_kind = cursor.node().kind_id();
|
||||
|
||||
let block = if cursor.goto_next_sibling()
|
||||
&& cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT
|
||||
{
|
||||
// because HEADING_CONTENT node includes the space after the heading marker
|
||||
// offset by 1 to exclude the space
|
||||
block_for_node(&mut cursor, &self.content, 1, theme)
|
||||
} else {
|
||||
ContentBlock::Text {
|
||||
decoration: None,
|
||||
text: gpui::SharedString::new(&self.content[current_node.byte_range()]),
|
||||
highlights: Vec::new(),
|
||||
links: Vec::new(),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
| MARKDOWN_KIND_ID_ATX_HEADING => {
|
||||
if !cursor.goto_first_child() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
let marker_node_kind = cursor.node().kind_id();
|
||||
|
||||
if is_first_heading {
|
||||
is_first_heading = false;
|
||||
block = block.mt_0();
|
||||
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);
|
||||
}
|
||||
|
||||
cursor.goto_parent();
|
||||
| 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_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;
|
||||
self.blocks.push(block);
|
||||
}
|
||||
|
||||
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
|
||||
| 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;
|
||||
}
|
||||
|
||||
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(
|
||||
cursor
|
||||
.node()
|
||||
.utf8_text(self.content.as_bytes())
|
||||
.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(),
|
||||
}
|
||||
} 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_sm()
|
||||
.text_color(theme.colors.text)
|
||||
.text_sm();
|
||||
.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);
|
||||
}
|
||||
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() {
|
||||
@@ -533,55 +535,55 @@ impl gpui::Render for MarkdownText {
|
||||
) -> impl gpui::prelude::IntoElement {
|
||||
let children = self.blocks.iter().enumerate().map(|(i, block)| {
|
||||
match block {
|
||||
| ContentBlock::Text {
|
||||
decoration,
|
||||
text,
|
||||
highlights,
|
||||
links,
|
||||
style,
|
||||
} => {
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
|
||||
| ContentBlock::Text {
|
||||
decoration,
|
||||
text,
|
||||
highlights,
|
||||
links,
|
||||
style,
|
||||
} => {
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
|
||||
|
||||
let content = if links.is_empty() {
|
||||
div().w_full().child(styled_text)
|
||||
} else {
|
||||
// if link in block, interactive text is needed
|
||||
// to handle link clicks
|
||||
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
|
||||
let content = if links.is_empty() {
|
||||
div().w_full().child(styled_text)
|
||||
} else {
|
||||
// if link in block, interactive text is needed
|
||||
// to handle link clicks
|
||||
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
|
||||
|
||||
let weak = cx.entity();
|
||||
let t = gpui::InteractiveText::new(i, styled_text).on_click(
|
||||
link_ranges,
|
||||
move |i, _, cx| {
|
||||
if let Some(src) = srcs.get(i) {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.on_open_link(src, cx);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
let weak = cx.entity();
|
||||
let t = gpui::InteractiveText::new(i, styled_text).on_click(
|
||||
link_ranges,
|
||||
move |i, _, cx| {
|
||||
if let Some(src) = srcs.get(i) {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.on_open_link(src, cx);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
div().w_full().child(t)
|
||||
};
|
||||
div().w_full().child(t)
|
||||
};
|
||||
|
||||
let mut div = match decoration {
|
||||
| Some(d) => div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.child(d.clone())
|
||||
.child(div().flex_1().min_w_0().child(content)),
|
||||
| None => div().w_full().child(content),
|
||||
};
|
||||
let mut div = match decoration {
|
||||
| Some(d) => div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.child(d.clone())
|
||||
.child(div().flex_1().min_w_0().child(content)),
|
||||
| None => div().w_full().child(content),
|
||||
};
|
||||
|
||||
div.style().refine(&style);
|
||||
div.style().refine(&style);
|
||||
|
||||
div
|
||||
}
|
||||
div
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
27
src/query.rs
27
src/query.rs
@@ -1,4 +1,4 @@
|
||||
use gpui::{AppContext, BorrowAppContext};
|
||||
use gpui::BorrowAppContext;
|
||||
use std::{any::Any, borrow::Cow, collections::HashMap, marker::PhantomData, ops::Deref};
|
||||
|
||||
pub(crate) trait QueryFn: Clone + 'static {
|
||||
@@ -187,19 +187,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe_query<E, F>(
|
||||
query: &Entity<F>,
|
||||
mut on_notify: impl FnMut(&mut E, &Entity<F>, &mut gpui::Context<E>) + 'static,
|
||||
cx: &mut gpui::Context<E>,
|
||||
) -> gpui::Subscription
|
||||
pub fn watch_query<E, F, H>(query: &Entity<F>, on_notify: H, cx: &mut gpui::Context<E>) -> gpui::Subscription
|
||||
where
|
||||
E: 'static,
|
||||
F: QueryFn,
|
||||
H: Fn(&mut E, &Entity<F>, &mut gpui::Context<E>) + Clone + 'static,
|
||||
{
|
||||
let q = query.clone();
|
||||
cx.observe(&query, move |this, _, cx| {
|
||||
on_notify(this, &q, cx);
|
||||
let observed_query = query.clone();
|
||||
let sub = cx.observe(query, {
|
||||
let on_notify = on_notify.clone();
|
||||
move |this, _, cx| on_notify(this, &observed_query, cx)
|
||||
});
|
||||
|
||||
let initial_query = query.clone();
|
||||
cx.spawn({
|
||||
let on_notify = on_notify.clone();
|
||||
async move |weak, cx| {
|
||||
let _ = weak.update(cx, |this, cx| on_notify(this, &initial_query, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
sub
|
||||
}
|
||||
|
||||
// ================= Store ==================
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
font_icon::{FontIcon, FontIconSvg, font_icon},
|
||||
text::text,
|
||||
},
|
||||
query::{self, QueryStatus, read_query, use_query},
|
||||
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||
util::str::ToSharedString,
|
||||
};
|
||||
|
||||
@@ -56,28 +56,44 @@ pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
|
||||
|
||||
impl IssueList {
|
||||
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
cx.observe(&self.pr_query, |this, _, cx| {
|
||||
let data = read_query(&this.pr_query, cx);
|
||||
if let QueryStatus::Loaded(res) = data {
|
||||
let old_len = this.list_state.item_count();
|
||||
let new_len = res.items.len();
|
||||
let pr_query = self.pr_query.clone();
|
||||
|
||||
let new_items = res.items.iter().enumerate().map(|(i, it)| IssueListItem {
|
||||
watch_query(&pr_query, Self::sync_pr_query, cx).detach();
|
||||
}
|
||||
|
||||
fn sync_pr_query(
|
||||
&mut self,
|
||||
query: &query::Entity<api::issues::ListPullRequests>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
let data = read_query(query, cx);
|
||||
if let QueryStatus::Loaded(res) = data {
|
||||
let selected_id = self
|
||||
.list_items
|
||||
.iter()
|
||||
.find(|item| item.is_selected)
|
||||
.map(|item| item.id.clone());
|
||||
let old_len = self.list_state.item_count();
|
||||
let new_len = res.items.len();
|
||||
|
||||
self.list_items = res
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, it)| IssueListItem {
|
||||
is_selected: selected_id.as_ref().is_some_and(|id| *id == it.id),
|
||||
id: it.id.clone(),
|
||||
repo_name: Some(it.repo_slug.to_shared_string()),
|
||||
title: it.title.to_shared_string(),
|
||||
description: None,
|
||||
status: it.state,
|
||||
is_selected: false,
|
||||
is_last: i == new_len - 1,
|
||||
is_draft: it.is_draft,
|
||||
});
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.list_items.splice(old_len..old_len, new_items);
|
||||
this.list_state.splice(old_len..old_len, new_len);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.list_state.splice(0..old_len, new_len);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_item_click(&mut self, i: usize, cx: &mut gpui::Context<Self>) {
|
||||
|
||||
@@ -6,9 +6,10 @@ use crate::{
|
||||
diff_view::{DiffViewContent, DiffViewState, diff_view},
|
||||
text::text,
|
||||
},
|
||||
query::{self, QueryStatus, observe_query, read_query, use_query},
|
||||
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||
util,
|
||||
};
|
||||
use gpui::{ParentElement, Styled, div};
|
||||
use gpui::{AppContext, ParentElement, Styled, div};
|
||||
|
||||
pub(crate) struct PullRequestDiffView {
|
||||
selected_file_path: Option<Arc<str>>,
|
||||
@@ -104,23 +105,68 @@ impl PullRequestDiffView {
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
_ = observe_query(
|
||||
&content_diff_query,
|
||||
|this, query, cx| {
|
||||
if let QueryStatus::Loaded(diff) = read_query(query, cx) {
|
||||
println!("diff len {}", diff.len());
|
||||
this.diff_view_state.reset(diff.len());
|
||||
this.diff_view_content = Some(Arc::clone(diff).into());
|
||||
}
|
||||
cx.notify();
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
_ = watch_query(&content_diff_query, Self::sync_content_diff_query, cx).detach();
|
||||
|
||||
self.content_diff_query = Some(content_diff_query);
|
||||
}
|
||||
|
||||
fn sync_content_diff_query(
|
||||
&mut self,
|
||||
query: &query::Entity<api::repo::FetchFileDiff>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
if let Some(diff) = {
|
||||
match read_query(query, cx) {
|
||||
| QueryStatus::Loaded(diff) => Some(Arc::clone(diff)),
|
||||
| _ => None,
|
||||
}
|
||||
} {
|
||||
self.load_diff_view(diff, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn load_diff_view(
|
||||
&mut self,
|
||||
content_diff: Arc<util::diff::ContentDiff>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
let theme = app::current_theme(cx);
|
||||
let old_content = content_diff.old_content.clone();
|
||||
let new_content = content_diff.new_content.clone();
|
||||
|
||||
self.diff_view_state.reset(content_diff.len());
|
||||
self.diff_view_content = Some(content_diff.into());
|
||||
|
||||
let theme_syntax = theme.syntax;
|
||||
|
||||
if let Some(path) = &self.selected_file_path {
|
||||
let path = Arc::clone(&path);
|
||||
let file_type = util::file::file_type_from_path(&path);
|
||||
|
||||
let t1 = cx.background_spawn(async move {
|
||||
util::syntax_highlight::highlight_content(old_content, file_type, &theme_syntax)
|
||||
});
|
||||
let t2 = cx.background_spawn(async move {
|
||||
util::syntax_highlight::highlight_content(new_content, file_type, &theme_syntax)
|
||||
});
|
||||
|
||||
_ = cx
|
||||
.spawn(async move |weak, cx| match tokio::join!(t1, t2) {
|
||||
| (Some(old_side_highlights), Some(new_side_highlights)) => {
|
||||
_ = weak.update(cx, |this, cx| {
|
||||
this.diff_view_state
|
||||
.set_old_side_highlights(old_side_highlights);
|
||||
this.diff_view_state
|
||||
.set_new_side_highlights(new_side_highlights);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
| _ => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for PullRequestDiffView {
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
markdown::{self, MarkdownText},
|
||||
text::text,
|
||||
},
|
||||
query::{self, QueryStatus, read_query, use_query},
|
||||
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||
screen::dashboard::pull_request_diff_view::{self, PullRequestDiffView},
|
||||
};
|
||||
|
||||
@@ -46,21 +46,20 @@ impl PullRequestView {
|
||||
|
||||
self.pull_request_query = Some(query.clone());
|
||||
|
||||
_ = cx
|
||||
.observe(&query.clone(), move |this, _, cx| {
|
||||
this.load_markdown_content(cx);
|
||||
this.load_pr_diff(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
// cached query will not trigger observe callback
|
||||
// this is required so that content is loaded immediately for cached query
|
||||
self.load_markdown_content(cx);
|
||||
self.load_pr_diff(cx);
|
||||
_ = watch_query(&query, Self::sync_pull_request_query, cx).detach();
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn sync_pull_request_query(
|
||||
&mut self,
|
||||
_query: &query::Entity<api::issues::FetchPullRequest>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
self.load_markdown_content(cx);
|
||||
self.load_pr_diff(cx);
|
||||
}
|
||||
|
||||
fn load_markdown_content(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
let Some(query) = &self.pull_request_query else {
|
||||
return;
|
||||
@@ -115,41 +114,41 @@ impl PullRequestView {
|
||||
.rounded_full();
|
||||
|
||||
match pr.state {
|
||||
| api::issues::PullRequestState::Open => {
|
||||
status_pill = status_pill
|
||||
.bg(theme.colors.success_solid)
|
||||
.child(
|
||||
font_icon(FontIcon::PullRequestArrow)
|
||||
.size_3()
|
||||
.text_color(theme.colors.success_on_solid),
|
||||
)
|
||||
.child(
|
||||
text("Open")
|
||||
.text_color(theme.colors.success_on_solid)
|
||||
| api::issues::PullRequestState::Open => {
|
||||
status_pill = status_pill
|
||||
.bg(theme.colors.success_solid)
|
||||
.child(
|
||||
font_icon(FontIcon::PullRequestArrow)
|
||||
.size_3()
|
||||
.text_color(theme.colors.success_on_solid),
|
||||
)
|
||||
.child(
|
||||
text("Open")
|
||||
.text_color(theme.colors.success_on_solid)
|
||||
.text_xs(),
|
||||
);
|
||||
}
|
||||
| api::issues::PullRequestState::Closed => {
|
||||
status_pill = status_pill
|
||||
.bg(theme.colors.danger_solid)
|
||||
.child(
|
||||
font_icon(FontIcon::PullRequestClosed)
|
||||
.size_3()
|
||||
.text_color(theme.colors.danger_on_solid),
|
||||
)
|
||||
.child(
|
||||
text("Closed")
|
||||
.text_color(theme.colors.danger_on_solid)
|
||||
.text_xs(),
|
||||
);
|
||||
}
|
||||
| api::issues::PullRequestState::Merged => {
|
||||
status_pill = status_pill.bg(theme.colors.accent_solid).child(
|
||||
text("Merged")
|
||||
.text_color(theme.colors.accent_on_solid)
|
||||
.text_xs(),
|
||||
);
|
||||
}
|
||||
| api::issues::PullRequestState::Closed => {
|
||||
status_pill = status_pill
|
||||
.bg(theme.colors.danger_solid)
|
||||
.child(
|
||||
font_icon(FontIcon::PullRequestClosed)
|
||||
.size_3()
|
||||
.text_color(theme.colors.danger_on_solid),
|
||||
)
|
||||
.child(
|
||||
text("Closed")
|
||||
.text_color(theme.colors.danger_on_solid)
|
||||
.text_xs(),
|
||||
);
|
||||
}
|
||||
| api::issues::PullRequestState::Merged => {
|
||||
status_pill = status_pill.bg(theme.colors.accent_solid).child(
|
||||
text("Merged")
|
||||
.text_color(theme.colors.accent_on_solid)
|
||||
.text_xs(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let merge_text = pr.author.as_ref().map(|author| {
|
||||
@@ -284,23 +283,23 @@ impl gpui::Render for PullRequestView {
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
div().size_full().child(match &self.pull_request_query {
|
||||
| Some(q) => match read_query(q, cx) {
|
||||
| QueryStatus::Loaded(pr) => match &self.diff_view {
|
||||
| Some(v) => v.clone().into_any_element(),
|
||||
| None => self.pr_content(pr, cx),
|
||||
},
|
||||
| Some(q) => match read_query(q, cx) {
|
||||
| QueryStatus::Loaded(pr) => match &self.diff_view {
|
||||
| Some(v) => v.clone().into_any_element(),
|
||||
| None => self.pr_content(pr, cx),
|
||||
},
|
||||
|
||||
| QueryStatus::Err(e) => div()
|
||||
.size_full()
|
||||
.child(format!("{:?}", e))
|
||||
.into_any_element(),
|
||||
| QueryStatus::Loading => div()
|
||||
.size_full()
|
||||
.child("loading pr content")
|
||||
.into_any_element(),
|
||||
},
|
||||
| QueryStatus::Err(e) => div()
|
||||
.size_full()
|
||||
.child(format!("{:?}", e))
|
||||
.into_any_element(),
|
||||
| QueryStatus::Loading => div()
|
||||
.size_full()
|
||||
.child("loading pr content")
|
||||
.into_any_element(),
|
||||
},
|
||||
|
||||
| None => div().size_full().child("no pr selected").into_any_element(),
|
||||
| None => div().size_full().child("no pr selected").into_any_element(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ impl Screen {
|
||||
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
_ = cx
|
||||
.subscribe(&self.issue_list, |this, _, event, cx| match event {
|
||||
| issue_list::Event::ItemSelected(pr_id) => {
|
||||
this.handle_issue_list_item_selected(pr_id, cx);
|
||||
}
|
||||
| issue_list::Event::ItemSelected(pr_id) => {
|
||||
this.handle_issue_list_item_selected(pr_id, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ use std::{ops::Range, sync::Arc};
|
||||
use bytes::Bytes;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, InteractiveElement, IntoElement, ParentElement,
|
||||
StatefulInteractiveElement,
|
||||
Styled, div, point, px, size,
|
||||
StatefulInteractiveElement, Styled, div, point, px, size,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -54,6 +53,8 @@ pub(crate) struct Screen {
|
||||
struct DiffCase {
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
old_line_count: usize,
|
||||
new_line_count: usize,
|
||||
old_lines: Vec<SourceLine>,
|
||||
new_lines: Vec<SourceLine>,
|
||||
op_groups: Vec<OpGroup>,
|
||||
@@ -233,8 +234,8 @@ impl gpui::Render for Screen {
|
||||
text(format!(
|
||||
"{} ops, {} old lines, {} new lines",
|
||||
case.op_groups.len(),
|
||||
line_count(&case.old_lines),
|
||||
line_count(&case.new_lines),
|
||||
case.old_line_count,
|
||||
case.new_line_count,
|
||||
))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
@@ -252,11 +253,11 @@ impl gpui::Render for Screen {
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.child(
|
||||
panel_header("Old", line_count(&case.old_lines), theme)
|
||||
panel_header("Old", case.old_line_count, theme)
|
||||
.flex_1(),
|
||||
)
|
||||
.child(
|
||||
panel_header("New", line_count(&case.new_lines), theme)
|
||||
panel_header("New", case.new_line_count, theme)
|
||||
.flex_1(),
|
||||
),
|
||||
)
|
||||
@@ -276,6 +277,8 @@ impl gpui::Render for Screen {
|
||||
.child(render_source_content(
|
||||
&case.old_lines,
|
||||
&case.new_lines,
|
||||
case.old_line_count,
|
||||
case.new_line_count,
|
||||
theme,
|
||||
))
|
||||
.child(text("Diff Rows Render").text_sm())
|
||||
@@ -377,6 +380,8 @@ impl DiffCase {
|
||||
Self {
|
||||
title,
|
||||
description,
|
||||
old_line_count: diff.old_line_count,
|
||||
new_line_count: diff.new_line_count,
|
||||
old_lines: collect_source_lines(&diff, SourceSide::Old),
|
||||
new_lines: collect_source_lines(&diff, SourceSide::New),
|
||||
op_groups: collect_op_groups(&diff),
|
||||
@@ -408,23 +413,24 @@ fn panel_header(label: &'static str, line_count: usize, theme: &crate::theme::Th
|
||||
fn render_source_content(
|
||||
old_lines: &[SourceLine],
|
||||
new_lines: &[SourceLine],
|
||||
old_line_count: usize,
|
||||
new_line_count: usize,
|
||||
theme: &crate::theme::Theme,
|
||||
) -> gpui::Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_2()
|
||||
.child(render_source_panel("Old Content", old_lines, theme).flex_1())
|
||||
.child(render_source_panel("New Content", new_lines, theme).flex_1())
|
||||
.child(render_source_panel("Old Content", old_lines, old_line_count, theme).flex_1())
|
||||
.child(render_source_panel("New Content", new_lines, new_line_count, theme).flex_1())
|
||||
}
|
||||
|
||||
fn render_source_panel(
|
||||
title: &'static str,
|
||||
lines: &[SourceLine],
|
||||
line_count: usize,
|
||||
theme: &crate::theme::Theme,
|
||||
) -> gpui::Div {
|
||||
let line_count = line_count(lines);
|
||||
|
||||
let rows: Vec<AnyElement> = lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
@@ -573,7 +579,7 @@ fn render_row(op_index: usize, row: &DiffLine, theme: &crate::theme::Theme) -> g
|
||||
.child(render_line_cell(
|
||||
op_index,
|
||||
row.op,
|
||||
row.old_content.as_ref().map(|_| row.old_line),
|
||||
row.old_line,
|
||||
row.old_content.as_deref().map(display_text),
|
||||
true,
|
||||
theme,
|
||||
@@ -581,7 +587,7 @@ fn render_row(op_index: usize, row: &DiffLine, theme: &crate::theme::Theme) -> g
|
||||
.child(render_line_cell(
|
||||
op_index,
|
||||
row.op,
|
||||
row.new_content.as_ref().map(|_| row.new_line),
|
||||
row.new_line,
|
||||
row.new_content.as_deref().map(display_text),
|
||||
false,
|
||||
theme,
|
||||
@@ -661,10 +667,6 @@ fn display_text(text: &str) -> String {
|
||||
rendered
|
||||
}
|
||||
|
||||
fn line_count(lines: &[SourceLine]) -> usize {
|
||||
lines.last().map(|line| line.line_number + 1).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn collect_source_lines(diff: &ContentDiff, side: SourceSide) -> Vec<SourceLine> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
@@ -673,17 +675,17 @@ fn collect_source_lines(diff: &ContentDiff, side: SourceSide) -> Vec<SourceLine>
|
||||
|
||||
match side {
|
||||
| SourceSide::Old => {
|
||||
if let Some(content) = &row.old_content {
|
||||
if let (Some(line_number), Some(content)) = (row.old_line, &row.old_content) {
|
||||
lines.push(SourceLine {
|
||||
line_number: row.old_line,
|
||||
line_number,
|
||||
content: Arc::clone(content),
|
||||
});
|
||||
}
|
||||
}
|
||||
| SourceSide::New => {
|
||||
if let Some(content) = &row.new_content {
|
||||
if let (Some(line_number), Some(content)) = (row.new_line, &row.new_content) {
|
||||
lines.push(SourceLine {
|
||||
line_number: row.new_line,
|
||||
line_number,
|
||||
content: Arc::clone(content),
|
||||
});
|
||||
}
|
||||
@@ -710,8 +712,8 @@ fn collect_op_groups(diff: &ContentDiff) -> Vec<OpGroup> {
|
||||
|
||||
groups.push(OpGroup {
|
||||
op,
|
||||
old_range: group_range(&rows, SourceSide::Old),
|
||||
new_range: group_range(&rows, SourceSide::New),
|
||||
old_range: group_range(diff, start, end, SourceSide::Old),
|
||||
new_range: group_range(diff, start, end, SourceSide::New),
|
||||
rows,
|
||||
});
|
||||
|
||||
@@ -721,18 +723,13 @@ fn collect_op_groups(diff: &ContentDiff) -> Vec<OpGroup> {
|
||||
groups
|
||||
}
|
||||
|
||||
fn group_range(rows: &[DiffLine], side: SourceSide) -> Range<usize> {
|
||||
let anchor = match side {
|
||||
| SourceSide::Old => rows.first().map(|row| row.old_line).unwrap_or(0),
|
||||
| SourceSide::New => rows.first().map(|row| row.new_line).unwrap_or(0),
|
||||
};
|
||||
|
||||
fn group_range(diff: &ContentDiff, start: usize, end: usize, side: SourceSide) -> Range<usize> {
|
||||
let mut first = None;
|
||||
let mut last = None;
|
||||
|
||||
for line_number in rows.iter().filter_map(|row| match side {
|
||||
| SourceSide::Old => row.old_content.as_ref().map(|_| row.old_line),
|
||||
| SourceSide::New => row.new_content.as_ref().map(|_| row.new_line),
|
||||
for line_number in (start..end).filter_map(|index| match side {
|
||||
| SourceSide::Old => diff.get(index).old_line,
|
||||
| SourceSide::New => diff.get(index).new_line,
|
||||
}) {
|
||||
if first.is_none() {
|
||||
first = Some(line_number);
|
||||
@@ -742,10 +739,31 @@ fn group_range(rows: &[DiffLine], side: SourceSide) -> Range<usize> {
|
||||
|
||||
match (first, last) {
|
||||
| (Some(start), Some(end)) => start..end + 1,
|
||||
| _ => anchor..anchor,
|
||||
| _ => {
|
||||
let anchor = group_anchor(diff, start, end, side);
|
||||
anchor..anchor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn group_anchor(diff: &ContentDiff, start: usize, end: usize, side: SourceSide) -> usize {
|
||||
if let Some(line_number) = (end..diff.len()).find_map(|index| match side {
|
||||
| SourceSide::Old => diff.get(index).old_line,
|
||||
| SourceSide::New => diff.get(index).new_line,
|
||||
}) {
|
||||
return line_number;
|
||||
}
|
||||
|
||||
if let Some(line_number) = (0..start).rev().find_map(|index| match side {
|
||||
| SourceSide::Old => diff.get(index).old_line,
|
||||
| SourceSide::New => diff.get(index).new_line,
|
||||
}) {
|
||||
return line_number + 1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
struct Colors {
|
||||
background: gpui::Rgba,
|
||||
border: gpui::Rgba,
|
||||
|
||||
@@ -183,60 +183,60 @@ impl GithubStepView {
|
||||
let poll_interval = u64::from(*interval);
|
||||
|
||||
match read_query(query, cx) {
|
||||
| QueryStatus::Loaded(data) => {
|
||||
let auth_tokens = api::AuthTokens {
|
||||
access_token: data.access_token.clone(),
|
||||
};
|
||||
|
||||
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
|
||||
store.update_query_context(|c| {
|
||||
c.auth = Some(auth_tokens.clone());
|
||||
});
|
||||
});
|
||||
|
||||
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(""))
|
||||
| QueryStatus::Loaded(data) => {
|
||||
let auth_tokens = api::AuthTokens {
|
||||
access_token: data.access_token.clone(),
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
| QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
|
||||
if error == "authorization_pending" {
|
||||
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
|
||||
store.update_query_context(|c| {
|
||||
c.auth = Some(auth_tokens.clone());
|
||||
});
|
||||
});
|
||||
|
||||
self.user_query = Some(use_query(api::user::Fetch, cx));
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
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| {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
| _ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,8 +257,8 @@ impl GithubStepView {
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
let (displayed_code, copyable_code) = match create_device_code_query {
|
||||
| QueryStatus::Loaded(data) => (data.user_code.as_ref(), Some(data.user_code.clone())),
|
||||
| _ => (self.placeholder_code.as_str(), None),
|
||||
| QueryStatus::Loaded(data) => (data.user_code.as_ref(), Some(data.user_code.clone())),
|
||||
| _ => (self.placeholder_code.as_str(), None),
|
||||
};
|
||||
|
||||
let border_color = theme.colors.border.clone();
|
||||
@@ -352,14 +352,16 @@ impl gpui::Render for GithubStepView {
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let (can_go_next, header, body) = match self.user_query {
|
||||
| None => (false, self.header(), self.device_code_area(cx)),
|
||||
| Some(ref q) => {
|
||||
let user_query = read_query(q, cx);
|
||||
match user_query {
|
||||
| QueryStatus::Loaded(user) => (true, connected_header(), connected_body(user, cx)),
|
||||
| _ => (false, self.header(), self.device_code_area(cx)),
|
||||
| None => (false, self.header(), self.device_code_area(cx)),
|
||||
| Some(ref q) => {
|
||||
let user_query = read_query(q, cx);
|
||||
match user_query {
|
||||
| QueryStatus::Loaded(user) => {
|
||||
(true, connected_header(), connected_body(user, cx))
|
||||
}
|
||||
| _ => (false, self.header(), self.device_code_area(cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
mod catppuccin;
|
||||
pub(crate) mod syntax;
|
||||
|
||||
use gpui::Rgba;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use syntax::{HIGHLIGHT_NAMES, ThemeSyntax, ThemeSyntaxHighlight};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ThemeMode {
|
||||
Light,
|
||||
@@ -15,6 +19,7 @@ pub struct Theme {
|
||||
pub name: &'static str,
|
||||
pub mode: ThemeMode,
|
||||
pub colors: ThemeColors,
|
||||
pub syntax: ThemeSyntax,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
@@ -103,7 +108,7 @@ impl ThemeFamily {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn theme(self, mode: ThemeMode) -> Theme {
|
||||
pub fn theme(self, mode: ThemeMode) -> Theme {
|
||||
self.variant(mode).theme()
|
||||
}
|
||||
|
||||
@@ -144,7 +149,7 @@ impl ThemeVariant {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn theme(self) -> Theme {
|
||||
pub fn theme(self) -> Theme {
|
||||
match self {
|
||||
| Self::CatppuccinLatte => catppuccin::latte(),
|
||||
| Self::CatppuccinMocha => catppuccin::mocha(),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::colors::{hex, hex_alpha};
|
||||
|
||||
use super::{Theme, ThemeColors, ThemeMode};
|
||||
use super::{
|
||||
Theme, ThemeColors, ThemeMode, ThemeSyntaxHighlight,
|
||||
syntax::{build, key},
|
||||
};
|
||||
|
||||
pub(crate) const FAMILY_ID: &str = "catppuccin";
|
||||
pub(crate) const FAMILY_LABEL: &str = "Catppuccin";
|
||||
@@ -8,7 +11,31 @@ pub(crate) const FAMILY_LABEL: &str = "Catppuccin";
|
||||
pub(crate) const LATTE_LABEL: &str = "Catppuccin Latte";
|
||||
pub(crate) const MOCHA_LABEL: &str = "Catppuccin Mocha";
|
||||
|
||||
pub(crate) const fn latte() -> Theme {
|
||||
const fn highlight(color: u32) -> ThemeSyntaxHighlight {
|
||||
ThemeSyntaxHighlight {
|
||||
color: hex(color),
|
||||
font_style: gpui::FontStyle::Normal,
|
||||
font_weight: None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn highlight_italic(color: u32) -> ThemeSyntaxHighlight {
|
||||
ThemeSyntaxHighlight {
|
||||
color: hex(color),
|
||||
font_style: gpui::FontStyle::Italic,
|
||||
font_weight: None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn highlight_weight(color: u32, font_weight: gpui::FontWeight) -> ThemeSyntaxHighlight {
|
||||
ThemeSyntaxHighlight {
|
||||
color: hex(color),
|
||||
font_style: gpui::FontStyle::Normal,
|
||||
font_weight: Some(font_weight),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn latte() -> Theme {
|
||||
Theme {
|
||||
id: "catppuccin-latte",
|
||||
name: LATTE_LABEL,
|
||||
@@ -61,10 +88,63 @@ pub(crate) const fn latte() -> Theme {
|
||||
info_solid: hex(0x1e66f5),
|
||||
info_on_solid: hex(0xeff1f5),
|
||||
},
|
||||
syntax: build([
|
||||
(key::ATTRIBUTE, highlight(0x179299)),
|
||||
(key::BOOLEAN, highlight(0xfe640b)),
|
||||
(key::COMMENT, highlight(0x7c7f93)),
|
||||
(key::COMMENT_DOC, highlight(0x8c8fa1)),
|
||||
(key::CONSTANT, highlight(0xfe640b)),
|
||||
(key::CONSTRUCTOR, highlight(0x209fb5)),
|
||||
(key::EMBEDDED, highlight(0x4c4f69)),
|
||||
(key::EMPHASIS, highlight(0x1e66f5)),
|
||||
(
|
||||
key::EMPHASIS_STRONG,
|
||||
highlight_weight(0xdf8e1d, gpui::FontWeight::BOLD),
|
||||
),
|
||||
(key::ENUM, highlight(0x179299)),
|
||||
(key::FUNCTION, highlight(0x1e66f5)),
|
||||
(key::HINT, highlight(0x9ca0b0)),
|
||||
(key::KEYWORD, highlight(0x8839ef)),
|
||||
(key::LABEL, highlight(0x7287fd)),
|
||||
(key::LINK_TEXT, highlight_italic(0x1e66f5)),
|
||||
(key::LINK_URI, highlight(0x179299)),
|
||||
(key::NAMESPACE, highlight(0x4c4f69)),
|
||||
(key::NUMBER, highlight(0xfe640b)),
|
||||
(key::OPERATOR, highlight(0x04a5e5)),
|
||||
(key::PREDICTIVE, highlight_italic(0x9ca0b0)),
|
||||
(key::PREPROC, highlight(0x8839ef)),
|
||||
(key::PRIMARY, highlight(0x4c4f69)),
|
||||
(key::PROPERTY, highlight(0xdc8a78)),
|
||||
(key::PUNCTUATION, highlight(0x6c6f85)),
|
||||
(key::PUNCTUATION_BRACKET, highlight(0x7c7f93)),
|
||||
(key::PUNCTUATION_DELIMITER, highlight(0x7c7f93)),
|
||||
(key::PUNCTUATION_LIST_MARKER, highlight(0xd20f39)),
|
||||
(key::PUNCTUATION_MARKUP, highlight(0xd20f39)),
|
||||
(key::PUNCTUATION_SPECIAL, highlight(0xe64553)),
|
||||
(key::SELECTOR, highlight(0x40a02b)),
|
||||
(key::SELECTOR_PSEUDO, highlight(0x1e66f5)),
|
||||
(key::STRING, highlight(0x40a02b)),
|
||||
(key::STRING_ESCAPE, highlight(0xea76cb)),
|
||||
(key::STRING_REGEX, highlight(0xfe640b)),
|
||||
(key::STRING_SPECIAL, highlight(0xfe640b)),
|
||||
(key::STRING_SPECIAL_SYMBOL, highlight(0xfe640b)),
|
||||
(key::TAG, highlight(0x1e66f5)),
|
||||
(key::TEXT_LITERAL, highlight(0x40a02b)),
|
||||
(
|
||||
key::TITLE,
|
||||
highlight_weight(0xdc8a78, gpui::FontWeight::NORMAL),
|
||||
),
|
||||
(key::TYPE, highlight(0xdf8e1d)),
|
||||
(key::VARIABLE, highlight(0x4c4f69)),
|
||||
(key::VARIABLE_SPECIAL, highlight(0xfe640b)),
|
||||
(key::VARIANT, highlight(0x7287fd)),
|
||||
(key::DIFF_PLUS, highlight(0x40a02b)),
|
||||
(key::DIFF_MINUS, highlight(0xd20f39)),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn mocha() -> Theme {
|
||||
pub(crate) fn mocha() -> Theme {
|
||||
Theme {
|
||||
id: "catppuccin-mocha",
|
||||
name: MOCHA_LABEL,
|
||||
@@ -117,5 +197,58 @@ pub(crate) const fn mocha() -> Theme {
|
||||
info_solid: hex(0x89b4fa),
|
||||
info_on_solid: hex(0x1e1e2e),
|
||||
},
|
||||
syntax: build([
|
||||
(key::ATTRIBUTE, highlight(0x94e2d5)),
|
||||
(key::BOOLEAN, highlight(0xfab387)),
|
||||
(key::COMMENT, highlight(0x6c7086)),
|
||||
(key::COMMENT_DOC, highlight(0x7f849c)),
|
||||
(key::CONSTANT, highlight(0xfab387)),
|
||||
(key::CONSTRUCTOR, highlight(0x74c7ec)),
|
||||
(key::EMBEDDED, highlight(0xcdd6f4)),
|
||||
(key::EMPHASIS, highlight(0x89b4fa)),
|
||||
(
|
||||
key::EMPHASIS_STRONG,
|
||||
highlight_weight(0xf9e2af, gpui::FontWeight::BOLD),
|
||||
),
|
||||
(key::ENUM, highlight(0x94e2d5)),
|
||||
(key::FUNCTION, highlight(0x89b4fa)),
|
||||
(key::HINT, highlight(0x9399b2)),
|
||||
(key::KEYWORD, highlight(0xcba6f7)),
|
||||
(key::LABEL, highlight(0xb4befe)),
|
||||
(key::LINK_TEXT, highlight_italic(0x89b4fa)),
|
||||
(key::LINK_URI, highlight(0x94e2d5)),
|
||||
(key::NAMESPACE, highlight(0xcdd6f4)),
|
||||
(key::NUMBER, highlight(0xfab387)),
|
||||
(key::OPERATOR, highlight(0x89dceb)),
|
||||
(key::PREDICTIVE, highlight_italic(0x9399b2)),
|
||||
(key::PREPROC, highlight(0xcba6f7)),
|
||||
(key::PRIMARY, highlight(0xcdd6f4)),
|
||||
(key::PROPERTY, highlight(0xf5e0dc)),
|
||||
(key::PUNCTUATION, highlight(0xa6adc8)),
|
||||
(key::PUNCTUATION_BRACKET, highlight(0x9399b2)),
|
||||
(key::PUNCTUATION_DELIMITER, highlight(0x9399b2)),
|
||||
(key::PUNCTUATION_LIST_MARKER, highlight(0xf38ba8)),
|
||||
(key::PUNCTUATION_MARKUP, highlight(0xf38ba8)),
|
||||
(key::PUNCTUATION_SPECIAL, highlight(0xeba0ac)),
|
||||
(key::SELECTOR, highlight(0xa6e3a1)),
|
||||
(key::SELECTOR_PSEUDO, highlight(0x89b4fa)),
|
||||
(key::STRING, highlight(0xa6e3a1)),
|
||||
(key::STRING_ESCAPE, highlight(0xf5c2e7)),
|
||||
(key::STRING_REGEX, highlight(0xfab387)),
|
||||
(key::STRING_SPECIAL, highlight(0xfab387)),
|
||||
(key::STRING_SPECIAL_SYMBOL, highlight(0xfab387)),
|
||||
(key::TAG, highlight(0x89b4fa)),
|
||||
(key::TEXT_LITERAL, highlight(0xa6e3a1)),
|
||||
(
|
||||
key::TITLE,
|
||||
highlight_weight(0xf5e0dc, gpui::FontWeight::NORMAL),
|
||||
),
|
||||
(key::TYPE, highlight(0xf9e2af)),
|
||||
(key::VARIABLE, highlight(0xcdd6f4)),
|
||||
(key::VARIABLE_SPECIAL, highlight(0xfab387)),
|
||||
(key::VARIANT, highlight(0xb4befe)),
|
||||
(key::DIFF_PLUS, highlight(0xa6e3a1)),
|
||||
(key::DIFF_MINUS, highlight(0xf38ba8)),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
165
src/theme/syntax.rs
Normal file
165
src/theme/syntax.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use gpui::Rgba;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ThemeSyntaxHighlight {
|
||||
pub color: Rgba,
|
||||
pub font_style: gpui::FontStyle,
|
||||
pub font_weight: Option<gpui::FontWeight>,
|
||||
}
|
||||
|
||||
pub const HIGHLIGHT_NAME_COUNT: usize = HIGHLIGHT_NAMES.len();
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ThemeSyntax([ThemeSyntaxHighlight; HIGHLIGHT_NAME_COUNT]);
|
||||
|
||||
impl ThemeSyntax {
|
||||
pub fn resolve(&self, key: &str) -> ThemeSyntaxHighlight {
|
||||
Self::index_of(key)
|
||||
.and_then(|index| self.get(index))
|
||||
.unwrap_or_else(|| self.text_literal())
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<ThemeSyntaxHighlight> {
|
||||
self.0.get(index).copied()
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[ThemeSyntaxHighlight] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn text_literal(&self) -> ThemeSyntaxHighlight {
|
||||
self.get(Self::index_of(key::TEXT_LITERAL).expect("missing text.literal index"))
|
||||
.expect("theme syntax must define text.literal")
|
||||
}
|
||||
|
||||
fn index_of(key: &str) -> Option<usize> {
|
||||
HIGHLIGHT_NAMES
|
||||
.iter()
|
||||
.position(|candidate| *candidate == key)
|
||||
}
|
||||
}
|
||||
|
||||
pub const HIGHLIGHT_NAMES: &[&str] = &[
|
||||
key::ATTRIBUTE,
|
||||
key::BOOLEAN,
|
||||
key::COMMENT,
|
||||
key::COMMENT_DOC,
|
||||
key::CONSTANT,
|
||||
key::CONSTRUCTOR,
|
||||
key::DIFF_MINUS,
|
||||
key::DIFF_PLUS,
|
||||
key::EMBEDDED,
|
||||
key::EMPHASIS,
|
||||
key::EMPHASIS_STRONG,
|
||||
key::ENUM,
|
||||
key::FUNCTION,
|
||||
key::HINT,
|
||||
key::KEYWORD,
|
||||
key::LABEL,
|
||||
key::LINK_TEXT,
|
||||
key::LINK_URI,
|
||||
key::NAMESPACE,
|
||||
key::NUMBER,
|
||||
key::OPERATOR,
|
||||
key::PREDICTIVE,
|
||||
key::PREPROC,
|
||||
key::PRIMARY,
|
||||
key::PROPERTY,
|
||||
key::PUNCTUATION,
|
||||
key::PUNCTUATION_BRACKET,
|
||||
key::PUNCTUATION_DELIMITER,
|
||||
key::PUNCTUATION_LIST_MARKER,
|
||||
key::PUNCTUATION_MARKUP,
|
||||
key::PUNCTUATION_SPECIAL,
|
||||
key::SELECTOR,
|
||||
key::SELECTOR_PSEUDO,
|
||||
key::STRING,
|
||||
key::STRING_ESCAPE,
|
||||
key::STRING_REGEX,
|
||||
key::STRING_SPECIAL,
|
||||
key::STRING_SPECIAL_SYMBOL,
|
||||
key::TAG,
|
||||
key::TEXT_LITERAL,
|
||||
key::TITLE,
|
||||
key::TYPE,
|
||||
key::VARIABLE,
|
||||
key::VARIABLE_SPECIAL,
|
||||
key::VARIANT,
|
||||
];
|
||||
|
||||
pub(crate) fn build(
|
||||
entries: impl IntoIterator<Item = (&'static str, ThemeSyntaxHighlight)>,
|
||||
) -> ThemeSyntax {
|
||||
let syntax_by_name: BTreeMap<&'static str, ThemeSyntaxHighlight> = entries
|
||||
.into_iter()
|
||||
.map(|(name, style)| (name, style))
|
||||
.collect();
|
||||
|
||||
debug_assert!(syntax_by_name.contains_key(key::TEXT_LITERAL));
|
||||
debug_assert!(
|
||||
syntax_by_name
|
||||
.keys()
|
||||
.all(|name| HIGHLIGHT_NAMES.contains(name))
|
||||
);
|
||||
|
||||
let fallback = *syntax_by_name
|
||||
.get(key::TEXT_LITERAL)
|
||||
.expect("theme syntax must define text.literal");
|
||||
|
||||
ThemeSyntax(std::array::from_fn(|index| {
|
||||
syntax_by_name
|
||||
.get(HIGHLIGHT_NAMES[index])
|
||||
.copied()
|
||||
.unwrap_or(fallback)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) mod key {
|
||||
pub const ATTRIBUTE: &str = "attribute";
|
||||
pub const BOOLEAN: &str = "boolean";
|
||||
pub const COMMENT: &str = "comment";
|
||||
pub const COMMENT_DOC: &str = "comment.doc";
|
||||
pub const CONSTANT: &str = "constant";
|
||||
pub const CONSTRUCTOR: &str = "constructor";
|
||||
pub const DIFF_MINUS: &str = "diff.minus";
|
||||
pub const DIFF_PLUS: &str = "diff.plus";
|
||||
pub const EMBEDDED: &str = "embedded";
|
||||
pub const EMPHASIS: &str = "emphasis";
|
||||
pub const EMPHASIS_STRONG: &str = "emphasis.strong";
|
||||
pub const ENUM: &str = "enum";
|
||||
pub const FUNCTION: &str = "function";
|
||||
pub const HINT: &str = "hint";
|
||||
pub const KEYWORD: &str = "keyword";
|
||||
pub const LABEL: &str = "label";
|
||||
pub const LINK_TEXT: &str = "link_text";
|
||||
pub const LINK_URI: &str = "link_uri";
|
||||
pub const NAMESPACE: &str = "namespace";
|
||||
pub const NUMBER: &str = "number";
|
||||
pub const OPERATOR: &str = "operator";
|
||||
pub const PREDICTIVE: &str = "predictive";
|
||||
pub const PREPROC: &str = "preproc";
|
||||
pub const PRIMARY: &str = "primary";
|
||||
pub const PROPERTY: &str = "property";
|
||||
pub const PUNCTUATION: &str = "punctuation";
|
||||
pub const PUNCTUATION_BRACKET: &str = "punctuation.bracket";
|
||||
pub const PUNCTUATION_DELIMITER: &str = "punctuation.delimiter";
|
||||
pub const PUNCTUATION_LIST_MARKER: &str = "punctuation.list_marker";
|
||||
pub const PUNCTUATION_MARKUP: &str = "punctuation.markup";
|
||||
pub const PUNCTUATION_SPECIAL: &str = "punctuation.special";
|
||||
pub const SELECTOR: &str = "selector";
|
||||
pub const SELECTOR_PSEUDO: &str = "selector.pseudo";
|
||||
pub const STRING: &str = "string";
|
||||
pub const STRING_ESCAPE: &str = "string.escape";
|
||||
pub const STRING_REGEX: &str = "string.regex";
|
||||
pub const STRING_SPECIAL: &str = "string.special";
|
||||
pub const STRING_SPECIAL_SYMBOL: &str = "string.special.symbol";
|
||||
pub const TAG: &str = "tag";
|
||||
pub const TEXT_LITERAL: &str = "text.literal";
|
||||
pub const TITLE: &str = "title";
|
||||
pub const TYPE: &str = "type";
|
||||
pub const VARIABLE: &str = "variable";
|
||||
pub const VARIABLE_SPECIAL: &str = "variable.special";
|
||||
pub const VARIANT: &str = "variant";
|
||||
}
|
||||
136
src/util/diff.rs
136
src/util/diff.rs
@@ -1,13 +1,8 @@
|
||||
use std::{ops::Range, slice::Iter, sync::Arc, thread::current};
|
||||
use std::sync::Arc;
|
||||
|
||||
use memchr::{memchr2, memchr2_iter, memchr3_iter};
|
||||
use similar::DiffableStr;
|
||||
|
||||
pub(crate) struct Span {
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_range: Range<usize>,
|
||||
pub(crate) new_range: Range<usize>,
|
||||
}
|
||||
use crate::util;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Op {
|
||||
@@ -17,39 +12,32 @@ pub(crate) enum Op {
|
||||
Replace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum DiffSide {
|
||||
Old,
|
||||
New,
|
||||
}
|
||||
|
||||
pub(crate) struct DiffRow {
|
||||
pub(crate) op_index: usize,
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_line: Option<usize>,
|
||||
pub(crate) old_content_range: Option<Range<usize>>,
|
||||
pub(crate) new_line: Option<usize>,
|
||||
pub(crate) new_content_range: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiffLine {
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_content: Option<Arc<str>>,
|
||||
pub(crate) old_line: usize,
|
||||
pub(crate) old_line: Option<usize>,
|
||||
pub(crate) old_byte_range: std::ops::Range<usize>,
|
||||
pub(crate) new_content: Option<Arc<str>>,
|
||||
pub(crate) new_line: usize,
|
||||
pub(crate) new_line: Option<usize>,
|
||||
pub(crate) new_byte_range: std::ops::Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ContentDiff(Vec<DiffLine>);
|
||||
pub(crate) struct ContentDiff {
|
||||
pub(crate) diff_lines: Vec<DiffLine>,
|
||||
pub(crate) old_content: bytes::Bytes,
|
||||
pub(crate) old_line_count: usize,
|
||||
pub(crate) new_content: bytes::Bytes,
|
||||
pub(crate) new_line_count: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn diff_content(
|
||||
old_content: bytes::Bytes,
|
||||
new_content: bytes::Bytes,
|
||||
) -> Option<ContentDiff> {
|
||||
let old_line_ranges = line_ranges(&old_content);
|
||||
let new_line_ranges = line_ranges(&new_content);
|
||||
let old_line_ranges = util::file::line_ranges(&old_content);
|
||||
let new_line_ranges = util::file::line_ranges(&new_content);
|
||||
let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content);
|
||||
|
||||
let mut diff_lines: Vec<DiffLine> = Vec::new();
|
||||
@@ -68,28 +56,30 @@ pub(crate) fn diff_content(
|
||||
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Equal,
|
||||
old_line,
|
||||
old_line: Some(old_line),
|
||||
old_content: Some(Arc::clone(&content)),
|
||||
new_line,
|
||||
old_byte_range: old_line_range.clone(),
|
||||
new_line: Some(new_line),
|
||||
new_content: Some(content),
|
||||
new_byte_range: new_line_ranges[new_line].clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Insert {
|
||||
old_index,
|
||||
new_index,
|
||||
new_len,
|
||||
new_index, new_len, ..
|
||||
} => {
|
||||
for i in 0..new_len {
|
||||
let new_line_range = &new_line_ranges[new_index + i];
|
||||
let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Insert,
|
||||
old_line: old_index,
|
||||
old_line: None,
|
||||
old_content: None,
|
||||
new_line: new_index + i,
|
||||
old_byte_range: 0..0,
|
||||
new_line: Some(new_index + i),
|
||||
new_content: Some(content),
|
||||
new_byte_range: new_line_range.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -108,26 +98,32 @@ pub(crate) fn diff_content(
|
||||
{
|
||||
| (Some(old_range), Some(new_range)) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line,
|
||||
old_line: Some(old_line),
|
||||
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
|
||||
new_line: new_index + i,
|
||||
old_byte_range: old_range.clone(),
|
||||
new_line: Some(new_line),
|
||||
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
|
||||
new_byte_range: new_range.clone(),
|
||||
},
|
||||
|
||||
| (None, Some(new_range)) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line: old_index + old_len,
|
||||
old_line: None,
|
||||
old_content: None,
|
||||
new_line: new_index + i,
|
||||
old_byte_range: 0..0,
|
||||
new_line: Some(new_index + i),
|
||||
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
|
||||
new_byte_range: new_range.clone(),
|
||||
},
|
||||
|
||||
| (Some(old_range), None) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line: old_index + i,
|
||||
old_line: Some(old_index + i),
|
||||
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
|
||||
new_line: new_index + new_len,
|
||||
old_byte_range: old_range.clone(),
|
||||
new_line: None,
|
||||
new_content: None,
|
||||
new_byte_range: 0..0,
|
||||
},
|
||||
|
||||
| (None, None) => {
|
||||
@@ -143,74 +139,44 @@ pub(crate) fn diff_content(
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Delete {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
old_index, old_len, ..
|
||||
} => {
|
||||
for i in 0..old_len {
|
||||
let old_line_range = &old_line_ranges[old_index];
|
||||
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Delete,
|
||||
old_line: old_index + i,
|
||||
old_line: Some(old_index + i),
|
||||
old_content: Some(content),
|
||||
new_line: new_index,
|
||||
old_byte_range: old_line_range.clone(),
|
||||
new_line: None,
|
||||
new_content: None,
|
||||
new_byte_range: 0..0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContentDiff(diff_lines))
|
||||
Some(ContentDiff {
|
||||
diff_lines,
|
||||
old_content,
|
||||
old_line_count: old_line_ranges.len(),
|
||||
new_content,
|
||||
new_line_count: new_line_ranges.len(),
|
||||
})
|
||||
}
|
||||
|
||||
impl ContentDiff {
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
self.diff_lines.len()
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, i: usize) -> &DiffLine {
|
||||
&self.0[i]
|
||||
&self.diff_lines[i]
|
||||
}
|
||||
|
||||
pub(crate) fn last(&self) -> Option<&DiffLine> {
|
||||
self.0.last()
|
||||
self.diff_lines.last()
|
||||
}
|
||||
}
|
||||
|
||||
fn line_ranges(content: &[u8]) -> Vec<Range<usize>> {
|
||||
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
|
||||
let mut line_start: usize = 0;
|
||||
let mut skip_next = false;
|
||||
|
||||
for i in memchr2_iter(b'\n', b'\r', content) {
|
||||
if skip_next {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = content[i];
|
||||
|
||||
match (c, content.get(i + 1)) {
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if line_start < content.len() {
|
||||
ranges.push(line_start..content.len());
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
use memchr::memchr;
|
||||
use std::path::Path;
|
||||
|
||||
use memchr::{memchr, memchr2_iter};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ContentType {
|
||||
Text,
|
||||
Binary,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum FileType {
|
||||
Rust,
|
||||
JavaScript,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||
if content.is_empty() {
|
||||
ContentType::Text
|
||||
@@ -17,8 +27,52 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||
ContentType::Text
|
||||
} else {
|
||||
match memchr(0, &content[..content.len().min(8192)]) {
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_type_from_path(path: &str) -> FileType {
|
||||
match Path::new(path).extension().map(|it| it.to_str()).flatten() {
|
||||
| Some("rs") => FileType::Rust,
|
||||
| Some("js") | Some("jsx") => FileType::JavaScript,
|
||||
| _ => FileType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn line_ranges(content: &[u8]) -> Vec<std::ops::Range<usize>> {
|
||||
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
|
||||
let mut line_start: usize = 0;
|
||||
let mut skip_next = false;
|
||||
|
||||
for i in memchr2_iter(b'\n', b'\r', content) {
|
||||
if skip_next {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = content[i];
|
||||
|
||||
match (c, content.get(i + 1)) {
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if line_start < content.len() {
|
||||
ranges.push(line_start..content.len());
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod str;
|
||||
pub(crate) mod syntax_highlight;
|
||||
pub(crate) mod timeout;
|
||||
|
||||
91
src/util/syntax_highlight.rs
Normal file
91
src/util/syntax_highlight.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::{theme, util};
|
||||
|
||||
pub(crate) struct HighlightedContent(Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>>);
|
||||
|
||||
fn ts_highlight_configuration_for_file_type(
|
||||
file_type: util::file::FileType,
|
||||
) -> Option<tree_sitter_highlight::HighlightConfiguration> {
|
||||
match file_type {
|
||||
| util::file::FileType::Rust => tree_sitter_highlight::HighlightConfiguration::new(
|
||||
tree_sitter_rust::LANGUAGE.into(),
|
||||
"rust",
|
||||
tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_rust::INJECTIONS_QUERY,
|
||||
"",
|
||||
)
|
||||
.ok(),
|
||||
|
||||
| _ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn highlight_content(
|
||||
content: impl AsRef<[u8]>,
|
||||
file_type: util::file::FileType,
|
||||
theme_syntax: &theme::ThemeSyntax,
|
||||
) -> Option<HighlightedContent> {
|
||||
let mut config = ts_highlight_configuration_for_file_type(file_type)?;
|
||||
|
||||
config.configure(theme::syntax::HIGHLIGHT_NAMES);
|
||||
|
||||
let mut highlighter = tree_sitter_highlight::Highlighter::new();
|
||||
let events = highlighter
|
||||
.highlight(&config, content.as_ref(), None, |_| None)
|
||||
.ok()?;
|
||||
|
||||
let default_highlight = theme_syntax.resolve(theme::syntax::key::TEXT_LITERAL);
|
||||
let mut highlight = default_highlight;
|
||||
|
||||
let line_ranges = util::file::line_ranges(content.as_ref());
|
||||
|
||||
let mut highlights: Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>> =
|
||||
Vec::with_capacity(line_ranges.len());
|
||||
let mut current_line: usize = 0;
|
||||
|
||||
for highlight_event in events {
|
||||
match highlight_event.ok()? {
|
||||
| tree_sitter_highlight::HighlightEvent::HighlightStart(h) => {
|
||||
highlight = theme_syntax.as_slice()[h.0];
|
||||
}
|
||||
| tree_sitter_highlight::HighlightEvent::Source { start, end } => {
|
||||
while current_line < line_ranges.len() && start >= line_ranges[current_line].end {
|
||||
highlights.push(Vec::new());
|
||||
current_line += 1;
|
||||
}
|
||||
let mut line = current_line;
|
||||
while line < line_ranges.len() && end > line_ranges[line].start {
|
||||
if highlights.get(line).is_none() {
|
||||
highlights.push(Vec::new());
|
||||
}
|
||||
let line_range = &line_ranges[line];
|
||||
let highlight_start = start.max(line_range.start);
|
||||
let highlight_end = end.min(line_range.end);
|
||||
highlights[line].push((
|
||||
(highlight_start - line_range.start)..(highlight_end - line_range.start),
|
||||
gpui::HighlightStyle {
|
||||
color: Some(highlight.color.into()),
|
||||
font_weight: highlight.font_weight,
|
||||
font_style: Some(highlight.font_style),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
| tree_sitter_highlight::HighlightEvent::HighlightEnd => {
|
||||
highlight = default_highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(HighlightedContent(highlights))
|
||||
}
|
||||
|
||||
impl HighlightedContent {
|
||||
pub(crate) fn highlights_at_line(
|
||||
&self,
|
||||
line: usize,
|
||||
) -> &Vec<(std::ops::Range<usize>, gpui::HighlightStyle)> {
|
||||
&self.0[line]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user