diff --git a/src/api.rs b/src/api.rs index 2765410..ab7d979 100644 --- a/src/api.rs +++ b/src/api.rs @@ -116,9 +116,9 @@ async fn raw_content(res: reqwest::Response) -> Result { Ok(res.bytes().await?) } else { match res.status() { - | reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist), - | reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed), - | _ => Err(Error::MalformedResponse(res.status().to_string())), + | reqwest::StatusCode::NOT_FOUND => Err(Error::DoesNotExist), + | reqwest::StatusCode::FORBIDDEN => Err(Error::NotAllowed), + | _ => Err(Error::MalformedResponse(res.status().to_string())), } } } @@ -155,7 +155,7 @@ where { let mut body: graphql_client::Response = res.json().await?; match body.data.take() { - | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), - | Some(data) => Ok((body, data)), + | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), + | Some(data) => Ok((body, data)), } } diff --git a/src/api/issues.rs b/src/api/issues.rs index 9d2b03f..605f4b2 100644 --- a/src/api/issues.rs +++ b/src/api/issues.rs @@ -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::>() @@ -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; diff --git a/src/api/mock.rs b/src/api/mock.rs index bc9dfad..3dbbcd6 100644 --- a/src/api/mock.rs +++ b/src/api/mock.rs @@ -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!( diff --git a/src/component/code_view.rs b/src/component/code_view.rs index 9fdc86a..b24d41e 100644 --- a/src/component/code_view.rs +++ b/src/component/code_view.rs @@ -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, - content: Option, + content: Option, 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) -> Self { - Self { - lines: lines.into(), - } +pub(crate) fn code_line_with_highlights( + line_index: Option, + content: Option, + highlights: impl IntoIterator, 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!()) } } diff --git a/src/component/diff_view.rs b/src/component/diff_view.rs index 828a321..cc18bc9 100644 --- a/src/component/diff_view.rs +++ b/src/component/diff_view.rs @@ -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>); + +struct DiffViewStateInner { + list_state: gpui::ListState, + old_side_highlights: Option, + new_side_highlights: Option, +} #[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> 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), + } } } diff --git a/src/component/markdown.rs b/src/component/markdown.rs index d6e0842..efb48fd 100644 --- a/src/component/markdown.rs +++ b/src/component/markdown.rs @@ -115,7 +115,7 @@ pub(crate) fn new(content: Arc, cx: &mut gpui::Context) -> 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::() - .unwrap() - }); - let j = *i; - *i = j + 1; - Some(format!("{j}.")) - } + | marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => { + let i = list_index.get_or_insert_with(|| { + marker_content + .strip_suffix('.') + .unwrap() + .parse::() + .unwrap() + }); + let j = *i; + *i = j + 1; + Some(format!("{j}.")) + } - | _ => None, + | _ => None, }; let Some(list_marker_char) = list_marker_char else { @@ -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 + } } }); diff --git a/src/query.rs b/src/query.rs index aaa5f74..0a81828 100644 --- a/src/query.rs +++ b/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( - query: &Entity, - mut on_notify: impl FnMut(&mut E, &Entity, &mut gpui::Context) + 'static, - cx: &mut gpui::Context, -) -> gpui::Subscription +pub fn watch_query(query: &Entity, on_notify: H, cx: &mut gpui::Context) -> gpui::Subscription where E: 'static, F: QueryFn, + H: Fn(&mut E, &Entity, &mut gpui::Context) + 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 ================== diff --git a/src/screen/dashboard/issue_list.rs b/src/screen/dashboard/issue_list.rs index 530c561..625458a 100644 --- a/src/screen/dashboard/issue_list.rs +++ b/src/screen/dashboard/issue_list.rs @@ -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 { impl IssueList { fn on_create(&mut self, cx: &mut gpui::Context) { - 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, + cx: &mut gpui::Context, + ) { + 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) { diff --git a/src/screen/dashboard/pull_request_diff_view.rs b/src/screen/dashboard/pull_request_diff_view.rs index 4c3badc..a4c4258 100644 --- a/src/screen/dashboard/pull_request_diff_view.rs +++ b/src/screen/dashboard/pull_request_diff_view.rs @@ -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>, @@ -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, + cx: &mut gpui::Context, + ) { + 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, + cx: &mut gpui::Context, + ) { + 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 { diff --git a/src/screen/dashboard/pull_request_view.rs b/src/screen/dashboard/pull_request_view.rs index d9f853a..46441c3 100644 --- a/src/screen/dashboard/pull_request_view.rs +++ b/src/screen/dashboard/pull_request_view.rs @@ -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, + cx: &mut gpui::Context, + ) { + self.load_markdown_content(cx); + self.load_pr_diff(cx); + } + fn load_markdown_content(&mut self, cx: &mut gpui::Context) { 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, ) -> 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(), }) } } diff --git a/src/screen/dashboard/screen.rs b/src/screen/dashboard/screen.rs index 984640e..c873917 100644 --- a/src/screen/dashboard/screen.rs +++ b/src/screen/dashboard/screen.rs @@ -36,9 +36,9 @@ impl Screen { fn on_create(&mut self, cx: &mut gpui::Context) { _ = 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(); } diff --git a/src/screen/diffops_playground.rs b/src/screen/diffops_playground.rs index eda3a93..74241eb 100644 --- a/src/screen/diffops_playground.rs +++ b/src/screen/diffops_playground.rs @@ -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, new_lines: Vec, op_groups: Vec, @@ -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 = 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 { let mut lines = Vec::new(); @@ -673,17 +675,17 @@ fn collect_source_lines(diff: &ContentDiff, side: SourceSide) -> Vec 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 { 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 { groups } -fn group_range(rows: &[DiffLine], side: SourceSide) -> Range { - 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 { 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 { 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, diff --git a/src/screen/setup_wizard/github_step.rs b/src/screen/setup_wizard/github_step.rs index 2f2e838..198ac3b 100644 --- a/src/screen/setup_wizard/github_step.rs +++ b/src/screen/setup_wizard/github_step.rs @@ -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::, _>(|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::, _>(|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, ) -> 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() diff --git a/src/theme.rs b/src/theme.rs index 1e7eb32..bce8afa 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -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(), diff --git a/src/theme/catppuccin.rs b/src/theme/catppuccin.rs index b03f2aa..1c6a38d 100644 --- a/src/theme/catppuccin.rs +++ b/src/theme/catppuccin.rs @@ -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)), + ]), } } diff --git a/src/theme/syntax.rs b/src/theme/syntax.rs new file mode 100644 index 0000000..1d5c62a --- /dev/null +++ b/src/theme/syntax.rs @@ -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, +} + +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 { + 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 { + 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, +) -> 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"; +} diff --git a/src/util/diff.rs b/src/util/diff.rs index 926414a..a7e9bc9 100644 --- a/src/util/diff.rs +++ b/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, - pub(crate) new_range: Range, -} +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, - pub(crate) old_content_range: Option>, - pub(crate) new_line: Option, - pub(crate) new_content_range: Option>, -} - #[derive(Clone)] pub(crate) struct DiffLine { pub(crate) op: Op, pub(crate) old_content: Option>, - pub(crate) old_line: usize, + pub(crate) old_line: Option, + pub(crate) old_byte_range: std::ops::Range, pub(crate) new_content: Option>, - pub(crate) new_line: usize, + pub(crate) new_line: Option, + pub(crate) new_byte_range: std::ops::Range, } #[derive(Clone)] -pub(crate) struct ContentDiff(Vec); +pub(crate) struct ContentDiff { + pub(crate) diff_lines: Vec, + 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 { - 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 = 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> { - let mut ranges: Vec> = 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 -} diff --git a/src/util/file.rs b/src/util/file.rs index 4e469ab..9e807e2 100644 --- a/src/util/file.rs +++ b/src/util/file.rs @@ -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> { + let mut ranges: Vec> = 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 +} diff --git a/src/util/mod.rs b/src/util/mod.rs index e7b204b..91dd000 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -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; diff --git a/src/util/syntax_highlight.rs b/src/util/syntax_highlight.rs new file mode 100644 index 0000000..9f7274e --- /dev/null +++ b/src/util/syntax_highlight.rs @@ -0,0 +1,91 @@ +use crate::{theme, util}; + +pub(crate) struct HighlightedContent(Vec, gpui::HighlightStyle)>>); + +fn ts_highlight_configuration_for_file_type( + file_type: util::file::FileType, +) -> Option { + 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 { + 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, 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, gpui::HighlightStyle)> { + &self.0[line] + } +}