feat: syntax highlighting for diff view

This commit is contained in:
2026-05-25 00:08:22 +01:00
parent b3e041a257
commit a6cf96ea96
20 changed files with 1295 additions and 722 deletions

View File

@@ -116,9 +116,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())),
} }
} }
} }
@@ -155,7 +155,7 @@ where
{ {
let mut body: graphql_client::Response<T> = res.json().await?; let mut body: graphql_client::Response<T> = res.json().await?;
match body.data.take() { match body.data.take() {
| None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
| Some(data) => Ok((body, data)), | Some(data) => Ok((body, data)),
} }
} }

View File

@@ -318,8 +318,8 @@ impl query::QueryFn for ListPullRequests {
} }
let query_string = match self.filter { let query_string = match self.filter {
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter), | Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
| None => "is:pr archived:false sort:updated-desc".into(), | None => "is:pr archived:false sort:updated-desc".into(),
}; };
let gql = let gql =
@@ -341,20 +341,20 @@ impl query::QueryFn for ListPullRequests {
.flatten() .flatten()
.filter_map(|edge| { .filter_map(|edge| {
edge.node.and_then(|n| match n { edge.node.and_then(|n| match n {
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => { | PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
Some(PullRequest { Some(PullRequest {
id: p.id.into(), id: p.id.into(),
title: p.title.into(), 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(), .into(),
}) })
} }
| _ => None, | _ => None,
}) })
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -399,43 +399,43 @@ impl query::QueryFn for FetchPullRequest {
"missing 'node' field on PullRequestQuery response".into(), "missing 'node' field on PullRequestQuery response".into(),
)) ))
.and_then(|n| match n { .and_then(|n| match n {
| PullRequestQueryNode::PullRequest(p) => { | PullRequestQueryNode::PullRequest(p) => {
let created_at = let created_at =
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| { chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
api::Error::MalformedResponse(format!( api::Error::MalformedResponse(format!(
"invalid pull request createdAt {:?}: {err}", "invalid pull request createdAt {:?}: {err}",
p.created_at p.created_at
)) ))
})?; })?;
Ok(DetailedPullRequest { Ok(DetailedPullRequest {
id: Id(p.id.into()), id: Id(p.id.into()),
title: p.title.into(), title: p.title.into(),
state: p.state, state: p.state,
is_draft: p.is_draft, is_draft: p.is_draft,
body: p.body.into(), body: p.body.into(),
author: p.author.map(|it| api::user::Actor { author: p.author.map(|it| api::user::Actor {
login: it.login.into(), login: it.login.into(),
avatar_url: it.avatar_url.into(), avatar_url: it.avatar_url.into(),
}), }),
base_repo_slug: p base_repo_slug: p
.base_repository .base_repository
.map(|it| it.name_with_owner.into()) .map(|it| it.name_with_owner.into())
.unwrap_or_default(), .unwrap_or_default(),
base_branch_name: p.base_ref_name.into(), base_branch_name: p.base_ref_name.into(),
base_ref: p.base_ref_oid.into(), 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.into()) .map(|it| it.name_with_owner.into())
.unwrap_or_default(), .unwrap_or_default(),
head_branch_name: p.head_ref_name.into(), head_branch_name: p.head_ref_name.into(),
head_ref: p.head_ref_oid.into(), head_ref: p.head_ref_oid.into(),
created_at: Some(created_at), created_at: Some(created_at),
}) })
} }
| _ => Err(api::Error::MalformedResponse( | _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestQuery".into(), "unexpected node type on PullRequestQuery".into(),
)), )),
}) })
} }
} }
@@ -494,12 +494,12 @@ impl query::QueryFn for FetchPullRequestFileTree {
"missing 'node' field on PullRequestFileTreeQuery response".into(), "missing 'node' field on PullRequestFileTreeQuery response".into(),
)) ))
.and_then(|node| match node { .and_then(|node| match node {
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest( | pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
pull_request, pull_request,
) => Ok(pull_request), ) => Ok(pull_request),
| _ => Err(api::Error::MalformedResponse( | _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestFileTreeQuery".into(), "unexpected node type on PullRequestFileTreeQuery".into(),
)), )),
})?; })?;
Ok(pull_request Ok(pull_request
@@ -513,23 +513,25 @@ impl query::QueryFn for FetchPullRequestFileTree {
edge.node.map(|node| ChangedFile { edge.node.map(|node| ChangedFile {
cursor, cursor,
change_type: match node.change_type { change_type: match node.change_type {
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added, | pull_request_file_tree_query::PatchStatus::ADDED => {
| pull_request_file_tree_query::PatchStatus::MODIFIED => { ChangeType::Added
ChangeType::Modified }
} | pull_request_file_tree_query::PatchStatus::MODIFIED => {
| pull_request_file_tree_query::PatchStatus::DELETED => { ChangeType::Modified
ChangeType::Deleted }
} | pull_request_file_tree_query::PatchStatus::DELETED => {
| pull_request_file_tree_query::PatchStatus::RENAMED => { ChangeType::Deleted
ChangeType::Renamed }
} | pull_request_file_tree_query::PatchStatus::RENAMED => {
| pull_request_file_tree_query::PatchStatus::COPIED => { ChangeType::Renamed
ChangeType::Copied }
} | pull_request_file_tree_query::PatchStatus::COPIED => {
| pull_request_file_tree_query::PatchStatus::CHANGED => { ChangeType::Copied
ChangeType::Changed }
} | pull_request_file_tree_query::PatchStatus::CHANGED => {
| _ => ChangeType::Changed, ChangeType::Changed
}
| _ => ChangeType::Changed,
}, },
additions: node.additions, additions: node.additions,
deletions: node.deletions, deletions: node.deletions,
@@ -575,11 +577,11 @@ impl query::QueryFn for FetchPullRequestTimeline {
TimelineActor { TimelineActor {
kind: match on { kind: match on {
| actorFieldsOn::Bot => "Bot", | actorFieldsOn::Bot => "Bot",
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount", | actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
| actorFieldsOn::Mannequin => "Mannequin", | actorFieldsOn::Mannequin => "Mannequin",
| actorFieldsOn::Organization => "Organization", | actorFieldsOn::Organization => "Organization",
| actorFieldsOn::User => "User", | actorFieldsOn::User => "User",
} }
.into(), .into(),
name: login, name: login,
@@ -589,62 +591,62 @@ impl query::QueryFn for FetchPullRequestTimeline {
fn normalize_assignee(actor: assigneeFields) -> TimelineActor { fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
match actor { match actor {
| assigneeFields::Bot(actor) => TimelineActor { | assigneeFields::Bot(actor) => TimelineActor {
kind: "Bot".into(), kind: "Bot".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
| assigneeFields::Mannequin(actor) => TimelineActor { | assigneeFields::Mannequin(actor) => TimelineActor {
kind: "Mannequin".into(), kind: "Mannequin".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
| assigneeFields::Organization(actor) => TimelineActor { | assigneeFields::Organization(actor) => TimelineActor {
kind: "Organization".into(), kind: "Organization".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
| assigneeFields::User(actor) => TimelineActor { | assigneeFields::User(actor) => TimelineActor {
kind: "User".into(), kind: "User".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
} }
} }
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor { fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
match actor { match actor {
| requestedReviewerFields::Bot(actor) => TimelineActor { | requestedReviewerFields::Bot(actor) => TimelineActor {
kind: "Bot".into(), kind: "Bot".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
| requestedReviewerFields::Mannequin(actor) => TimelineActor { | requestedReviewerFields::Mannequin(actor) => TimelineActor {
kind: "Mannequin".into(), kind: "Mannequin".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
| requestedReviewerFields::Team(actor) => TimelineActor { | requestedReviewerFields::Team(actor) => TimelineActor {
kind: "Team".into(), kind: "Team".into(),
name: actor.name, name: actor.name,
avatar_url: None, avatar_url: None,
}, },
| requestedReviewerFields::User(actor) => TimelineActor { | requestedReviewerFields::User(actor) => TimelineActor {
kind: "User".into(), kind: "User".into(),
name: actor.login, name: actor.login,
avatar_url: Some(actor.avatar_url), avatar_url: Some(actor.avatar_url),
}, },
} }
} }
fn normalize_review_state(state: PullRequestReviewState) -> String { fn normalize_review_state(state: PullRequestReviewState) -> String {
match state { match state {
| PullRequestReviewState::PENDING => "PENDING", | PullRequestReviewState::PENDING => "PENDING",
| PullRequestReviewState::COMMENTED => "COMMENTED", | PullRequestReviewState::COMMENTED => "COMMENTED",
| PullRequestReviewState::APPROVED => "APPROVED", | PullRequestReviewState::APPROVED => "APPROVED",
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED", | PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
| PullRequestReviewState::DISMISSED => "DISMISSED", | PullRequestReviewState::DISMISSED => "DISMISSED",
| _ => "OTHER", | _ => "OTHER",
} }
.into() .into()
} }
@@ -864,10 +866,10 @@ impl query::QueryFn for FetchPullRequestTimeline {
"missing 'node' field on PullRequestTimelineQuery response".into(), "missing 'node' field on PullRequestTimelineQuery response".into(),
)) ))
.and_then(|node| match node { .and_then(|node| match node {
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request), | PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
| _ => Err(api::Error::MalformedResponse( | _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestTimelineQuery".into(), "unexpected node type on PullRequestTimelineQuery".into(),
)), )),
})?; })?;
let timeline = pull_request.timeline_items; let timeline = pull_request.timeline_items;

View File

@@ -463,30 +463,36 @@ 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)) => pull_request | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
.timeline_items pull_request
.nodes .timeline_items
.as_ref() .nodes
.expect("first timeline fixture page should contain timeline nodes"), .as_ref()
| _ => panic!("first timeline fixture page should resolve to a pull request node"), .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() { let second_page_nodes = match second_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
.timeline_items pull_request
.nodes .timeline_items
.as_ref() .nodes
.expect("second timeline fixture page should contain timeline nodes"), .as_ref()
| _ => panic!("second timeline fixture page should resolve to a pull request node"), .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() { let third_page_nodes = match third_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
.timeline_items pull_request
.nodes .timeline_items
.as_ref() .nodes
.expect("third timeline fixture page should contain timeline nodes"), .as_ref()
| _ => panic!("third timeline fixture page should resolve to a pull request node"), .expect("third timeline fixture page should contain timeline nodes")
}
| _ => panic!("third timeline fixture page should resolve to a pull request node"),
}; };
assert_eq!( assert_eq!(

View File

@@ -1,15 +1,13 @@
use std::{num::NonZeroUsize, rc::Rc, sync::Arc}; use std::{num::NonZeroUsize, rc::Rc};
use gpui::{ use gpui::{IntoElement, ParentElement, Refineable, Styled, div, list, prelude::FluentBuilder, px};
IntoElement, ParentElement, Refineable, Styled, div, list, prelude::FluentBuilder, px, rems,
};
use crate::app; use crate::app;
#[derive(gpui::IntoElement, Clone)] #[derive(gpui::IntoElement)]
pub(crate) struct CodeLine { pub(crate) struct CodeLine {
line_number: Option<NonZeroUsize>, line_number: Option<NonZeroUsize>,
content: Option<gpui::SharedString>, content: Option<gpui::AnyElement>,
diff_marker: CodeLineMarker, diff_marker: CodeLineMarker,
gutter_width: gpui::Pixels, gutter_width: gpui::Pixels,
style: gpui::StyleRefinement, style: gpui::StyleRefinement,
@@ -45,18 +43,29 @@ pub(crate) fn code_line(
) -> CodeLine { ) -> CodeLine {
CodeLine { CodeLine {
line_number: line_index.map(|i| unsafe { NonZeroUsize::new_unchecked(i + 1) }), line_number: line_index.map(|i| unsafe { NonZeroUsize::new_unchecked(i + 1) }),
content, content: content.map(|it| it.into_any_element()),
diff_marker: marker, diff_marker: marker,
gutter_width: px(0.), gutter_width: px(0.),
style: gpui::StyleRefinement::default(), style: gpui::StyleRefinement::default(),
} }
} }
impl CodeViewContent { pub(crate) fn code_line_with_highlights(
pub(crate) fn new(lines: Vec<CodeLine>) -> Self { line_index: Option<usize>,
Self { content: Option<gpui::SharedString>,
lines: lines.into(), 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); println!("gutter width {}", gutter_width);
list(self.state.0, move |i, _window, _app| { list(self.state.0, move |i, _window, _app| todo!())
let line = self.content.lines[i].clone();
div()
.flex()
.flex_row()
.items_start()
.w_full()
.child(line.gutter_width(gutter_width))
.into_any_element()
})
} }
} }

View File

@@ -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::{ use crate::{
component::code_view::{self, CodeLine, code_line}, component::code_view::{self, CodeLine, code_line, code_line_with_highlights},
util::{self, str::ToSharedString}, util::{self, str::ToSharedString},
}; };
@@ -14,7 +14,13 @@ pub(crate) struct DiffView {
} }
#[derive(Clone)] #[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)] #[derive(Clone)]
pub(crate) struct DiffViewContent { pub(crate) struct DiffViewContent {
@@ -23,6 +29,7 @@ pub(crate) struct DiffViewContent {
#[derive(Clone, gpui::IntoElement)] #[derive(Clone, gpui::IntoElement)]
struct DiffRow { struct DiffRow {
state: DiffViewState,
line: util::diff::DiffLine, line: util::diff::DiffLine,
old_side_gutter_width: gpui::Pixels, old_side_gutter_width: gpui::Pixels,
new_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 { impl DiffViewState {
pub(crate) fn new() -> Self { 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) { 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 { impl gpui::RenderOnce for DiffView {
fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement { fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
let (old_digits, new_digits) = self let old_digits = self.content.diff.old_line_count.to_string().len();
.content let new_digits = self.content.diff.new_line_count.to_string().len();
.diff
.last()
.map(|l| (l.old_line.to_string().len(), l.new_line.to_string().len()))
.unwrap_or((1, 1));
let text_style = window.text_style(); let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size()); 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 old_side_gutter_width = ch * old_digits;
let new_side_gutter_width = ch * new_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 { DiffRow {
state: self.state.clone(),
line: self.content.diff.get(i).clone(), line: self.content.diff.get(i).clone(),
old_side_gutter_width, old_side_gutter_width,
new_side_gutter_width, new_side_gutter_width,
@@ -84,35 +108,69 @@ impl gpui::RenderOnce for DiffView {
impl DiffRow { impl DiffRow {
fn old_code_line(&self) -> CodeLine { fn old_code_line(&self) -> CodeLine {
code_line( let state = self.state.0.borrow();
Some(self.line.old_line),
self.line let content = self
.old_content .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() .as_ref()
.map(|it| it.to_shared_string()), .map(|it| it.highlights_at_line(line))
match self.line.op { }) {
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged, | Some(highlights) => code_line_with_highlights(
| util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder, self.line.new_line,
| util::diff::Op::Replace | util::diff::Op::Delete => { content,
code_view::CodeLineMarker::Deleted highlights.iter().cloned(),
} marker,
}, ),
)
| None => code_line(self.line.new_line, content, marker),
}
} }
fn new_code_line(&self) -> CodeLine { fn new_code_line(&self) -> CodeLine {
code_line( let state = self.state.0.borrow();
Some(self.line.new_line),
self.line let content = self
.new_content .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() .as_ref()
.map(|it| it.to_shared_string()), .map(|it| it.highlights_at_line(line))
match self.line.op { }) {
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged, | Some(highlights) => code_line_with_highlights(
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added, self.line.new_line,
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted, content,
}, highlights.iter().cloned(),
) marker,
),
| None => code_line(self.line.new_line, content, marker),
}
} }
} }

View File

@@ -115,7 +115,7 @@ pub(crate) fn new(content: Arc<str>, cx: &mut gpui::Context<MarkdownText>) -> Ma
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,
} }
} }
} }
@@ -179,54 +179,56 @@ 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 {
color: Some(theme.colors.link.into()), font_style: Some(gpui::FontStyle::Italic),
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()
},
)),
if cursor.goto_next_sibling() | MARKDOWN_KIND_ID_LINK => {
&& let Ok(src) = cursor.node().utf8_text(content.as_bytes()) if cursor.goto_first_child() {
{ highlights.push((
links node_range!(),
.push((node_range!(), gpui::SharedString::from(String::from(src)))); gpui::HighlightStyle {
} else { color: Some(theme.colors.link.into()),
// the link src is invalid, use an empty string as a fallback underline: Some(gpui::UnderlineStyle {
// link on click handler will ignore empty string color: Some(theme.colors.link.into()),
links.push((node_range!(), "".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() { if !cursor.goto_next_sibling() {
@@ -303,23 +305,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 {
@@ -331,9 +333,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 {
@@ -372,150 +374,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 { let marker_node_kind = cursor.node().kind_id();
| 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 { let block = if cursor.goto_next_sibling()
is_first_heading = false; && cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT
block = block.mt_0(); {
// 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); 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;
} }
let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING { | MARKDOWN_KIND_ID_TIGHT_LIST => {
// skipping info string (which annotates the code block) let is_rendered =
if cursor.goto_next_sibling() { render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0);
// this is code_fence_content node 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( 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(),
} }
} else { .text_sm()
// 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)
.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() { if !cursor.goto_next_sibling() {
@@ -533,55 +535,55 @@ impl gpui::Render for MarkdownText {
) -> impl gpui::prelude::IntoElement { ) -> impl gpui::prelude::IntoElement {
let children = self.blocks.iter().enumerate().map(|(i, block)| { let children = self.blocks.iter().enumerate().map(|(i, block)| {
match block { match block {
| ContentBlock::Text { | ContentBlock::Text {
decoration, decoration,
text, text,
highlights, highlights,
links, links,
style, style,
} => { } => {
let styled_text = let styled_text =
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone()); gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
let content = if links.is_empty() { let content = if links.is_empty() {
div().w_full().child(styled_text) div().w_full().child(styled_text)
} else { } else {
// if link in block, interactive text is needed // if link in block, interactive text is needed
// to handle link clicks // to handle link clicks
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip(); let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
let weak = cx.entity(); let weak = cx.entity();
let t = gpui::InteractiveText::new(i, styled_text).on_click( let t = gpui::InteractiveText::new(i, styled_text).on_click(
link_ranges, link_ranges,
move |i, _, cx| { move |i, _, cx| {
if let Some(src) = srcs.get(i) { if let Some(src) = srcs.get(i) {
weak.update(cx, |this, cx| { weak.update(cx, |this, cx| {
this.on_open_link(src, cx); this.on_open_link(src, cx);
cx.notify(); cx.notify();
}) })
} }
}, },
); );
div().w_full().child(t) div().w_full().child(t)
}; };
let mut div = match decoration { let mut div = match decoration {
| Some(d) => div() | Some(d) => div()
.w_full() .w_full()
.flex() .flex()
.flex_row() .flex_row()
.gap_2() .gap_2()
.items_start() .items_start()
.child(d.clone()) .child(d.clone())
.child(div().flex_1().min_w_0().child(content)), .child(div().flex_1().min_w_0().child(content)),
| None => div().w_full().child(content), | None => div().w_full().child(content),
}; };
div.style().refine(&style); div.style().refine(&style);
div div
} }
} }
}); });

View File

@@ -1,4 +1,4 @@
use gpui::{AppContext, BorrowAppContext}; use gpui::BorrowAppContext;
use std::{any::Any, borrow::Cow, collections::HashMap, marker::PhantomData, ops::Deref}; use std::{any::Any, borrow::Cow, collections::HashMap, marker::PhantomData, ops::Deref};
pub(crate) trait QueryFn: Clone + 'static { pub(crate) trait QueryFn: Clone + 'static {
@@ -187,19 +187,28 @@ where
} }
} }
pub fn observe_query<E, F>( pub fn watch_query<E, F, H>(query: &Entity<F>, on_notify: H, cx: &mut gpui::Context<E>) -> gpui::Subscription
query: &Entity<F>,
mut on_notify: impl FnMut(&mut E, &Entity<F>, &mut gpui::Context<E>) + 'static,
cx: &mut gpui::Context<E>,
) -> gpui::Subscription
where where
E: 'static, E: 'static,
F: QueryFn, F: QueryFn,
H: Fn(&mut E, &Entity<F>, &mut gpui::Context<E>) + Clone + 'static,
{ {
let q = query.clone(); let observed_query = query.clone();
cx.observe(&query, move |this, _, cx| { let sub = cx.observe(query, {
on_notify(this, &q, cx); 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 ================== // ================= Store ==================

View File

@@ -10,7 +10,7 @@ use crate::{
font_icon::{FontIcon, FontIconSvg, font_icon}, font_icon::{FontIcon, FontIconSvg, font_icon},
text::text, text::text,
}, },
query::{self, QueryStatus, read_query, use_query}, query::{self, QueryStatus, read_query, use_query, watch_query},
util::str::ToSharedString, util::str::ToSharedString,
}; };
@@ -56,28 +56,44 @@ pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
impl IssueList { impl IssueList {
fn on_create(&mut self, cx: &mut gpui::Context<Self>) { fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
cx.observe(&self.pr_query, |this, _, cx| { let pr_query = self.pr_query.clone();
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 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(), id: it.id.clone(),
repo_name: Some(it.repo_slug.to_shared_string()), repo_name: Some(it.repo_slug.to_shared_string()),
title: it.title.to_shared_string(), title: it.title.to_shared_string(),
description: None, description: None,
status: it.state, status: it.state,
is_selected: false,
is_last: i == new_len - 1, is_last: i == new_len - 1,
is_draft: it.is_draft, is_draft: it.is_draft,
}); })
.collect();
this.list_items.splice(old_len..old_len, new_items); self.list_state.splice(0..old_len, new_len);
this.list_state.splice(old_len..old_len, new_len); }
}
})
.detach();
} }
fn on_item_click(&mut self, i: usize, cx: &mut gpui::Context<Self>) { fn on_item_click(&mut self, i: usize, cx: &mut gpui::Context<Self>) {

View File

@@ -6,9 +6,10 @@ use crate::{
diff_view::{DiffViewContent, DiffViewState, diff_view}, diff_view::{DiffViewContent, DiffViewState, diff_view},
text::text, 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 { pub(crate) struct PullRequestDiffView {
selected_file_path: Option<Arc<str>>, selected_file_path: Option<Arc<str>>,
@@ -104,23 +105,68 @@ impl PullRequestDiffView {
}, },
cx, cx,
); );
_ = watch_query(&content_diff_query, Self::sync_content_diff_query, cx).detach();
_ = 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();
self.content_diff_query = Some(content_diff_query); 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 { impl gpui::Render for PullRequestDiffView {

View File

@@ -14,7 +14,7 @@ use crate::{
markdown::{self, MarkdownText}, markdown::{self, MarkdownText},
text::text, 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}, screen::dashboard::pull_request_diff_view::{self, PullRequestDiffView},
}; };
@@ -46,21 +46,20 @@ impl PullRequestView {
self.pull_request_query = Some(query.clone()); self.pull_request_query = Some(query.clone());
_ = cx _ = watch_query(&query, Self::sync_pull_request_query, cx).detach();
.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);
cx.notify(); 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>) { fn load_markdown_content(&mut self, cx: &mut gpui::Context<Self>) {
let Some(query) = &self.pull_request_query else { let Some(query) = &self.pull_request_query else {
return; return;
@@ -115,41 +114,41 @@ impl PullRequestView {
.rounded_full(); .rounded_full();
match pr.state { match pr.state {
| api::issues::PullRequestState::Open => { | api::issues::PullRequestState::Open => {
status_pill = status_pill status_pill = status_pill
.bg(theme.colors.success_solid) .bg(theme.colors.success_solid)
.child( .child(
font_icon(FontIcon::PullRequestArrow) font_icon(FontIcon::PullRequestArrow)
.size_3() .size_3()
.text_color(theme.colors.success_on_solid), .text_color(theme.colors.success_on_solid),
) )
.child( .child(
text("Open") text("Open")
.text_color(theme.colors.success_on_solid) .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(), .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| { let merge_text = pr.author.as_ref().map(|author| {
@@ -284,23 +283,23 @@ impl gpui::Render for PullRequestView {
cx: &mut gpui::Context<Self>, cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement { ) -> impl gpui::IntoElement {
div().size_full().child(match &self.pull_request_query { div().size_full().child(match &self.pull_request_query {
| Some(q) => match read_query(q, cx) { | Some(q) => match read_query(q, cx) {
| QueryStatus::Loaded(pr) => match &self.diff_view { | QueryStatus::Loaded(pr) => match &self.diff_view {
| Some(v) => v.clone().into_any_element(), | Some(v) => v.clone().into_any_element(),
| None => self.pr_content(pr, cx), | None => self.pr_content(pr, cx),
}, },
| QueryStatus::Err(e) => div() | QueryStatus::Err(e) => div()
.size_full() .size_full()
.child(format!("{:?}", e)) .child(format!("{:?}", e))
.into_any_element(), .into_any_element(),
| QueryStatus::Loading => div() | QueryStatus::Loading => div()
.size_full() .size_full()
.child("loading pr content") .child("loading pr content")
.into_any_element(), .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(),
}) })
} }
} }

View File

@@ -36,9 +36,9 @@ impl Screen {
fn on_create(&mut self, cx: &mut gpui::Context<Self>) { fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
_ = cx _ = cx
.subscribe(&self.issue_list, |this, _, event, cx| match event { .subscribe(&self.issue_list, |this, _, event, cx| match event {
| issue_list::Event::ItemSelected(pr_id) => { | issue_list::Event::ItemSelected(pr_id) => {
this.handle_issue_list_item_selected(pr_id, cx); this.handle_issue_list_item_selected(pr_id, cx);
} }
}) })
.detach(); .detach();
} }

View File

@@ -3,8 +3,7 @@ use std::{ops::Range, sync::Arc};
use bytes::Bytes; use bytes::Bytes;
use gpui::{ use gpui::{
AnyElement, AppContext, InteractiveElement, IntoElement, ParentElement, AnyElement, AppContext, InteractiveElement, IntoElement, ParentElement,
StatefulInteractiveElement, StatefulInteractiveElement, Styled, div, point, px, size,
Styled, div, point, px, size,
}; };
use crate::{ use crate::{
@@ -54,6 +53,8 @@ pub(crate) struct Screen {
struct DiffCase { struct DiffCase {
title: &'static str, title: &'static str,
description: &'static str, description: &'static str,
old_line_count: usize,
new_line_count: usize,
old_lines: Vec<SourceLine>, old_lines: Vec<SourceLine>,
new_lines: Vec<SourceLine>, new_lines: Vec<SourceLine>,
op_groups: Vec<OpGroup>, op_groups: Vec<OpGroup>,
@@ -233,8 +234,8 @@ impl gpui::Render for Screen {
text(format!( text(format!(
"{} ops, {} old lines, {} new lines", "{} ops, {} old lines, {} new lines",
case.op_groups.len(), case.op_groups.len(),
line_count(&case.old_lines), case.old_line_count,
line_count(&case.new_lines), case.new_line_count,
)) ))
.text_xs() .text_xs()
.font_family("Menlo") .font_family("Menlo")
@@ -252,11 +253,11 @@ impl gpui::Render for Screen {
.border_b_1() .border_b_1()
.border_color(theme.colors.border_muted) .border_color(theme.colors.border_muted)
.child( .child(
panel_header("Old", line_count(&case.old_lines), theme) panel_header("Old", case.old_line_count, theme)
.flex_1(), .flex_1(),
) )
.child( .child(
panel_header("New", line_count(&case.new_lines), theme) panel_header("New", case.new_line_count, theme)
.flex_1(), .flex_1(),
), ),
) )
@@ -276,6 +277,8 @@ impl gpui::Render for Screen {
.child(render_source_content( .child(render_source_content(
&case.old_lines, &case.old_lines,
&case.new_lines, &case.new_lines,
case.old_line_count,
case.new_line_count,
theme, theme,
)) ))
.child(text("Diff Rows Render").text_sm()) .child(text("Diff Rows Render").text_sm())
@@ -377,6 +380,8 @@ impl DiffCase {
Self { Self {
title, title,
description, description,
old_line_count: diff.old_line_count,
new_line_count: diff.new_line_count,
old_lines: collect_source_lines(&diff, SourceSide::Old), old_lines: collect_source_lines(&diff, SourceSide::Old),
new_lines: collect_source_lines(&diff, SourceSide::New), new_lines: collect_source_lines(&diff, SourceSide::New),
op_groups: collect_op_groups(&diff), 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( fn render_source_content(
old_lines: &[SourceLine], old_lines: &[SourceLine],
new_lines: &[SourceLine], new_lines: &[SourceLine],
old_line_count: usize,
new_line_count: usize,
theme: &crate::theme::Theme, theme: &crate::theme::Theme,
) -> gpui::Div { ) -> gpui::Div {
div() div()
.flex() .flex()
.flex_row() .flex_row()
.gap_2() .gap_2()
.child(render_source_panel("Old Content", old_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, theme).flex_1()) .child(render_source_panel("New Content", new_lines, new_line_count, theme).flex_1())
} }
fn render_source_panel( fn render_source_panel(
title: &'static str, title: &'static str,
lines: &[SourceLine], lines: &[SourceLine],
line_count: usize,
theme: &crate::theme::Theme, theme: &crate::theme::Theme,
) -> gpui::Div { ) -> gpui::Div {
let line_count = line_count(lines);
let rows: Vec<AnyElement> = lines let rows: Vec<AnyElement> = lines
.iter() .iter()
.map(|line| { .map(|line| {
@@ -573,7 +579,7 @@ fn render_row(op_index: usize, row: &DiffLine, theme: &crate::theme::Theme) -> g
.child(render_line_cell( .child(render_line_cell(
op_index, op_index,
row.op, row.op,
row.old_content.as_ref().map(|_| row.old_line), row.old_line,
row.old_content.as_deref().map(display_text), row.old_content.as_deref().map(display_text),
true, true,
theme, theme,
@@ -581,7 +587,7 @@ fn render_row(op_index: usize, row: &DiffLine, theme: &crate::theme::Theme) -> g
.child(render_line_cell( .child(render_line_cell(
op_index, op_index,
row.op, row.op,
row.new_content.as_ref().map(|_| row.new_line), row.new_line,
row.new_content.as_deref().map(display_text), row.new_content.as_deref().map(display_text),
false, false,
theme, theme,
@@ -661,10 +667,6 @@ fn display_text(text: &str) -> String {
rendered 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> { fn collect_source_lines(diff: &ContentDiff, side: SourceSide) -> Vec<SourceLine> {
let mut lines = Vec::new(); let mut lines = Vec::new();
@@ -673,17 +675,17 @@ fn collect_source_lines(diff: &ContentDiff, side: SourceSide) -> Vec<SourceLine>
match side { match side {
| SourceSide::Old => { | 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 { lines.push(SourceLine {
line_number: row.old_line, line_number,
content: Arc::clone(content), content: Arc::clone(content),
}); });
} }
} }
| SourceSide::New => { | 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 { lines.push(SourceLine {
line_number: row.new_line, line_number,
content: Arc::clone(content), content: Arc::clone(content),
}); });
} }
@@ -710,8 +712,8 @@ fn collect_op_groups(diff: &ContentDiff) -> Vec<OpGroup> {
groups.push(OpGroup { groups.push(OpGroup {
op, op,
old_range: group_range(&rows, SourceSide::Old), old_range: group_range(diff, start, end, SourceSide::Old),
new_range: group_range(&rows, SourceSide::New), new_range: group_range(diff, start, end, SourceSide::New),
rows, rows,
}); });
@@ -721,18 +723,13 @@ fn collect_op_groups(diff: &ContentDiff) -> Vec<OpGroup> {
groups groups
} }
fn group_range(rows: &[DiffLine], side: SourceSide) -> Range<usize> { fn group_range(diff: &ContentDiff, start: usize, end: usize, 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),
};
let mut first = None; let mut first = None;
let mut last = None; let mut last = None;
for line_number in rows.iter().filter_map(|row| match side { for line_number in (start..end).filter_map(|index| match side {
| SourceSide::Old => row.old_content.as_ref().map(|_| row.old_line), | SourceSide::Old => diff.get(index).old_line,
| SourceSide::New => row.new_content.as_ref().map(|_| row.new_line), | SourceSide::New => diff.get(index).new_line,
}) { }) {
if first.is_none() { if first.is_none() {
first = Some(line_number); first = Some(line_number);
@@ -742,10 +739,31 @@ fn group_range(rows: &[DiffLine], side: SourceSide) -> Range<usize> {
match (first, last) { match (first, last) {
| (Some(start), Some(end)) => start..end + 1, | (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 { struct Colors {
background: gpui::Rgba, background: gpui::Rgba,
border: gpui::Rgba, border: gpui::Rgba,

View File

@@ -183,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, _| {
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(""))
}; };
})
.detach();
}
| QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => { cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
if error == "authorization_pending" { 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| { cx.spawn(async move |weak, cx| {
Timer::after(Duration::from_secs(poll_interval)).await; let ent = fetch_query(api::user::Fetch, cx).await;
if let Ok(Some(query)) =
weak.read_with(cx, |this, _cx| this.request_access_token_query.clone()) let fut = weak
{ .update(cx, move |_this, cx| {
let _ = weak.update(cx, |_this, cx| { let Ok(query) = ent else {
query.refetch(cx); 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(); .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 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_ref(), 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();
@@ -352,14 +352,16 @@ 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) => (true, connected_header(), connected_body(user, cx)), | QueryStatus::Loaded(user) => {
| _ => (false, self.header(), self.device_code_area(cx)), (true, connected_header(), connected_body(user, cx))
}
| _ => (false, self.header(), self.device_code_area(cx)),
}
} }
}
}; };
div() div()

View File

@@ -1,7 +1,11 @@
mod catppuccin; mod catppuccin;
pub(crate) mod syntax;
use gpui::Rgba; use gpui::Rgba;
#[allow(unused_imports)]
pub use syntax::{HIGHLIGHT_NAMES, ThemeSyntax, ThemeSyntaxHighlight};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeMode { pub enum ThemeMode {
Light, Light,
@@ -15,6 +19,7 @@ pub struct Theme {
pub name: &'static str, pub name: &'static str,
pub mode: ThemeMode, pub mode: ThemeMode,
pub colors: ThemeColors, pub colors: ThemeColors,
pub syntax: ThemeSyntax,
} }
impl Default for Theme { 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() self.variant(mode).theme()
} }
@@ -144,7 +149,7 @@ impl ThemeVariant {
} }
} }
pub const fn theme(self) -> Theme { pub fn theme(self) -> Theme {
match self { match self {
| Self::CatppuccinLatte => catppuccin::latte(), | Self::CatppuccinLatte => catppuccin::latte(),
| Self::CatppuccinMocha => catppuccin::mocha(), | Self::CatppuccinMocha => catppuccin::mocha(),

View File

@@ -1,6 +1,9 @@
use crate::colors::{hex, hex_alpha}; 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_ID: &str = "catppuccin";
pub(crate) const FAMILY_LABEL: &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 LATTE_LABEL: &str = "Catppuccin Latte";
pub(crate) const MOCHA_LABEL: &str = "Catppuccin Mocha"; 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 { Theme {
id: "catppuccin-latte", id: "catppuccin-latte",
name: LATTE_LABEL, name: LATTE_LABEL,
@@ -61,10 +88,63 @@ pub(crate) const fn latte() -> Theme {
info_solid: hex(0x1e66f5), info_solid: hex(0x1e66f5),
info_on_solid: hex(0xeff1f5), 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 { Theme {
id: "catppuccin-mocha", id: "catppuccin-mocha",
name: MOCHA_LABEL, name: MOCHA_LABEL,
@@ -117,5 +197,58 @@ pub(crate) const fn mocha() -> Theme {
info_solid: hex(0x89b4fa), info_solid: hex(0x89b4fa),
info_on_solid: hex(0x1e1e2e), 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
View 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";
}

View File

@@ -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; use similar::DiffableStr;
pub(crate) struct Span { use crate::util;
pub(crate) op: Op,
pub(crate) old_range: Range<usize>,
pub(crate) new_range: Range<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Op { pub(crate) enum Op {
@@ -17,39 +12,32 @@ pub(crate) enum Op {
Replace, 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)] #[derive(Clone)]
pub(crate) struct DiffLine { pub(crate) struct DiffLine {
pub(crate) op: Op, pub(crate) op: Op,
pub(crate) old_content: Option<Arc<str>>, 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_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)] #[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( pub(crate) fn diff_content(
old_content: bytes::Bytes, old_content: bytes::Bytes,
new_content: bytes::Bytes, new_content: bytes::Bytes,
) -> Option<ContentDiff> { ) -> Option<ContentDiff> {
let old_line_ranges = line_ranges(&old_content); let old_line_ranges = util::file::line_ranges(&old_content);
let new_line_ranges = line_ranges(&new_content); let new_line_ranges = util::file::line_ranges(&new_content);
let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content); let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content);
let mut diff_lines: Vec<DiffLine> = Vec::new(); 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()?); let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
diff_lines.push(DiffLine { diff_lines.push(DiffLine {
op: Op::Equal, op: Op::Equal,
old_line, old_line: Some(old_line),
old_content: Some(Arc::clone(&content)), 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_content: Some(content),
new_byte_range: new_line_ranges[new_line].clone(),
}); });
} }
} }
| &similar::DiffOp::Insert { | &similar::DiffOp::Insert {
old_index, new_index, new_len, ..
new_index,
new_len,
} => { } => {
for i in 0..new_len { for i in 0..new_len {
let new_line_range = &new_line_ranges[new_index + i]; let new_line_range = &new_line_ranges[new_index + i];
let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?); let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?);
diff_lines.push(DiffLine { diff_lines.push(DiffLine {
op: Op::Insert, op: Op::Insert,
old_line: old_index, old_line: None,
old_content: 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_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 { | (Some(old_range), Some(new_range)) => DiffLine {
op: Op::Replace, op: Op::Replace,
old_line, old_line: Some(old_line),
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)), 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_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
new_byte_range: new_range.clone(),
}, },
| (None, Some(new_range)) => DiffLine { | (None, Some(new_range)) => DiffLine {
op: Op::Replace, op: Op::Replace,
old_line: old_index + old_len, old_line: None,
old_content: 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_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
new_byte_range: new_range.clone(),
}, },
| (Some(old_range), None) => DiffLine { | (Some(old_range), None) => DiffLine {
op: Op::Replace, 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()?)), 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_content: None,
new_byte_range: 0..0,
}, },
| (None, None) => { | (None, None) => {
@@ -143,74 +139,44 @@ pub(crate) fn diff_content(
} }
| &similar::DiffOp::Delete { | &similar::DiffOp::Delete {
old_index, old_index, old_len, ..
old_len,
new_index,
} => { } => {
for i in 0..old_len { for i in 0..old_len {
let old_line_range = &old_line_ranges[old_index]; let old_line_range = &old_line_ranges[old_index];
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
diff_lines.push(DiffLine { diff_lines.push(DiffLine {
op: Op::Delete, op: Op::Delete,
old_line: old_index + i, old_line: Some(old_index + i),
old_content: Some(content), old_content: Some(content),
new_line: new_index, old_byte_range: old_line_range.clone(),
new_line: None,
new_content: 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 { impl ContentDiff {
pub(crate) fn len(&self) -> usize { pub(crate) fn len(&self) -> usize {
self.0.len() self.diff_lines.len()
} }
pub(crate) fn get(&self, i: usize) -> &DiffLine { pub(crate) fn get(&self, i: usize) -> &DiffLine {
&self.0[i] &self.diff_lines[i]
} }
pub(crate) fn last(&self) -> Option<&DiffLine> { 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
}

View File

@@ -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 { pub(crate) enum ContentType {
Text, Text,
Binary, Binary,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FileType {
Rust,
JavaScript,
Unknown,
}
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
@@ -17,8 +27,52 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType {
ContentType::Text ContentType::Text
} else { } else {
match memchr(0, &content[..content.len().min(8192)]) { match memchr(0, &content[..content.len().min(8192)]) {
| None => ContentType::Text, | None => ContentType::Text,
| Some(_) => ContentType::Binary, | 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
}

View File

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

View 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]
}
}