Add DiffOps playground
This commit is contained in:
@@ -6,7 +6,7 @@ use tokio::sync::OwnedRwLockReadGuard;
|
||||
use crate::{
|
||||
api,
|
||||
query::{self, Query, fetch_query},
|
||||
util::file,
|
||||
util::{self, file},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -79,8 +79,8 @@ impl query::QueryFn for FetchFileContent {
|
||||
|
||||
fn key(&self) -> query::Key {
|
||||
match &self.reff {
|
||||
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
|
||||
| None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(),
|
||||
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
|
||||
| None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +95,11 @@ impl query::QueryFn for FetchFileContent {
|
||||
}
|
||||
|
||||
let path = match &self.reff {
|
||||
| Some(reff) => format!(
|
||||
"/repos/{}/contents/{}?ref={}",
|
||||
self.repo_slug, self.path, reff
|
||||
),
|
||||
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
|
||||
| Some(reff) => format!(
|
||||
"/repos/{}/contents/{}?ref={}",
|
||||
self.repo_slug, self.path, reff
|
||||
),
|
||||
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
|
||||
};
|
||||
|
||||
let res = c
|
||||
@@ -113,13 +113,13 @@ impl query::QueryFn for FetchFileContent {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueryFileDiff {
|
||||
pub struct FetchFileDiff {
|
||||
pub base: FileRef,
|
||||
pub head: FileRef,
|
||||
}
|
||||
|
||||
impl query::QueryFn for QueryFileDiff {
|
||||
type Data = Option<()>;
|
||||
impl query::QueryFn for FetchFileDiff {
|
||||
type Data = util::diff::ContentDiff;
|
||||
type Error = api::Error;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
@@ -134,11 +134,11 @@ impl query::QueryFn for QueryFileDiff {
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
async fn fetch_content(
|
||||
r: &FileRef,
|
||||
c: &<QueryFileDiff as query::QueryFn>::Context,
|
||||
c: &<FetchFileDiff as query::QueryFn>::Context,
|
||||
) -> Result<Option<bytes::Bytes>, api::Error> {
|
||||
let path = match &r.reff {
|
||||
| Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff),
|
||||
| None => format!("/repos/{}/contents/{}", r.repo_slug, r.path),
|
||||
| Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff),
|
||||
| None => format!("/repos/{}/contents/{}", r.repo_slug, r.path),
|
||||
};
|
||||
|
||||
let res = c
|
||||
@@ -160,17 +160,10 @@ impl query::QueryFn for QueryFileDiff {
|
||||
let (old, new) = tokio::join!(fetch_content(&self.base, c), fetch_content(&self.head, c),);
|
||||
|
||||
match (old, new) {
|
||||
| (Ok(Some(ref old)), Ok(Some(ref new))) => {
|
||||
let diff = similar::TextDiff::from_lines::<[u8]>(old, new);
|
||||
for change in diff.iter_all_changes() {}
|
||||
}
|
||||
| _ => {
|
||||
return Err(api::Error::MalformedResponse(
|
||||
| (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)),
|
||||
| _ => Err(api::Error::MalformedResponse(
|
||||
"failed to fetch content".to_string(),
|
||||
));
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{bounds, point, prelude::*, px, size};
|
||||
|
||||
use crate::screen::{dashboard, setup_wizard};
|
||||
use crate::screen::{dashboard, diffops_playground, setup_wizard};
|
||||
|
||||
mod api;
|
||||
mod app;
|
||||
@@ -62,6 +62,11 @@ fn setup_application(cx: &mut gpui::App) {
|
||||
cx.set_global(global);
|
||||
cx.set_global(query_store);
|
||||
|
||||
if diffops_playground::is_enabled() {
|
||||
_ = diffops_playground::open_window(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: handle failure
|
||||
_ = storage::ensure_data_dir();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
api,
|
||||
api, app,
|
||||
component::text::text,
|
||||
query::{self, QueryStatus, read_query, use_query},
|
||||
};
|
||||
|
||||
@@ -7,16 +8,14 @@ pub(crate) struct PullRequestDiffView {
|
||||
selected_file_path: Option<String>,
|
||||
|
||||
pr_query: query::Entity<api::issues::FetchPullRequest>,
|
||||
old_content_query: Option<query::Entity<api::repo::FetchFileContent>>,
|
||||
new_content_query: Option<query::Entity<api::repo::FetchFileContent>>,
|
||||
content_diff_query: Option<query::Entity<api::repo::FetchFileDiff>>,
|
||||
}
|
||||
|
||||
fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<PullRequestDiffView>) -> PullRequestDiffView {
|
||||
let mut view = PullRequestDiffView {
|
||||
selected_file_path: None,
|
||||
pr_query: use_query(api::issues::FetchPullRequest { id: pr_id }, cx),
|
||||
old_content_query: None,
|
||||
new_content_query: None,
|
||||
content_diff_query: None,
|
||||
};
|
||||
view.on_create(cx);
|
||||
view
|
||||
@@ -35,15 +34,15 @@ impl PullRequestDiffView {
|
||||
}
|
||||
|
||||
fn start_content_queries(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
let Some((old_content_query, new_content_query)) = ({
|
||||
let Some((old_file_ref, new_file_ref)) = ({
|
||||
if let QueryStatus::Loaded(pr) = read_query(&self.pr_query, cx) {
|
||||
Some((
|
||||
api::repo::FetchFileContent {
|
||||
api::repo::FileRef {
|
||||
repo_slug: pr.base_repo_slug.clone(),
|
||||
path: pr.base_branch_name.clone(),
|
||||
reff: Some(pr.base_ref.clone()),
|
||||
},
|
||||
api::repo::FetchFileContent {
|
||||
api::repo::FileRef {
|
||||
repo_slug: pr.head_repo_slug.clone(),
|
||||
path: pr.head_branch_name.clone(),
|
||||
reff: Some(pr.head_ref.clone()),
|
||||
@@ -56,24 +55,38 @@ impl PullRequestDiffView {
|
||||
return;
|
||||
};
|
||||
|
||||
let old_content_query = use_query(old_content_query, cx);
|
||||
let new_content_query = use_query(new_content_query, cx);
|
||||
let content_diff_query = use_query(
|
||||
api::repo::FetchFileDiff {
|
||||
base: old_file_ref,
|
||||
head: new_file_ref,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
_ = cx.observe(&old_content_query, |this, _, cx| {}).detach();
|
||||
|
||||
_ = cx.observe(&new_content_query, |this, _, cx| {}).detach();
|
||||
|
||||
self.old_content_query = Some(old_content_query);
|
||||
self.new_content_query = Some(new_content_query);
|
||||
self.content_diff_query = Some(content_diff_query);
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for PullRequestDiffView {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
todo!()
|
||||
use gpui::{ParentElement, Styled, div};
|
||||
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.colors.surface)
|
||||
.p_4()
|
||||
.child(
|
||||
text(
|
||||
"Pull request diff rendering is still under construction. Launch the DiffOps playground with NOVEM_DIFFOPS_PLAYGROUND=1 cargo run.",
|
||||
)
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text_muted),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
721
src/screen/diffops_playground.rs
Normal file
721
src/screen/diffops_playground.rs
Normal file
@@ -0,0 +1,721 @@
|
||||
use bytes::Bytes;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, InteractiveElement, IntoElement, ParentElement,
|
||||
StatefulInteractiveElement, Styled, div, point, px, size,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app,
|
||||
component::{
|
||||
button::{self, button},
|
||||
text::text,
|
||||
},
|
||||
util::diff::{ContentDiff, DiffRow, DiffSide, Op, Span, diff_content},
|
||||
};
|
||||
|
||||
pub(crate) fn is_enabled() -> bool {
|
||||
match std::env::var("NOVEM_DIFFOPS_PLAYGROUND") {
|
||||
| Ok(value) => matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "on"),
|
||||
| Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_window(cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
let (top_left, window_bounds) = cx.read_global::<app::Global, _>(|global, cx| {
|
||||
(
|
||||
global.safe_area.origin,
|
||||
gpui::Bounds::centered(None, size(px(1440.), px(900.)), cx),
|
||||
)
|
||||
});
|
||||
|
||||
app::open_window(
|
||||
cx,
|
||||
gpui::WindowOptions {
|
||||
window_bounds: Some(gpui::WindowBounds::Windowed(window_bounds)),
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
appears_transparent: true,
|
||||
traffic_light_position: Some(top_left + point(px(12.), px(12.))),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|_window, cx| new(cx),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) struct Screen {
|
||||
cases: Vec<DiffCase>,
|
||||
selected_case: usize,
|
||||
}
|
||||
|
||||
struct DiffCase {
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
diff: ContentDiff,
|
||||
}
|
||||
|
||||
fn new(_cx: &mut gpui::Context<Screen>) -> Screen {
|
||||
Screen {
|
||||
cases: sample_cases(),
|
||||
selected_case: 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
fn selected_case(&self) -> &DiffCase {
|
||||
&self.cases[self.selected_case]
|
||||
}
|
||||
|
||||
fn select_case(&mut self, index: usize, cx: &mut gpui::Context<Self>) {
|
||||
if index < self.cases.len() {
|
||||
self.selected_case = index;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for Screen {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let theme = app::current_theme(cx);
|
||||
let weak = cx.entity();
|
||||
let case = self.selected_case();
|
||||
|
||||
let case_buttons: Vec<AnyElement> = self
|
||||
.cases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, case)| {
|
||||
let weak = weak.clone();
|
||||
button(("diffops-case", index))
|
||||
.label(case.title)
|
||||
.variant(if index == self.selected_case {
|
||||
button::Variant::Primary
|
||||
} else {
|
||||
button::Variant::Secondary
|
||||
})
|
||||
.w_full()
|
||||
.on_click(move |_, _, cx| {
|
||||
_ = weak.update(cx, |this, cx| {
|
||||
this.select_case(index, cx);
|
||||
});
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let op_cards: Vec<AnyElement> = case
|
||||
.diff
|
||||
.spans()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, span)| render_op_card(index, span, theme).into_any_element())
|
||||
.collect();
|
||||
|
||||
let op_groups: Vec<AnyElement> = case
|
||||
.diff
|
||||
.spans()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, span)| {
|
||||
render_op_group(
|
||||
index,
|
||||
span,
|
||||
case.diff.rows_for_span(index),
|
||||
&case.diff,
|
||||
theme,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect();
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.colors.surface_chrome)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
div()
|
||||
.w_64()
|
||||
.h_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.border_r_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.bg(theme.colors.surface)
|
||||
.child(
|
||||
div()
|
||||
.p_4()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(text("DiffOps Playground").text_lg())
|
||||
.child(
|
||||
text(
|
||||
"Sample content is diffed once at startup, then the UI renders the stored DiffOps and aligned rows.",
|
||||
)
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text_muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p_4()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.children(case_buttons),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("diffops-sidebar-scroll")
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
div()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(text("Precomputed DiffOps").text_sm())
|
||||
.children(op_cards),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.p_4()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.bg(theme.colors.surface)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(text(case.title).text_xl())
|
||||
.child(
|
||||
text(case.description)
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text_muted),
|
||||
)
|
||||
.child(
|
||||
text(format!(
|
||||
"{} ops, {} old lines, {} new lines",
|
||||
case.diff.spans().len(),
|
||||
case.diff.old_line_count(),
|
||||
case.diff.new_line_count(),
|
||||
))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
.text_color(theme.colors.text_subtle),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.gap_2()
|
||||
.bg(theme.colors.surface_elevated)
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.child(
|
||||
panel_header("Old", case.diff.old_line_count(), theme)
|
||||
.flex_1(),
|
||||
)
|
||||
.child(
|
||||
panel_header("New", case.diff.new_line_count(), theme)
|
||||
.flex_1(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("diffops-main-scroll")
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
div()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(text("Source Content").text_sm())
|
||||
.child(render_source_content(&case.diff, theme))
|
||||
.child(text("DiffOps Render").text_sm())
|
||||
.children(op_groups),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_cases() -> Vec<DiffCase> {
|
||||
vec![
|
||||
DiffCase::new(
|
||||
"Insert Block",
|
||||
"A pure insert leaves the old side with an empty anchor span such as 2..2 while the new side grows.",
|
||||
r#"fn config() {
|
||||
let host = "localhost";
|
||||
start(host);
|
||||
}
|
||||
"#,
|
||||
r#"fn config() {
|
||||
let host = "localhost";
|
||||
let port = 8080;
|
||||
start(host);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
DiffCase::new(
|
||||
"Delete Block",
|
||||
"A delete keeps the old side non-empty and gives the new side an empty anchor span at the removal point.",
|
||||
r#"fn handle(req: Request) {
|
||||
trace_request(&req);
|
||||
authorize(&req);
|
||||
dispatch(req);
|
||||
}
|
||||
"#,
|
||||
r#"fn handle(req: Request) {
|
||||
authorize(&req);
|
||||
dispatch(req);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
DiffCase::new(
|
||||
"Replace Span",
|
||||
"A replace can cover different line counts on each side. The viewer pairs rows by position inside the op span.",
|
||||
r#"fn render() {
|
||||
let theme = current_theme(cx);
|
||||
layout(theme);
|
||||
}
|
||||
"#,
|
||||
r#"fn render() {
|
||||
let palette = current_palette(cx);
|
||||
let spacing = spacing_scale();
|
||||
layout(palette, spacing);
|
||||
}
|
||||
"#,
|
||||
),
|
||||
DiffCase::new(
|
||||
"Mixed Hunk",
|
||||
"This sample produces several DiffOps in sequence so you can see equal, replace, insert, and delete spans together.",
|
||||
r#"use crate::auth::Token;
|
||||
use crate::http::Client;
|
||||
|
||||
fn fetch() {
|
||||
let timeout = 30;
|
||||
let retries = 1;
|
||||
request(timeout, retries);
|
||||
}
|
||||
"#,
|
||||
r#"use crate::auth::Session;
|
||||
use crate::http::Client;
|
||||
|
||||
fn fetch() {
|
||||
let timeout = 45;
|
||||
let retries = 3;
|
||||
let backoff = 250;
|
||||
request(timeout, retries);
|
||||
log_request();
|
||||
}
|
||||
"#,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
impl DiffCase {
|
||||
fn new(
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
old: &'static str,
|
||||
new: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
title,
|
||||
description,
|
||||
diff: diff_content(
|
||||
Bytes::from_static(old.as_bytes()),
|
||||
Bytes::from_static(new.as_bytes()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn panel_header(label: &'static str, line_count: usize, theme: &crate::theme::Theme) -> gpui::Div {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.bg(theme.colors.surface)
|
||||
.px_3()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(text(label).text_sm())
|
||||
.child(
|
||||
text(format!("{line_count} lines"))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
.text_color(theme.colors.text_subtle),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_source_content(diff: &ContentDiff, theme: &crate::theme::Theme) -> gpui::Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_2()
|
||||
.child(render_source_panel("Old Content", DiffSide::Old, diff, theme).flex_1())
|
||||
.child(render_source_panel("New Content", DiffSide::New, diff, theme).flex_1())
|
||||
}
|
||||
|
||||
fn render_source_panel(
|
||||
title: &'static str,
|
||||
side: DiffSide,
|
||||
diff: &ContentDiff,
|
||||
theme: &crate::theme::Theme,
|
||||
) -> gpui::Div {
|
||||
let line_count = match side {
|
||||
| DiffSide::Old => diff.old_line_count(),
|
||||
| DiffSide::New => diff.new_line_count(),
|
||||
};
|
||||
|
||||
let lines: Vec<AnyElement> = (0..line_count)
|
||||
.map(|line| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_start()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.child(
|
||||
div()
|
||||
.w(px(64.))
|
||||
.px_2()
|
||||
.py_2()
|
||||
.font_family("Menlo")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_subtle)
|
||||
.child(format!("{:>4}", line + 1)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.px_2()
|
||||
.py_2()
|
||||
.font_family("Menlo")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text)
|
||||
.child(display_bytes(diff.line_slice_at(side, line))),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect();
|
||||
|
||||
div()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.bg(theme.colors.surface)
|
||||
.child(
|
||||
div()
|
||||
.bg(theme.colors.surface_elevated)
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.px_3()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(text(title).text_sm())
|
||||
.child(
|
||||
text(format!("{line_count} lines"))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
.text_color(theme.colors.text_subtle),
|
||||
),
|
||||
)
|
||||
.child(div().flex().flex_col().children(lines))
|
||||
}
|
||||
|
||||
fn render_op_card(index: usize, span: &Span, theme: &crate::theme::Theme) -> gpui::Div {
|
||||
let colors = tag_colors(span.op, theme);
|
||||
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.p_3()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
text(format!("Op {index}: {}", tag_label(span.op)))
|
||||
.text_sm()
|
||||
.text_color(colors.foreground),
|
||||
)
|
||||
.child(
|
||||
text(format!(
|
||||
"old {:?} new {:?}",
|
||||
span.old_range, span.new_range
|
||||
))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
.text_color(theme.colors.text_muted),
|
||||
)
|
||||
.child(
|
||||
text(format!("{:?}", span.op))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
.text_color(theme.colors.text_subtle),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_op_group(
|
||||
index: usize,
|
||||
span: &Span,
|
||||
rows: Vec<DiffRow>,
|
||||
diff: &ContentDiff,
|
||||
theme: &crate::theme::Theme,
|
||||
) -> gpui::Div {
|
||||
let colors = tag_colors(span.op, theme);
|
||||
let row_elements: Vec<AnyElement> = rows
|
||||
.into_iter()
|
||||
.map(|row| render_row(row, diff, theme).into_any_element())
|
||||
.collect();
|
||||
|
||||
div()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.child(
|
||||
div()
|
||||
.bg(colors.background)
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.px_3()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
text(format!("Op {index}: {}", tag_label(span.op)))
|
||||
.text_sm()
|
||||
.text_color(colors.foreground),
|
||||
)
|
||||
.child(
|
||||
text(format!(
|
||||
"old {:?} new {:?}",
|
||||
span.old_range, span.new_range
|
||||
))
|
||||
.text_xs()
|
||||
.font_family("Menlo")
|
||||
.text_color(theme.colors.text_muted),
|
||||
),
|
||||
)
|
||||
.child(div().flex().flex_col().children(row_elements))
|
||||
}
|
||||
|
||||
fn render_row(row: DiffRow, diff: &ContentDiff, theme: &crate::theme::Theme) -> gpui::Div {
|
||||
let old_text = row
|
||||
.old_content_range
|
||||
.as_ref()
|
||||
.map(|range| display_bytes(diff.line_slice(DiffSide::Old, range)));
|
||||
let new_text = row
|
||||
.new_content_range
|
||||
.as_ref()
|
||||
.map(|range| display_bytes(diff.line_slice(DiffSide::New, range)));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.bg(theme.colors.surface)
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.child(render_line_cell(
|
||||
row.op_index,
|
||||
row.op,
|
||||
row.old_line,
|
||||
old_text,
|
||||
true,
|
||||
theme,
|
||||
))
|
||||
.child(render_line_cell(
|
||||
row.op_index,
|
||||
row.op,
|
||||
row.new_line,
|
||||
new_text,
|
||||
false,
|
||||
theme,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_line_cell(
|
||||
op_index: usize,
|
||||
op: Op,
|
||||
line_number: Option<usize>,
|
||||
content: Option<String>,
|
||||
is_old_side: bool,
|
||||
theme: &crate::theme::Theme,
|
||||
) -> gpui::Div {
|
||||
let colors = row_colors(op, is_old_side, content.is_some(), theme);
|
||||
let line_label = line_number
|
||||
.map(|line| format!("{:>4}", line + 1))
|
||||
.unwrap_or_else(|| " ".to_string());
|
||||
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_start()
|
||||
.border_r_1()
|
||||
.border_color(theme.colors.border_muted)
|
||||
.bg(colors.background)
|
||||
.child(
|
||||
div()
|
||||
.w(px(64.))
|
||||
.px_2()
|
||||
.py_2()
|
||||
.font_family("Menlo")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_subtle)
|
||||
.child(line_label),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.px_2()
|
||||
.py_2()
|
||||
.font_family("Menlo")
|
||||
.text_xs()
|
||||
.text_color(colors.foreground)
|
||||
.child(content.unwrap_or_else(|| format!("anchor for span {op_index}"))),
|
||||
)
|
||||
}
|
||||
|
||||
fn tag_label(op: Op) -> &'static str {
|
||||
match op {
|
||||
| Op::Equal => "Equal",
|
||||
| Op::Delete => "Delete",
|
||||
| Op::Insert => "Insert",
|
||||
| Op::Replace => "Replace",
|
||||
}
|
||||
}
|
||||
|
||||
fn display_bytes(bytes: &[u8]) -> String {
|
||||
let mut rendered = String::new();
|
||||
|
||||
for ch in String::from_utf8_lossy(bytes).chars() {
|
||||
match ch {
|
||||
| '\n' => rendered.push_str("\\n"),
|
||||
| '\r' => rendered.push_str("\\r"),
|
||||
| '\t' => rendered.push_str("\\t"),
|
||||
| _ => rendered.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if rendered.is_empty() {
|
||||
rendered.push(' ');
|
||||
}
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
struct Colors {
|
||||
background: gpui::Rgba,
|
||||
border: gpui::Rgba,
|
||||
foreground: gpui::Rgba,
|
||||
}
|
||||
|
||||
fn tag_colors(op: Op, theme: &crate::theme::Theme) -> Colors {
|
||||
match op {
|
||||
| Op::Equal => Colors {
|
||||
background: theme.colors.surface_elevated,
|
||||
border: theme.colors.border,
|
||||
foreground: theme.colors.text,
|
||||
},
|
||||
| Op::Delete => Colors {
|
||||
background: theme.colors.danger_muted,
|
||||
border: theme.colors.danger_border,
|
||||
foreground: theme.colors.danger_fg,
|
||||
},
|
||||
| Op::Insert => Colors {
|
||||
background: theme.colors.success_muted,
|
||||
border: theme.colors.success_border,
|
||||
foreground: theme.colors.success_fg,
|
||||
},
|
||||
| Op::Replace => Colors {
|
||||
background: theme.colors.warning_muted,
|
||||
border: theme.colors.warning_border,
|
||||
foreground: theme.colors.warning_fg,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn row_colors(op: Op, is_old_side: bool, has_content: bool, theme: &crate::theme::Theme) -> Colors {
|
||||
if !has_content {
|
||||
return Colors {
|
||||
background: theme.colors.surface_chrome,
|
||||
border: theme.colors.border_muted,
|
||||
foreground: theme.colors.text_subtle,
|
||||
};
|
||||
}
|
||||
|
||||
match op {
|
||||
| Op::Equal => Colors {
|
||||
background: theme.colors.surface,
|
||||
border: theme.colors.border_muted,
|
||||
foreground: theme.colors.text,
|
||||
},
|
||||
| Op::Delete => Colors {
|
||||
background: theme.colors.danger_muted,
|
||||
border: theme.colors.danger_border,
|
||||
foreground: theme.colors.danger_fg,
|
||||
},
|
||||
| Op::Insert => Colors {
|
||||
background: theme.colors.success_muted,
|
||||
border: theme.colors.success_border,
|
||||
foreground: theme.colors.success_fg,
|
||||
},
|
||||
| Op::Replace if is_old_side => Colors {
|
||||
background: theme.colors.danger_muted,
|
||||
border: theme.colors.danger_border,
|
||||
foreground: theme.colors.danger_fg,
|
||||
},
|
||||
| Op::Replace => Colors {
|
||||
background: theme.colors.success_muted,
|
||||
border: theme.colors.success_border,
|
||||
foreground: theme.colors.success_fg,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub(crate) mod dashboard;
|
||||
pub(crate) mod diffops_playground;
|
||||
pub(crate) mod setup_wizard;
|
||||
|
||||
184
src/util/diff.rs
Normal file
184
src/util/diff.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::ops::Range;
|
||||
|
||||
pub(crate) struct ContentDiff {
|
||||
pub(crate) old_content: bytes::Bytes,
|
||||
pub(crate) new_content: bytes::Bytes,
|
||||
pub(crate) spans: Vec<Span>,
|
||||
old_line_ranges: Vec<Range<usize>>,
|
||||
new_line_ranges: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
pub(crate) struct Span {
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_range: Range<usize>,
|
||||
pub(crate) new_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum Op {
|
||||
Equal,
|
||||
Delete,
|
||||
Insert,
|
||||
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>>,
|
||||
}
|
||||
|
||||
pub(crate) fn diff_content(old_content: bytes::Bytes, new_content: bytes::Bytes) -> ContentDiff {
|
||||
let old_line_ranges = line_ranges(&old_content);
|
||||
let new_line_ranges = line_ranges(&new_content);
|
||||
let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content);
|
||||
|
||||
let spans = diff
|
||||
.ops()
|
||||
.iter()
|
||||
.map(|op| match op {
|
||||
| &similar::DiffOp::Equal {
|
||||
old_index,
|
||||
new_index,
|
||||
len,
|
||||
} => Span {
|
||||
op: Op::Equal,
|
||||
old_range: old_index..(old_index + len),
|
||||
new_range: new_index..(new_index + len),
|
||||
},
|
||||
|
||||
| &similar::DiffOp::Delete {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
} => Span {
|
||||
op: Op::Delete,
|
||||
old_range: old_index..(old_index + old_len),
|
||||
new_range: new_index..new_index,
|
||||
},
|
||||
|
||||
| &similar::DiffOp::Insert {
|
||||
old_index,
|
||||
new_index,
|
||||
new_len,
|
||||
} => Span {
|
||||
op: Op::Insert,
|
||||
old_range: old_index..old_index,
|
||||
new_range: new_index..(new_index + new_len),
|
||||
},
|
||||
|
||||
| &similar::DiffOp::Replace {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
new_len,
|
||||
} => Span {
|
||||
op: Op::Replace,
|
||||
old_range: old_index..(old_index + old_len),
|
||||
new_range: new_index..(new_index + new_len),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
ContentDiff {
|
||||
old_content,
|
||||
new_content,
|
||||
spans,
|
||||
old_line_ranges,
|
||||
new_line_ranges,
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentDiff {
|
||||
pub(crate) fn spans(&self) -> &[Span] {
|
||||
&self.spans
|
||||
}
|
||||
|
||||
pub(crate) fn old_line_count(&self) -> usize {
|
||||
self.old_line_ranges.len()
|
||||
}
|
||||
|
||||
pub(crate) fn new_line_count(&self) -> usize {
|
||||
self.new_line_ranges.len()
|
||||
}
|
||||
|
||||
pub(crate) fn line_slice(&self, side: DiffSide, range: &Range<usize>) -> &[u8] {
|
||||
match side {
|
||||
| DiffSide::Old => &self.old_content[range.clone()],
|
||||
| DiffSide::New => &self.new_content[range.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn line_slice_at(&self, side: DiffSide, line: usize) -> &[u8] {
|
||||
match side {
|
||||
| DiffSide::Old => self.line_slice(DiffSide::Old, &self.old_line_ranges[line]),
|
||||
| DiffSide::New => self.line_slice(DiffSide::New, &self.new_line_ranges[line]),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rows_for_span(&self, span_index: usize) -> Vec<DiffRow> {
|
||||
let span = &self.spans[span_index];
|
||||
let old_len = span.old_range.end.saturating_sub(span.old_range.start);
|
||||
let new_len = span.new_range.end.saturating_sub(span.new_range.start);
|
||||
let row_count = old_len.max(new_len);
|
||||
|
||||
let mut rows = Vec::with_capacity(row_count);
|
||||
for offset in 0..row_count {
|
||||
let old_line = (offset < old_len).then_some(span.old_range.start + offset);
|
||||
let new_line = (offset < new_len).then_some(span.new_range.start + offset);
|
||||
|
||||
rows.push(DiffRow {
|
||||
op_index: span_index,
|
||||
op: span.op,
|
||||
old_line,
|
||||
old_content_range: old_line.map(|line| self.old_line_ranges[line].clone()),
|
||||
new_line,
|
||||
new_content_range: new_line.map(|line| self.new_line_ranges[line].clone()),
|
||||
});
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
}
|
||||
|
||||
fn line_ranges(content: &[u8]) -> Vec<Range<usize>> {
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut index = 0;
|
||||
|
||||
while index < content.len() {
|
||||
match content[index] {
|
||||
| b'\r' => {
|
||||
index += 1;
|
||||
if index < content.len() && content[index] == b'\n' {
|
||||
index += 1;
|
||||
}
|
||||
ranges.push(start..index);
|
||||
start = index;
|
||||
}
|
||||
| b'\n' => {
|
||||
index += 1;
|
||||
ranges.push(start..index);
|
||||
start = index;
|
||||
}
|
||||
| _ => {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start < content.len() {
|
||||
ranges.push(start..content.len());
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod timeout;
|
||||
|
||||
Reference in New Issue
Block a user