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

@@ -10,7 +10,7 @@ use crate::{
font_icon::{FontIcon, FontIconSvg, font_icon},
text::text,
},
query::{self, QueryStatus, read_query, use_query},
query::{self, QueryStatus, read_query, use_query, watch_query},
util::str::ToSharedString,
};
@@ -56,28 +56,44 @@ pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
impl IssueList {
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
cx.observe(&self.pr_query, |this, _, cx| {
let data = read_query(&this.pr_query, cx);
if let QueryStatus::Loaded(res) = data {
let old_len = this.list_state.item_count();
let new_len = res.items.len();
let pr_query = self.pr_query.clone();
let new_items = res.items.iter().enumerate().map(|(i, it)| IssueListItem {
watch_query(&pr_query, Self::sync_pr_query, cx).detach();
}
fn sync_pr_query(
&mut self,
query: &query::Entity<api::issues::ListPullRequests>,
cx: &mut gpui::Context<Self>,
) {
let data = read_query(query, cx);
if let QueryStatus::Loaded(res) = data {
let selected_id = self
.list_items
.iter()
.find(|item| item.is_selected)
.map(|item| item.id.clone());
let old_len = self.list_state.item_count();
let new_len = res.items.len();
self.list_items = res
.items
.iter()
.enumerate()
.map(|(i, it)| IssueListItem {
is_selected: selected_id.as_ref().is_some_and(|id| *id == it.id),
id: it.id.clone(),
repo_name: Some(it.repo_slug.to_shared_string()),
title: it.title.to_shared_string(),
description: None,
status: it.state,
is_selected: false,
is_last: i == new_len - 1,
is_draft: it.is_draft,
});
})
.collect();
this.list_items.splice(old_len..old_len, new_items);
this.list_state.splice(old_len..old_len, new_len);
}
})
.detach();
self.list_state.splice(0..old_len, new_len);
}
}
fn on_item_click(&mut self, i: usize, cx: &mut gpui::Context<Self>) {

View File

@@ -6,9 +6,10 @@ use crate::{
diff_view::{DiffViewContent, DiffViewState, diff_view},
text::text,
},
query::{self, QueryStatus, observe_query, read_query, use_query},
query::{self, QueryStatus, read_query, use_query, watch_query},
util,
};
use gpui::{ParentElement, Styled, div};
use gpui::{AppContext, ParentElement, Styled, div};
pub(crate) struct PullRequestDiffView {
selected_file_path: Option<Arc<str>>,
@@ -104,23 +105,68 @@ impl PullRequestDiffView {
},
cx,
);
_ = observe_query(
&content_diff_query,
|this, query, cx| {
if let QueryStatus::Loaded(diff) = read_query(query, cx) {
println!("diff len {}", diff.len());
this.diff_view_state.reset(diff.len());
this.diff_view_content = Some(Arc::clone(diff).into());
}
cx.notify();
},
cx,
)
.detach();
_ = watch_query(&content_diff_query, Self::sync_content_diff_query, cx).detach();
self.content_diff_query = Some(content_diff_query);
}
fn sync_content_diff_query(
&mut self,
query: &query::Entity<api::repo::FetchFileDiff>,
cx: &mut gpui::Context<Self>,
) {
if let Some(diff) = {
match read_query(query, cx) {
| QueryStatus::Loaded(diff) => Some(Arc::clone(diff)),
| _ => None,
}
} {
self.load_diff_view(diff, cx);
cx.notify();
}
}
fn load_diff_view(
&mut self,
content_diff: Arc<util::diff::ContentDiff>,
cx: &mut gpui::Context<Self>,
) {
let theme = app::current_theme(cx);
let old_content = content_diff.old_content.clone();
let new_content = content_diff.new_content.clone();
self.diff_view_state.reset(content_diff.len());
self.diff_view_content = Some(content_diff.into());
let theme_syntax = theme.syntax;
if let Some(path) = &self.selected_file_path {
let path = Arc::clone(&path);
let file_type = util::file::file_type_from_path(&path);
let t1 = cx.background_spawn(async move {
util::syntax_highlight::highlight_content(old_content, file_type, &theme_syntax)
});
let t2 = cx.background_spawn(async move {
util::syntax_highlight::highlight_content(new_content, file_type, &theme_syntax)
});
_ = cx
.spawn(async move |weak, cx| match tokio::join!(t1, t2) {
| (Some(old_side_highlights), Some(new_side_highlights)) => {
_ = weak.update(cx, |this, cx| {
this.diff_view_state
.set_old_side_highlights(old_side_highlights);
this.diff_view_state
.set_new_side_highlights(new_side_highlights);
cx.notify();
});
}
| _ => {}
})
.detach();
}
}
}
impl gpui::Render for PullRequestDiffView {

View File

@@ -14,7 +14,7 @@ use crate::{
markdown::{self, MarkdownText},
text::text,
},
query::{self, QueryStatus, read_query, use_query},
query::{self, QueryStatus, read_query, use_query, watch_query},
screen::dashboard::pull_request_diff_view::{self, PullRequestDiffView},
};
@@ -46,21 +46,20 @@ impl PullRequestView {
self.pull_request_query = Some(query.clone());
_ = cx
.observe(&query.clone(), move |this, _, cx| {
this.load_markdown_content(cx);
this.load_pr_diff(cx);
})
.detach();
// cached query will not trigger observe callback
// this is required so that content is loaded immediately for cached query
self.load_markdown_content(cx);
self.load_pr_diff(cx);
_ = watch_query(&query, Self::sync_pull_request_query, cx).detach();
cx.notify();
}
fn sync_pull_request_query(
&mut self,
_query: &query::Entity<api::issues::FetchPullRequest>,
cx: &mut gpui::Context<Self>,
) {
self.load_markdown_content(cx);
self.load_pr_diff(cx);
}
fn load_markdown_content(&mut self, cx: &mut gpui::Context<Self>) {
let Some(query) = &self.pull_request_query else {
return;
@@ -115,41 +114,41 @@ impl PullRequestView {
.rounded_full();
match pr.state {
| api::issues::PullRequestState::Open => {
status_pill = status_pill
.bg(theme.colors.success_solid)
.child(
font_icon(FontIcon::PullRequestArrow)
.size_3()
.text_color(theme.colors.success_on_solid),
)
.child(
text("Open")
.text_color(theme.colors.success_on_solid)
| api::issues::PullRequestState::Open => {
status_pill = status_pill
.bg(theme.colors.success_solid)
.child(
font_icon(FontIcon::PullRequestArrow)
.size_3()
.text_color(theme.colors.success_on_solid),
)
.child(
text("Open")
.text_color(theme.colors.success_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Closed => {
status_pill = status_pill
.bg(theme.colors.danger_solid)
.child(
font_icon(FontIcon::PullRequestClosed)
.size_3()
.text_color(theme.colors.danger_on_solid),
)
.child(
text("Closed")
.text_color(theme.colors.danger_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Merged => {
status_pill = status_pill.bg(theme.colors.accent_solid).child(
text("Merged")
.text_color(theme.colors.accent_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Closed => {
status_pill = status_pill
.bg(theme.colors.danger_solid)
.child(
font_icon(FontIcon::PullRequestClosed)
.size_3()
.text_color(theme.colors.danger_on_solid),
)
.child(
text("Closed")
.text_color(theme.colors.danger_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Merged => {
status_pill = status_pill.bg(theme.colors.accent_solid).child(
text("Merged")
.text_color(theme.colors.accent_on_solid)
.text_xs(),
);
}
}
}
let merge_text = pr.author.as_ref().map(|author| {
@@ -284,23 +283,23 @@ impl gpui::Render for PullRequestView {
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
div().size_full().child(match &self.pull_request_query {
| Some(q) => match read_query(q, cx) {
| QueryStatus::Loaded(pr) => match &self.diff_view {
| Some(v) => v.clone().into_any_element(),
| None => self.pr_content(pr, cx),
},
| Some(q) => match read_query(q, cx) {
| QueryStatus::Loaded(pr) => match &self.diff_view {
| Some(v) => v.clone().into_any_element(),
| None => self.pr_content(pr, cx),
},
| QueryStatus::Err(e) => div()
.size_full()
.child(format!("{:?}", e))
.into_any_element(),
| QueryStatus::Loading => div()
.size_full()
.child("loading pr content")
.into_any_element(),
},
| QueryStatus::Err(e) => div()
.size_full()
.child(format!("{:?}", e))
.into_any_element(),
| QueryStatus::Loading => div()
.size_full()
.child("loading pr content")
.into_any_element(),
},
| None => div().size_full().child("no pr selected").into_any_element(),
| None => div().size_full().child("no pr selected").into_any_element(),
})
}
}

View File

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

View File

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

View File

@@ -183,60 +183,60 @@ impl GithubStepView {
let poll_interval = u64::from(*interval);
match read_query(query, cx) {
| QueryStatus::Loaded(data) => {
let auth_tokens = api::AuthTokens {
access_token: data.access_token.clone(),
};
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
store.update_query_context(|c| {
c.auth = Some(auth_tokens.clone());
});
});
self.user_query = Some(use_query(api::user::Fetch, cx));
cx.spawn(async move |weak, cx| {
let ent = fetch_query(api::user::Fetch, cx).await;
let fut = weak
.update(cx, move |_this, cx| {
let Ok(query) = ent else {
return None;
};
let QueryStatus::Loaded(user) = read_query(&query, cx) else {
return None;
};
Some(storage::store_auth_tokens(&auth_tokens, user, cx))
})
.unwrap_or_default();
_ = if let Some(task) = fut {
task.await
} else {
Err(anyhow::Error::msg(""))
| QueryStatus::Loaded(data) => {
let auth_tokens = api::AuthTokens {
access_token: data.access_token.clone(),
};
})
.detach();
}
| QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
if error == "authorization_pending" {
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
store.update_query_context(|c| {
c.auth = Some(auth_tokens.clone());
});
});
self.user_query = Some(use_query(api::user::Fetch, cx));
cx.spawn(async move |weak, cx| {
Timer::after(Duration::from_secs(poll_interval)).await;
if let Ok(Some(query)) =
weak.read_with(cx, |this, _cx| this.request_access_token_query.clone())
{
let _ = weak.update(cx, |_this, cx| {
query.refetch(cx);
});
}
let ent = fetch_query(api::user::Fetch, cx).await;
let fut = weak
.update(cx, move |_this, cx| {
let Ok(query) = ent else {
return None;
};
let QueryStatus::Loaded(user) = read_query(&query, cx) else {
return None;
};
Some(storage::store_auth_tokens(&auth_tokens, user, cx))
})
.unwrap_or_default();
_ = if let Some(task) = fut {
task.await
} else {
Err(anyhow::Error::msg(""))
};
})
.detach();
}
}
| _ => {}
| QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
if error == "authorization_pending" {
cx.spawn(async move |weak, cx| {
Timer::after(Duration::from_secs(poll_interval)).await;
if let Ok(Some(query)) =
weak.read_with(cx, |this, _cx| this.request_access_token_query.clone())
{
let _ = weak.update(cx, |_this, cx| {
query.refetch(cx);
});
}
})
.detach();
}
}
| _ => {}
}
}
@@ -257,8 +257,8 @@ impl GithubStepView {
let theme = app::current_theme(cx);
let (displayed_code, copyable_code) = match create_device_code_query {
| QueryStatus::Loaded(data) => (data.user_code.as_ref(), Some(data.user_code.clone())),
| _ => (self.placeholder_code.as_str(), None),
| QueryStatus::Loaded(data) => (data.user_code.as_ref(), Some(data.user_code.clone())),
| _ => (self.placeholder_code.as_str(), None),
};
let border_color = theme.colors.border.clone();
@@ -352,14 +352,16 @@ impl gpui::Render for GithubStepView {
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let (can_go_next, header, body) = match self.user_query {
| None => (false, self.header(), self.device_code_area(cx)),
| Some(ref q) => {
let user_query = read_query(q, cx);
match user_query {
| QueryStatus::Loaded(user) => (true, connected_header(), connected_body(user, cx)),
| _ => (false, self.header(), self.device_code_area(cx)),
| None => (false, self.header(), self.device_code_area(cx)),
| Some(ref q) => {
let user_query = read_query(q, cx);
match user_query {
| QueryStatus::Loaded(user) => {
(true, connected_header(), connected_body(user, cx))
}
| _ => (false, self.header(), self.device_code_area(cx)),
}
}
}
};
div()