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

@@ -513,7 +513,9 @@ impl query::QueryFn for FetchPullRequestFileTree {
edge.node.map(|node| ChangedFile { edge.node.map(|node| ChangedFile {
cursor, cursor,
change_type: match node.change_type { change_type: match node.change_type {
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added, | pull_request_file_tree_query::PatchStatus::ADDED => {
ChangeType::Added
}
| pull_request_file_tree_query::PatchStatus::MODIFIED => { | pull_request_file_tree_query::PatchStatus::MODIFIED => {
ChangeType::Modified ChangeType::Modified
} }

View File

@@ -463,29 +463,35 @@ mod tests {
.expect("third timeline fixture json should parse"); .expect("third timeline fixture json should parse");
let first_page_nodes = match first_page.node.as_ref() { let first_page_nodes = match first_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
pull_request
.timeline_items .timeline_items
.nodes .nodes
.as_ref() .as_ref()
.expect("first timeline fixture page should contain timeline nodes"), .expect("first timeline fixture page should contain timeline nodes")
}
| _ => panic!("first timeline fixture page should resolve to a pull request node"), | _ => panic!("first timeline fixture page should resolve to a pull request node"),
}; };
let second_page_nodes = match second_page.node.as_ref() { let second_page_nodes = match second_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
pull_request
.timeline_items .timeline_items
.nodes .nodes
.as_ref() .as_ref()
.expect("second timeline fixture page should contain timeline nodes"), .expect("second timeline fixture page should contain timeline nodes")
}
| _ => panic!("second timeline fixture page should resolve to a pull request node"), | _ => panic!("second timeline fixture page should resolve to a pull request node"),
}; };
let third_page_nodes = match third_page.node.as_ref() { let third_page_nodes = match third_page.node.as_ref() {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => pull_request | Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
pull_request
.timeline_items .timeline_items
.nodes .nodes
.as_ref() .as_ref()
.expect("third timeline fixture page should contain timeline nodes"), .expect("third timeline fixture page should contain timeline nodes")
}
| _ => panic!("third timeline fixture page should resolve to a pull request node"), | _ => panic!("third timeline fixture page should resolve to a pull request node"),
}; };

View File

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

View File

@@ -1,9 +1,9 @@
use std::sync::Arc; use std::{cell::RefCell, rc::Rc, sync::Arc};
use gpui::{IntoElement, ParentElement, Styled, div, list, px}; use gpui::{HighlightStyle, IntoElement, ParentElement, Styled, div, list, px};
use crate::{ use crate::{
component::code_view::{self, CodeLine, code_line}, component::code_view::{self, CodeLine, code_line, code_line_with_highlights},
util::{self, str::ToSharedString}, util::{self, str::ToSharedString},
}; };
@@ -14,7 +14,13 @@ pub(crate) struct DiffView {
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct DiffViewState(gpui::ListState); pub(crate) struct DiffViewState(Rc<RefCell<DiffViewStateInner>>);
struct DiffViewStateInner {
list_state: gpui::ListState,
old_side_highlights: Option<util::syntax_highlight::HighlightedContent>,
new_side_highlights: Option<util::syntax_highlight::HighlightedContent>,
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct DiffViewContent { pub(crate) struct DiffViewContent {
@@ -23,6 +29,7 @@ pub(crate) struct DiffViewContent {
#[derive(Clone, gpui::IntoElement)] #[derive(Clone, gpui::IntoElement)]
struct DiffRow { struct DiffRow {
state: DiffViewState,
line: util::diff::DiffLine, line: util::diff::DiffLine,
old_side_gutter_width: gpui::Pixels, old_side_gutter_width: gpui::Pixels,
new_side_gutter_width: gpui::Pixels, new_side_gutter_width: gpui::Pixels,
@@ -40,22 +47,36 @@ impl From<Arc<util::diff::ContentDiff>> for DiffViewContent {
impl DiffViewState { impl DiffViewState {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self(gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.))) Self(Rc::new(RefCell::new(DiffViewStateInner {
list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)),
old_side_highlights: None,
new_side_highlights: None,
})))
} }
pub(crate) fn reset(&mut self, line_count: usize) { pub(crate) fn reset(&mut self, line_count: usize) {
self.0.reset(line_count); self.0.borrow().list_state.reset(line_count);
}
pub(crate) fn set_old_side_highlights(
&mut self,
highlights: util::syntax_highlight::HighlightedContent,
) {
self.0.borrow_mut().old_side_highlights = Some(highlights);
}
pub(crate) fn set_new_side_highlights(
&mut self,
highlights: util::syntax_highlight::HighlightedContent,
) {
self.0.borrow_mut().new_side_highlights = Some(highlights);
} }
} }
impl gpui::RenderOnce for DiffView { impl gpui::RenderOnce for DiffView {
fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement { fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
let (old_digits, new_digits) = self let old_digits = self.content.diff.old_line_count.to_string().len();
.content let new_digits = self.content.diff.new_line_count.to_string().len();
.diff
.last()
.map(|l| (l.old_line.to_string().len(), l.new_line.to_string().len()))
.unwrap_or((1, 1));
let text_style = window.text_style(); let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size()); let font_size = text_style.font_size.to_pixels(window.rem_size());
@@ -69,8 +90,11 @@ impl gpui::RenderOnce for DiffView {
let old_side_gutter_width = ch * old_digits; let old_side_gutter_width = ch * old_digits;
let new_side_gutter_width = ch * new_digits; let new_side_gutter_width = ch * new_digits;
list(self.state.0, move |i, _, cx| { let list_state = self.state.0.borrow().list_state.clone();
list(list_state, move |i, _, _| {
DiffRow { DiffRow {
state: self.state.clone(),
line: self.content.diff.get(i).clone(), line: self.content.diff.get(i).clone(),
old_side_gutter_width, old_side_gutter_width,
new_side_gutter_width, new_side_gutter_width,
@@ -84,35 +108,69 @@ impl gpui::RenderOnce for DiffView {
impl DiffRow { impl DiffRow {
fn old_code_line(&self) -> CodeLine { fn old_code_line(&self) -> CodeLine {
code_line( let state = self.state.0.borrow();
Some(self.line.old_line),
self.line let content = self
.line
.old_content .old_content
.as_ref() .as_ref()
.map(|it| it.to_shared_string()), .map(|it| it.to_shared_string());
match self.line.op {
let marker = match self.line.op {
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged, | 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, | util::diff::Op::Insert => code_view::CodeLineMarker::Placeholder,
| util::diff::Op::Replace | util::diff::Op::Delete => { // old side replaced, so delete
code_view::CodeLineMarker::Deleted | 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.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 { fn new_code_line(&self) -> CodeLine {
code_line( let state = self.state.0.borrow();
Some(self.line.new_line),
self.line let content = self
.line
.new_content .new_content
.as_ref() .as_ref()
.map(|it| it.to_shared_string()), .map(|it| it.to_shared_string());
match self.line.op {
let marker = match self.line.op {
| util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged, | util::diff::Op::Equal => code_view::CodeLineMarker::Unchanged,
| util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added, | util::diff::Op::Insert | util::diff::Op::Replace => code_view::CodeLineMarker::Added,
| util::diff::Op::Delete => code_view::CodeLineMarker::Deleted, | 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.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),
}
} }
} }

View File

@@ -214,8 +214,10 @@ impl MarkdownText {
if cursor.goto_next_sibling() if cursor.goto_next_sibling()
&& let Ok(src) = cursor.node().utf8_text(content.as_bytes()) && let Ok(src) = cursor.node().utf8_text(content.as_bytes())
{ {
links links.push((
.push((node_range!(), gpui::SharedString::from(String::from(src)))); node_range!(),
gpui::SharedString::from(String::from(src)),
));
} else { } else {
// the link src is invalid, use an empty string as a fallback // the link src is invalid, use an empty string as a fallback
// link on click handler will ignore empty string // link on click handler will ignore empty string

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ use crate::{
markdown::{self, MarkdownText}, markdown::{self, MarkdownText},
text::text, text::text,
}, },
query::{self, QueryStatus, read_query, use_query}, query::{self, QueryStatus, read_query, use_query, watch_query},
screen::dashboard::pull_request_diff_view::{self, PullRequestDiffView}, screen::dashboard::pull_request_diff_view::{self, PullRequestDiffView},
}; };
@@ -46,21 +46,20 @@ impl PullRequestView {
self.pull_request_query = Some(query.clone()); self.pull_request_query = Some(query.clone());
_ = cx _ = watch_query(&query, Self::sync_pull_request_query, cx).detach();
.observe(&query.clone(), move |this, _, cx| {
this.load_markdown_content(cx);
this.load_pr_diff(cx);
})
.detach();
// cached query will not trigger observe callback
// this is required so that content is loaded immediately for cached query
self.load_markdown_content(cx);
self.load_pr_diff(cx);
cx.notify(); cx.notify();
} }
fn sync_pull_request_query(
&mut self,
_query: &query::Entity<api::issues::FetchPullRequest>,
cx: &mut gpui::Context<Self>,
) {
self.load_markdown_content(cx);
self.load_pr_diff(cx);
}
fn load_markdown_content(&mut self, cx: &mut gpui::Context<Self>) { fn load_markdown_content(&mut self, cx: &mut gpui::Context<Self>) {
let Some(query) = &self.pull_request_query else { let Some(query) = &self.pull_request_query else {
return; return;

View File

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

View File

@@ -356,7 +356,9 @@ impl gpui::Render for GithubStepView {
| Some(ref q) => { | Some(ref q) => {
let user_query = read_query(q, cx); let user_query = read_query(q, cx);
match user_query { match user_query {
| QueryStatus::Loaded(user) => (true, connected_header(), connected_body(user, cx)), | QueryStatus::Loaded(user) => {
(true, connected_header(), connected_body(user, cx))
}
| _ => (false, self.header(), self.device_code_area(cx)), | _ => (false, self.header(), self.device_code_area(cx)),
} }
} }

View File

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

View File

@@ -1,6 +1,9 @@
use crate::colors::{hex, hex_alpha}; use crate::colors::{hex, hex_alpha};
use super::{Theme, ThemeColors, ThemeMode}; use super::{
Theme, ThemeColors, ThemeMode, ThemeSyntaxHighlight,
syntax::{build, key},
};
pub(crate) const FAMILY_ID: &str = "catppuccin"; pub(crate) const FAMILY_ID: &str = "catppuccin";
pub(crate) const FAMILY_LABEL: &str = "Catppuccin"; pub(crate) const FAMILY_LABEL: &str = "Catppuccin";
@@ -8,7 +11,31 @@ pub(crate) const FAMILY_LABEL: &str = "Catppuccin";
pub(crate) const LATTE_LABEL: &str = "Catppuccin Latte"; pub(crate) const LATTE_LABEL: &str = "Catppuccin Latte";
pub(crate) const MOCHA_LABEL: &str = "Catppuccin Mocha"; pub(crate) const MOCHA_LABEL: &str = "Catppuccin Mocha";
pub(crate) const fn latte() -> Theme { const fn highlight(color: u32) -> ThemeSyntaxHighlight {
ThemeSyntaxHighlight {
color: hex(color),
font_style: gpui::FontStyle::Normal,
font_weight: None,
}
}
const fn highlight_italic(color: u32) -> ThemeSyntaxHighlight {
ThemeSyntaxHighlight {
color: hex(color),
font_style: gpui::FontStyle::Italic,
font_weight: None,
}
}
const fn highlight_weight(color: u32, font_weight: gpui::FontWeight) -> ThemeSyntaxHighlight {
ThemeSyntaxHighlight {
color: hex(color),
font_style: gpui::FontStyle::Normal,
font_weight: Some(font_weight),
}
}
pub(crate) fn latte() -> Theme {
Theme { Theme {
id: "catppuccin-latte", id: "catppuccin-latte",
name: LATTE_LABEL, name: LATTE_LABEL,
@@ -61,10 +88,63 @@ pub(crate) const fn latte() -> Theme {
info_solid: hex(0x1e66f5), info_solid: hex(0x1e66f5),
info_on_solid: hex(0xeff1f5), info_on_solid: hex(0xeff1f5),
}, },
syntax: build([
(key::ATTRIBUTE, highlight(0x179299)),
(key::BOOLEAN, highlight(0xfe640b)),
(key::COMMENT, highlight(0x7c7f93)),
(key::COMMENT_DOC, highlight(0x8c8fa1)),
(key::CONSTANT, highlight(0xfe640b)),
(key::CONSTRUCTOR, highlight(0x209fb5)),
(key::EMBEDDED, highlight(0x4c4f69)),
(key::EMPHASIS, highlight(0x1e66f5)),
(
key::EMPHASIS_STRONG,
highlight_weight(0xdf8e1d, gpui::FontWeight::BOLD),
),
(key::ENUM, highlight(0x179299)),
(key::FUNCTION, highlight(0x1e66f5)),
(key::HINT, highlight(0x9ca0b0)),
(key::KEYWORD, highlight(0x8839ef)),
(key::LABEL, highlight(0x7287fd)),
(key::LINK_TEXT, highlight_italic(0x1e66f5)),
(key::LINK_URI, highlight(0x179299)),
(key::NAMESPACE, highlight(0x4c4f69)),
(key::NUMBER, highlight(0xfe640b)),
(key::OPERATOR, highlight(0x04a5e5)),
(key::PREDICTIVE, highlight_italic(0x9ca0b0)),
(key::PREPROC, highlight(0x8839ef)),
(key::PRIMARY, highlight(0x4c4f69)),
(key::PROPERTY, highlight(0xdc8a78)),
(key::PUNCTUATION, highlight(0x6c6f85)),
(key::PUNCTUATION_BRACKET, highlight(0x7c7f93)),
(key::PUNCTUATION_DELIMITER, highlight(0x7c7f93)),
(key::PUNCTUATION_LIST_MARKER, highlight(0xd20f39)),
(key::PUNCTUATION_MARKUP, highlight(0xd20f39)),
(key::PUNCTUATION_SPECIAL, highlight(0xe64553)),
(key::SELECTOR, highlight(0x40a02b)),
(key::SELECTOR_PSEUDO, highlight(0x1e66f5)),
(key::STRING, highlight(0x40a02b)),
(key::STRING_ESCAPE, highlight(0xea76cb)),
(key::STRING_REGEX, highlight(0xfe640b)),
(key::STRING_SPECIAL, highlight(0xfe640b)),
(key::STRING_SPECIAL_SYMBOL, highlight(0xfe640b)),
(key::TAG, highlight(0x1e66f5)),
(key::TEXT_LITERAL, highlight(0x40a02b)),
(
key::TITLE,
highlight_weight(0xdc8a78, gpui::FontWeight::NORMAL),
),
(key::TYPE, highlight(0xdf8e1d)),
(key::VARIABLE, highlight(0x4c4f69)),
(key::VARIABLE_SPECIAL, highlight(0xfe640b)),
(key::VARIANT, highlight(0x7287fd)),
(key::DIFF_PLUS, highlight(0x40a02b)),
(key::DIFF_MINUS, highlight(0xd20f39)),
]),
} }
} }
pub(crate) const fn mocha() -> Theme { pub(crate) fn mocha() -> Theme {
Theme { Theme {
id: "catppuccin-mocha", id: "catppuccin-mocha",
name: MOCHA_LABEL, name: MOCHA_LABEL,
@@ -117,5 +197,58 @@ pub(crate) const fn mocha() -> Theme {
info_solid: hex(0x89b4fa), info_solid: hex(0x89b4fa),
info_on_solid: hex(0x1e1e2e), info_on_solid: hex(0x1e1e2e),
}, },
syntax: build([
(key::ATTRIBUTE, highlight(0x94e2d5)),
(key::BOOLEAN, highlight(0xfab387)),
(key::COMMENT, highlight(0x6c7086)),
(key::COMMENT_DOC, highlight(0x7f849c)),
(key::CONSTANT, highlight(0xfab387)),
(key::CONSTRUCTOR, highlight(0x74c7ec)),
(key::EMBEDDED, highlight(0xcdd6f4)),
(key::EMPHASIS, highlight(0x89b4fa)),
(
key::EMPHASIS_STRONG,
highlight_weight(0xf9e2af, gpui::FontWeight::BOLD),
),
(key::ENUM, highlight(0x94e2d5)),
(key::FUNCTION, highlight(0x89b4fa)),
(key::HINT, highlight(0x9399b2)),
(key::KEYWORD, highlight(0xcba6f7)),
(key::LABEL, highlight(0xb4befe)),
(key::LINK_TEXT, highlight_italic(0x89b4fa)),
(key::LINK_URI, highlight(0x94e2d5)),
(key::NAMESPACE, highlight(0xcdd6f4)),
(key::NUMBER, highlight(0xfab387)),
(key::OPERATOR, highlight(0x89dceb)),
(key::PREDICTIVE, highlight_italic(0x9399b2)),
(key::PREPROC, highlight(0xcba6f7)),
(key::PRIMARY, highlight(0xcdd6f4)),
(key::PROPERTY, highlight(0xf5e0dc)),
(key::PUNCTUATION, highlight(0xa6adc8)),
(key::PUNCTUATION_BRACKET, highlight(0x9399b2)),
(key::PUNCTUATION_DELIMITER, highlight(0x9399b2)),
(key::PUNCTUATION_LIST_MARKER, highlight(0xf38ba8)),
(key::PUNCTUATION_MARKUP, highlight(0xf38ba8)),
(key::PUNCTUATION_SPECIAL, highlight(0xeba0ac)),
(key::SELECTOR, highlight(0xa6e3a1)),
(key::SELECTOR_PSEUDO, highlight(0x89b4fa)),
(key::STRING, highlight(0xa6e3a1)),
(key::STRING_ESCAPE, highlight(0xf5c2e7)),
(key::STRING_REGEX, highlight(0xfab387)),
(key::STRING_SPECIAL, highlight(0xfab387)),
(key::STRING_SPECIAL_SYMBOL, highlight(0xfab387)),
(key::TAG, highlight(0x89b4fa)),
(key::TEXT_LITERAL, highlight(0xa6e3a1)),
(
key::TITLE,
highlight_weight(0xf5e0dc, gpui::FontWeight::NORMAL),
),
(key::TYPE, highlight(0xf9e2af)),
(key::VARIABLE, highlight(0xcdd6f4)),
(key::VARIABLE_SPECIAL, highlight(0xfab387)),
(key::VARIANT, highlight(0xb4befe)),
(key::DIFF_PLUS, highlight(0xa6e3a1)),
(key::DIFF_MINUS, highlight(0xf38ba8)),
]),
} }
} }

165
src/theme/syntax.rs Normal file
View File

@@ -0,0 +1,165 @@
use std::collections::BTreeMap;
use gpui::Rgba;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThemeSyntaxHighlight {
pub color: Rgba,
pub font_style: gpui::FontStyle,
pub font_weight: Option<gpui::FontWeight>,
}
pub const HIGHLIGHT_NAME_COUNT: usize = HIGHLIGHT_NAMES.len();
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThemeSyntax([ThemeSyntaxHighlight; HIGHLIGHT_NAME_COUNT]);
impl ThemeSyntax {
pub fn resolve(&self, key: &str) -> ThemeSyntaxHighlight {
Self::index_of(key)
.and_then(|index| self.get(index))
.unwrap_or_else(|| self.text_literal())
}
pub fn get(&self, index: usize) -> Option<ThemeSyntaxHighlight> {
self.0.get(index).copied()
}
pub fn as_slice(&self) -> &[ThemeSyntaxHighlight] {
&self.0
}
fn text_literal(&self) -> ThemeSyntaxHighlight {
self.get(Self::index_of(key::TEXT_LITERAL).expect("missing text.literal index"))
.expect("theme syntax must define text.literal")
}
fn index_of(key: &str) -> Option<usize> {
HIGHLIGHT_NAMES
.iter()
.position(|candidate| *candidate == key)
}
}
pub const HIGHLIGHT_NAMES: &[&str] = &[
key::ATTRIBUTE,
key::BOOLEAN,
key::COMMENT,
key::COMMENT_DOC,
key::CONSTANT,
key::CONSTRUCTOR,
key::DIFF_MINUS,
key::DIFF_PLUS,
key::EMBEDDED,
key::EMPHASIS,
key::EMPHASIS_STRONG,
key::ENUM,
key::FUNCTION,
key::HINT,
key::KEYWORD,
key::LABEL,
key::LINK_TEXT,
key::LINK_URI,
key::NAMESPACE,
key::NUMBER,
key::OPERATOR,
key::PREDICTIVE,
key::PREPROC,
key::PRIMARY,
key::PROPERTY,
key::PUNCTUATION,
key::PUNCTUATION_BRACKET,
key::PUNCTUATION_DELIMITER,
key::PUNCTUATION_LIST_MARKER,
key::PUNCTUATION_MARKUP,
key::PUNCTUATION_SPECIAL,
key::SELECTOR,
key::SELECTOR_PSEUDO,
key::STRING,
key::STRING_ESCAPE,
key::STRING_REGEX,
key::STRING_SPECIAL,
key::STRING_SPECIAL_SYMBOL,
key::TAG,
key::TEXT_LITERAL,
key::TITLE,
key::TYPE,
key::VARIABLE,
key::VARIABLE_SPECIAL,
key::VARIANT,
];
pub(crate) fn build(
entries: impl IntoIterator<Item = (&'static str, ThemeSyntaxHighlight)>,
) -> ThemeSyntax {
let syntax_by_name: BTreeMap<&'static str, ThemeSyntaxHighlight> = entries
.into_iter()
.map(|(name, style)| (name, style))
.collect();
debug_assert!(syntax_by_name.contains_key(key::TEXT_LITERAL));
debug_assert!(
syntax_by_name
.keys()
.all(|name| HIGHLIGHT_NAMES.contains(name))
);
let fallback = *syntax_by_name
.get(key::TEXT_LITERAL)
.expect("theme syntax must define text.literal");
ThemeSyntax(std::array::from_fn(|index| {
syntax_by_name
.get(HIGHLIGHT_NAMES[index])
.copied()
.unwrap_or(fallback)
}))
}
pub(crate) mod key {
pub const ATTRIBUTE: &str = "attribute";
pub const BOOLEAN: &str = "boolean";
pub const COMMENT: &str = "comment";
pub const COMMENT_DOC: &str = "comment.doc";
pub const CONSTANT: &str = "constant";
pub const CONSTRUCTOR: &str = "constructor";
pub const DIFF_MINUS: &str = "diff.minus";
pub const DIFF_PLUS: &str = "diff.plus";
pub const EMBEDDED: &str = "embedded";
pub const EMPHASIS: &str = "emphasis";
pub const EMPHASIS_STRONG: &str = "emphasis.strong";
pub const ENUM: &str = "enum";
pub const FUNCTION: &str = "function";
pub const HINT: &str = "hint";
pub const KEYWORD: &str = "keyword";
pub const LABEL: &str = "label";
pub const LINK_TEXT: &str = "link_text";
pub const LINK_URI: &str = "link_uri";
pub const NAMESPACE: &str = "namespace";
pub const NUMBER: &str = "number";
pub const OPERATOR: &str = "operator";
pub const PREDICTIVE: &str = "predictive";
pub const PREPROC: &str = "preproc";
pub const PRIMARY: &str = "primary";
pub const PROPERTY: &str = "property";
pub const PUNCTUATION: &str = "punctuation";
pub const PUNCTUATION_BRACKET: &str = "punctuation.bracket";
pub const PUNCTUATION_DELIMITER: &str = "punctuation.delimiter";
pub const PUNCTUATION_LIST_MARKER: &str = "punctuation.list_marker";
pub const PUNCTUATION_MARKUP: &str = "punctuation.markup";
pub const PUNCTUATION_SPECIAL: &str = "punctuation.special";
pub const SELECTOR: &str = "selector";
pub const SELECTOR_PSEUDO: &str = "selector.pseudo";
pub const STRING: &str = "string";
pub const STRING_ESCAPE: &str = "string.escape";
pub const STRING_REGEX: &str = "string.regex";
pub const STRING_SPECIAL: &str = "string.special";
pub const STRING_SPECIAL_SYMBOL: &str = "string.special.symbol";
pub const TAG: &str = "tag";
pub const TEXT_LITERAL: &str = "text.literal";
pub const TITLE: &str = "title";
pub const TYPE: &str = "type";
pub const VARIABLE: &str = "variable";
pub const VARIABLE_SPECIAL: &str = "variable.special";
pub const VARIANT: &str = "variant";
}

View File

@@ -1,13 +1,8 @@
use std::{ops::Range, slice::Iter, sync::Arc, thread::current}; use std::sync::Arc;
use memchr::{memchr2, memchr2_iter, memchr3_iter};
use similar::DiffableStr; use similar::DiffableStr;
pub(crate) struct Span { use crate::util;
pub(crate) op: Op,
pub(crate) old_range: Range<usize>,
pub(crate) new_range: Range<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Op { pub(crate) enum Op {
@@ -17,39 +12,32 @@ pub(crate) enum Op {
Replace, Replace,
} }
#[derive(Clone, Copy)]
pub(crate) enum DiffSide {
Old,
New,
}
pub(crate) struct DiffRow {
pub(crate) op_index: usize,
pub(crate) op: Op,
pub(crate) old_line: Option<usize>,
pub(crate) old_content_range: Option<Range<usize>>,
pub(crate) new_line: Option<usize>,
pub(crate) new_content_range: Option<Range<usize>>,
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct DiffLine { pub(crate) struct DiffLine {
pub(crate) op: Op, pub(crate) op: Op,
pub(crate) old_content: Option<Arc<str>>, pub(crate) old_content: Option<Arc<str>>,
pub(crate) old_line: usize, pub(crate) old_line: Option<usize>,
pub(crate) old_byte_range: std::ops::Range<usize>,
pub(crate) new_content: Option<Arc<str>>, pub(crate) new_content: Option<Arc<str>>,
pub(crate) new_line: usize, pub(crate) new_line: Option<usize>,
pub(crate) new_byte_range: std::ops::Range<usize>,
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ContentDiff(Vec<DiffLine>); pub(crate) struct ContentDiff {
pub(crate) diff_lines: Vec<DiffLine>,
pub(crate) old_content: bytes::Bytes,
pub(crate) old_line_count: usize,
pub(crate) new_content: bytes::Bytes,
pub(crate) new_line_count: usize,
}
pub(crate) fn diff_content( pub(crate) fn diff_content(
old_content: bytes::Bytes, old_content: bytes::Bytes,
new_content: bytes::Bytes, new_content: bytes::Bytes,
) -> Option<ContentDiff> { ) -> Option<ContentDiff> {
let old_line_ranges = line_ranges(&old_content); let old_line_ranges = util::file::line_ranges(&old_content);
let new_line_ranges = line_ranges(&new_content); let new_line_ranges = util::file::line_ranges(&new_content);
let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content); let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content);
let mut diff_lines: Vec<DiffLine> = Vec::new(); let mut diff_lines: Vec<DiffLine> = Vec::new();
@@ -68,28 +56,30 @@ pub(crate) fn diff_content(
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
diff_lines.push(DiffLine { diff_lines.push(DiffLine {
op: Op::Equal, op: Op::Equal,
old_line, old_line: Some(old_line),
old_content: Some(Arc::clone(&content)), old_content: Some(Arc::clone(&content)),
new_line, old_byte_range: old_line_range.clone(),
new_line: Some(new_line),
new_content: Some(content), new_content: Some(content),
new_byte_range: new_line_ranges[new_line].clone(),
}); });
} }
} }
| &similar::DiffOp::Insert { | &similar::DiffOp::Insert {
old_index, new_index, new_len, ..
new_index,
new_len,
} => { } => {
for i in 0..new_len { for i in 0..new_len {
let new_line_range = &new_line_ranges[new_index + i]; let new_line_range = &new_line_ranges[new_index + i];
let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?); let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?);
diff_lines.push(DiffLine { diff_lines.push(DiffLine {
op: Op::Insert, op: Op::Insert,
old_line: old_index, old_line: None,
old_content: None, old_content: None,
new_line: new_index + i, old_byte_range: 0..0,
new_line: Some(new_index + i),
new_content: Some(content), new_content: Some(content),
new_byte_range: new_line_range.clone(),
}) })
} }
} }
@@ -108,26 +98,32 @@ pub(crate) fn diff_content(
{ {
| (Some(old_range), Some(new_range)) => DiffLine { | (Some(old_range), Some(new_range)) => DiffLine {
op: Op::Replace, op: Op::Replace,
old_line, old_line: Some(old_line),
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)), old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
new_line: new_index + i, old_byte_range: old_range.clone(),
new_line: Some(new_line),
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)), new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
new_byte_range: new_range.clone(),
}, },
| (None, Some(new_range)) => DiffLine { | (None, Some(new_range)) => DiffLine {
op: Op::Replace, op: Op::Replace,
old_line: old_index + old_len, old_line: None,
old_content: None, old_content: None,
new_line: new_index + i, old_byte_range: 0..0,
new_line: Some(new_index + i),
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)), new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
new_byte_range: new_range.clone(),
}, },
| (Some(old_range), None) => DiffLine { | (Some(old_range), None) => DiffLine {
op: Op::Replace, op: Op::Replace,
old_line: old_index + i, old_line: Some(old_index + i),
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)), old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
new_line: new_index + new_len, old_byte_range: old_range.clone(),
new_line: None,
new_content: None, new_content: None,
new_byte_range: 0..0,
}, },
| (None, None) => { | (None, None) => {
@@ -143,74 +139,44 @@ pub(crate) fn diff_content(
} }
| &similar::DiffOp::Delete { | &similar::DiffOp::Delete {
old_index, old_index, old_len, ..
old_len,
new_index,
} => { } => {
for i in 0..old_len { for i in 0..old_len {
let old_line_range = &old_line_ranges[old_index]; let old_line_range = &old_line_ranges[old_index];
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
diff_lines.push(DiffLine { diff_lines.push(DiffLine {
op: Op::Delete, op: Op::Delete,
old_line: old_index + i, old_line: Some(old_index + i),
old_content: Some(content), old_content: Some(content),
new_line: new_index, old_byte_range: old_line_range.clone(),
new_line: None,
new_content: None, new_content: None,
new_byte_range: 0..0,
}) })
} }
} }
} }
} }
Some(ContentDiff(diff_lines)) Some(ContentDiff {
diff_lines,
old_content,
old_line_count: old_line_ranges.len(),
new_content,
new_line_count: new_line_ranges.len(),
})
} }
impl ContentDiff { impl ContentDiff {
pub(crate) fn len(&self) -> usize { pub(crate) fn len(&self) -> usize {
self.0.len() self.diff_lines.len()
} }
pub(crate) fn get(&self, i: usize) -> &DiffLine { pub(crate) fn get(&self, i: usize) -> &DiffLine {
&self.0[i] &self.diff_lines[i]
} }
pub(crate) fn last(&self) -> Option<&DiffLine> { pub(crate) fn last(&self) -> Option<&DiffLine> {
self.0.last() self.diff_lines.last()
} }
} }
fn line_ranges(content: &[u8]) -> Vec<Range<usize>> {
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
let mut line_start: usize = 0;
let mut skip_next = false;
for i in memchr2_iter(b'\n', b'\r', content) {
if skip_next {
skip_next = false;
continue;
}
let c = content[i];
match (c, content.get(i + 1)) {
| (b'\r', Some(b'\n')) => {
// if \r found, check if its \r\n or if its a lone \r
// if \r\n, then treat as one line break
ranges.push(line_start..i + 1);
// because we already counted the \n byte, the next iter into it needs to be skipped
skip_next = true;
line_start = i + 2;
}
| _ => {
ranges.push(line_start..i);
line_start = i + 1;
}
}
}
if line_start < content.len() {
ranges.push(line_start..content.len());
}
ranges
}

View File

@@ -1,10 +1,20 @@
use memchr::memchr; use std::path::Path;
use memchr::{memchr, memchr2_iter};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ContentType { pub(crate) enum ContentType {
Text, Text,
Binary, Binary,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FileType {
Rust,
JavaScript,
Unknown,
}
pub(crate) fn classify_content(content: &[u8]) -> ContentType { pub(crate) fn classify_content(content: &[u8]) -> ContentType {
if content.is_empty() { if content.is_empty() {
ContentType::Text ContentType::Text
@@ -22,3 +32,47 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType {
} }
} }
} }
pub(crate) fn file_type_from_path(path: &str) -> FileType {
match Path::new(path).extension().map(|it| it.to_str()).flatten() {
| Some("rs") => FileType::Rust,
| Some("js") | Some("jsx") => FileType::JavaScript,
| _ => FileType::Unknown,
}
}
pub(crate) fn line_ranges(content: &[u8]) -> Vec<std::ops::Range<usize>> {
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
let mut line_start: usize = 0;
let mut skip_next = false;
for i in memchr2_iter(b'\n', b'\r', content) {
if skip_next {
skip_next = false;
continue;
}
let c = content[i];
match (c, content.get(i + 1)) {
| (b'\r', Some(b'\n')) => {
// if \r found, check if its \r\n or if its a lone \r
// if \r\n, then treat as one line break
ranges.push(line_start..i + 1);
// because we already counted the \n byte, the next iter into it needs to be skipped
skip_next = true;
line_start = i + 2;
}
| _ => {
ranges.push(line_start..i);
line_start = i + 1;
}
}
}
if line_start < content.len() {
ranges.push(line_start..content.len());
}
ranges
}

View File

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

View File

@@ -0,0 +1,91 @@
use crate::{theme, util};
pub(crate) struct HighlightedContent(Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>>);
fn ts_highlight_configuration_for_file_type(
file_type: util::file::FileType,
) -> Option<tree_sitter_highlight::HighlightConfiguration> {
match file_type {
| util::file::FileType::Rust => tree_sitter_highlight::HighlightConfiguration::new(
tree_sitter_rust::LANGUAGE.into(),
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
tree_sitter_rust::INJECTIONS_QUERY,
"",
)
.ok(),
| _ => None,
}
}
pub(crate) fn highlight_content(
content: impl AsRef<[u8]>,
file_type: util::file::FileType,
theme_syntax: &theme::ThemeSyntax,
) -> Option<HighlightedContent> {
let mut config = ts_highlight_configuration_for_file_type(file_type)?;
config.configure(theme::syntax::HIGHLIGHT_NAMES);
let mut highlighter = tree_sitter_highlight::Highlighter::new();
let events = highlighter
.highlight(&config, content.as_ref(), None, |_| None)
.ok()?;
let default_highlight = theme_syntax.resolve(theme::syntax::key::TEXT_LITERAL);
let mut highlight = default_highlight;
let line_ranges = util::file::line_ranges(content.as_ref());
let mut highlights: Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>> =
Vec::with_capacity(line_ranges.len());
let mut current_line: usize = 0;
for highlight_event in events {
match highlight_event.ok()? {
| tree_sitter_highlight::HighlightEvent::HighlightStart(h) => {
highlight = theme_syntax.as_slice()[h.0];
}
| tree_sitter_highlight::HighlightEvent::Source { start, end } => {
while current_line < line_ranges.len() && start >= line_ranges[current_line].end {
highlights.push(Vec::new());
current_line += 1;
}
let mut line = current_line;
while line < line_ranges.len() && end > line_ranges[line].start {
if highlights.get(line).is_none() {
highlights.push(Vec::new());
}
let line_range = &line_ranges[line];
let highlight_start = start.max(line_range.start);
let highlight_end = end.min(line_range.end);
highlights[line].push((
(highlight_start - line_range.start)..(highlight_end - line_range.start),
gpui::HighlightStyle {
color: Some(highlight.color.into()),
font_weight: highlight.font_weight,
font_style: Some(highlight.font_style),
..Default::default()
},
));
line += 1;
}
}
| tree_sitter_highlight::HighlightEvent::HighlightEnd => {
highlight = default_highlight;
}
}
}
Some(HighlightedContent(highlights))
}
impl HighlightedContent {
pub(crate) fn highlights_at_line(
&self,
line: usize,
) -> &Vec<(std::ops::Range<usize>, gpui::HighlightStyle)> {
&self.0[line]
}
}