Add DiffOps playground
This commit is contained in:
@@ -6,7 +6,7 @@ use tokio::sync::OwnedRwLockReadGuard;
|
|||||||
use crate::{
|
use crate::{
|
||||||
api,
|
api,
|
||||||
query::{self, Query, fetch_query},
|
query::{self, Query, fetch_query},
|
||||||
util::file,
|
util::{self, file},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -79,8 +79,8 @@ impl query::QueryFn for FetchFileContent {
|
|||||||
|
|
||||||
fn key(&self) -> query::Key {
|
fn key(&self) -> query::Key {
|
||||||
match &self.reff {
|
match &self.reff {
|
||||||
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
|
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
|
||||||
| None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).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 {
|
let path = match &self.reff {
|
||||||
| Some(reff) => format!(
|
| Some(reff) => format!(
|
||||||
"/repos/{}/contents/{}?ref={}",
|
"/repos/{}/contents/{}?ref={}",
|
||||||
self.repo_slug, self.path, reff
|
self.repo_slug, self.path, reff
|
||||||
),
|
),
|
||||||
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
|
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = c
|
let res = c
|
||||||
@@ -113,13 +113,13 @@ impl query::QueryFn for FetchFileContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct QueryFileDiff {
|
pub struct FetchFileDiff {
|
||||||
pub base: FileRef,
|
pub base: FileRef,
|
||||||
pub head: FileRef,
|
pub head: FileRef,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl query::QueryFn for QueryFileDiff {
|
impl query::QueryFn for FetchFileDiff {
|
||||||
type Data = Option<()>;
|
type Data = util::diff::ContentDiff;
|
||||||
type Error = api::Error;
|
type Error = api::Error;
|
||||||
type Context = api::QueryContext;
|
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 run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||||
async fn fetch_content(
|
async fn fetch_content(
|
||||||
r: &FileRef,
|
r: &FileRef,
|
||||||
c: &<QueryFileDiff as query::QueryFn>::Context,
|
c: &<FetchFileDiff as query::QueryFn>::Context,
|
||||||
) -> Result<Option<bytes::Bytes>, api::Error> {
|
) -> Result<Option<bytes::Bytes>, api::Error> {
|
||||||
let path = match &r.reff {
|
let path = match &r.reff {
|
||||||
| Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff),
|
| Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff),
|
||||||
| None => format!("/repos/{}/contents/{}", r.repo_slug, r.path),
|
| None => format!("/repos/{}/contents/{}", r.repo_slug, r.path),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = c
|
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),);
|
let (old, new) = tokio::join!(fetch_content(&self.base, c), fetch_content(&self.head, c),);
|
||||||
|
|
||||||
match (old, new) {
|
match (old, new) {
|
||||||
| (Ok(Some(ref old)), Ok(Some(ref new))) => {
|
| (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)),
|
||||||
let diff = similar::TextDiff::from_lines::<[u8]>(old, new);
|
| _ => Err(api::Error::MalformedResponse(
|
||||||
for change in diff.iter_all_changes() {}
|
|
||||||
}
|
|
||||||
| _ => {
|
|
||||||
return Err(api::Error::MalformedResponse(
|
|
||||||
"failed to fetch content".to_string(),
|
"failed to fetch content".to_string(),
|
||||||
));
|
)),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use gpui::{bounds, point, prelude::*, px, size};
|
use gpui::{bounds, point, prelude::*, px, size};
|
||||||
|
|
||||||
use crate::screen::{dashboard, setup_wizard};
|
use crate::screen::{dashboard, diffops_playground, setup_wizard};
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod app;
|
mod app;
|
||||||
@@ -62,6 +62,11 @@ fn setup_application(cx: &mut gpui::App) {
|
|||||||
cx.set_global(global);
|
cx.set_global(global);
|
||||||
cx.set_global(query_store);
|
cx.set_global(query_store);
|
||||||
|
|
||||||
|
if diffops_playground::is_enabled() {
|
||||||
|
_ = diffops_playground::open_window(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: handle failure
|
// TODO: handle failure
|
||||||
_ = storage::ensure_data_dir();
|
_ = storage::ensure_data_dir();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
api,
|
api, app,
|
||||||
|
component::text::text,
|
||||||
query::{self, QueryStatus, read_query, use_query},
|
query::{self, QueryStatus, read_query, use_query},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -7,16 +8,14 @@ pub(crate) struct PullRequestDiffView {
|
|||||||
selected_file_path: Option<String>,
|
selected_file_path: Option<String>,
|
||||||
|
|
||||||
pr_query: query::Entity<api::issues::FetchPullRequest>,
|
pr_query: query::Entity<api::issues::FetchPullRequest>,
|
||||||
old_content_query: Option<query::Entity<api::repo::FetchFileContent>>,
|
content_diff_query: Option<query::Entity<api::repo::FetchFileDiff>>,
|
||||||
new_content_query: Option<query::Entity<api::repo::FetchFileContent>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<PullRequestDiffView>) -> PullRequestDiffView {
|
fn new(pr_id: api::issues::Id, cx: &mut gpui::Context<PullRequestDiffView>) -> PullRequestDiffView {
|
||||||
let mut view = PullRequestDiffView {
|
let mut view = PullRequestDiffView {
|
||||||
selected_file_path: None,
|
selected_file_path: None,
|
||||||
pr_query: use_query(api::issues::FetchPullRequest { id: pr_id }, cx),
|
pr_query: use_query(api::issues::FetchPullRequest { id: pr_id }, cx),
|
||||||
old_content_query: None,
|
content_diff_query: None,
|
||||||
new_content_query: None,
|
|
||||||
};
|
};
|
||||||
view.on_create(cx);
|
view.on_create(cx);
|
||||||
view
|
view
|
||||||
@@ -35,15 +34,15 @@ impl PullRequestDiffView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_content_queries(&mut self, cx: &mut gpui::Context<Self>) {
|
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) {
|
if let QueryStatus::Loaded(pr) = read_query(&self.pr_query, cx) {
|
||||||
Some((
|
Some((
|
||||||
api::repo::FetchFileContent {
|
api::repo::FileRef {
|
||||||
repo_slug: pr.base_repo_slug.clone(),
|
repo_slug: pr.base_repo_slug.clone(),
|
||||||
path: pr.base_branch_name.clone(),
|
path: pr.base_branch_name.clone(),
|
||||||
reff: Some(pr.base_ref.clone()),
|
reff: Some(pr.base_ref.clone()),
|
||||||
},
|
},
|
||||||
api::repo::FetchFileContent {
|
api::repo::FileRef {
|
||||||
repo_slug: pr.head_repo_slug.clone(),
|
repo_slug: pr.head_repo_slug.clone(),
|
||||||
path: pr.head_branch_name.clone(),
|
path: pr.head_branch_name.clone(),
|
||||||
reff: Some(pr.head_ref.clone()),
|
reff: Some(pr.head_ref.clone()),
|
||||||
@@ -56,24 +55,38 @@ impl PullRequestDiffView {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_content_query = use_query(old_content_query, cx);
|
let content_diff_query = use_query(
|
||||||
let new_content_query = use_query(new_content_query, cx);
|
api::repo::FetchFileDiff {
|
||||||
|
base: old_file_ref,
|
||||||
|
head: new_file_ref,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
_ = cx.observe(&old_content_query, |this, _, cx| {}).detach();
|
self.content_diff_query = Some(content_diff_query);
|
||||||
|
|
||||||
_ = cx.observe(&new_content_query, |this, _, cx| {}).detach();
|
|
||||||
|
|
||||||
self.old_content_query = Some(old_content_query);
|
|
||||||
self.new_content_query = Some(new_content_query);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl gpui::Render for PullRequestDiffView {
|
impl gpui::Render for PullRequestDiffView {
|
||||||
fn render(
|
fn render(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut gpui::Window,
|
_window: &mut gpui::Window,
|
||||||
cx: &mut gpui::Context<Self>,
|
cx: &mut gpui::Context<Self>,
|
||||||
) -> impl gpui::IntoElement {
|
) -> 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 dashboard;
|
||||||
|
pub(crate) mod diffops_playground;
|
||||||
pub(crate) mod setup_wizard;
|
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 file;
|
||||||
pub(crate) mod timeout;
|
pub(crate) mod timeout;
|
||||||
|
|||||||
Reference in New Issue
Block a user