Something went wrong. Try again.
fork of gpui
Something went wrong. Try again.
154 kB · 4362 lines
Rust
at master
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};use imara_diff::{Algorithm, Diff, InternedInput, sources::lines};use language::{ Capability, DiffOptions, Language, LanguageName, LanguageRegistry, language_settings::LanguageSettings, word_diff_ranges,};use rope::Rope;use std::{ cmp::Ordering, iter, ops::{Range, RangeInclusive}, sync::Arc,};use sum_tree::SumTree;use text::{ Anchor, Bias, BufferId, Edit, OffsetRangeExt, Patch, Point, ToOffset as _, ToPoint as _,};use util::{ResultExt, debug_panic};
pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
pub struct BufferDiff { pub buffer_id: BufferId, base_text_buffer: Entity<language::Buffer>, diff_snapshot: Option<BufferDiffSnapshot>, secondary_diff: Option<Entity<BufferDiff>>, buffer_snapshot: text::BufferSnapshot, base_kind: DiffBaseKind,}
/// Where this diff's base text came from. Only diffs whose base is HEAD/// support staging and restoring hunks; a diff against any other base (e.g./// the merge base with another branch) would rewrite committed work.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum DiffBaseKind { /// The buffer's committed (HEAD) content. Head, /// The buffer's index (staged) content. Index, /// An arbitrary blob, such as the merge base with another branch. Oid, /// Arbitrary caller-provided text, such as an agent's original text, /// the clipboard, or another file. Custom,}
#[derive(Clone)]pub struct BufferDiffSnapshot { hunks: SumTree<InternalDiffHunk>, pending_hunks: SumTree<PendingHunk>, base_text: language::BufferSnapshot, base_text_exists: bool, buffer_snapshot: text::BufferSnapshot, secondary_diff: Option<Arc<BufferDiffSnapshot>>,}
impl std::fmt::Debug for BufferDiffSnapshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("hunks", &self.hunks) .field("remote_id", &self.base_text.remote_id()) .field("secondary_diff", &self.secondary_diff) .finish() }}
#[derive(Clone)]pub struct BufferDiffUpdate { hunks: SumTree<InternalDiffHunk>, base_text: language::BufferSnapshot, base_text_exists: bool, buffer_snapshot: text::BufferSnapshot,}
impl BufferDiffUpdate { pub fn set_base_text_snapshot( &mut self, base_text: language::BufferSnapshot, base_text_exists: bool, ) { self.base_text = base_text; self.base_text_exists = base_text_exists; }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub struct DiffHunkStatus { pub kind: DiffHunkStatusKind, pub secondary: DiffHunkSecondaryStatus,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum DiffHunkStatusKind { Added, Modified, Deleted,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]/// Diff of Working Copy vs Index/// aka 'is this hunk staged or not'pub enum DiffHunkSecondaryStatus { /// Unstaged HasSecondaryHunk, /// Partially staged OverlapsWithSecondaryHunk, /// Staged NoSecondaryHunk, /// We are unstaging SecondaryHunkAdditionPending, /// We are stagind SecondaryHunkRemovalPending,}
/// A diff hunk resolved to rows in the buffer.#[derive(Debug, Clone, PartialEq, Eq)]pub struct DiffHunk { /// The buffer range as points. pub range: Range<Point>, /// The range in the buffer to which this hunk corresponds. pub buffer_range: Range<Anchor>, /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range<usize>, pub secondary_status: DiffHunkSecondaryStatus, // Anchors representing the word diff locations in the active buffer pub buffer_word_diffs: Vec<Range<Anchor>>, // Offsets relative to the start of the deleted diff that represent word diff locations pub base_word_diffs: Vec<Range<usize>>,}
/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.#[derive(Debug, Clone, PartialEq, Eq)]struct InternalDiffHunk { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, diff_base_point_range: Range<Point>, base_word_diffs: Vec<Range<usize>>, buffer_word_diffs: Vec<Range<Anchor>>,}
#[derive(Debug, Clone, PartialEq, Eq)]pub struct PendingHunk { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, buffer_version: clock::Global, sense: PendingSense,}
impl PendingHunk { pub fn new( buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, buffer_version: clock::Global, sense: PendingSense, ) -> Self { Self { buffer_range, diff_base_byte_range, buffer_version, sense, } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum PendingSense { /// Override the secondary status of the matched hunk (used by the /// uncommitted diff to show a hunk as staging/unstaging in place). SetSecondaryStatus { stage: bool }, /// Suppress the matched hunk entirely (used by the unstaged/staged diffs so /// that a hunk disappears the moment it is staged/unstaged). Suppress,}
#[derive(Debug, Clone)]pub struct DiffHunkSummary { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, added_rows: u32, removed_rows: u32,}
impl sum_tree::Item for InternalDiffHunk { type Summary = DiffHunkSummary;
fn summary(&self, buffer: &text::BufferSnapshot) -> Self::Summary { let buffer_start = self.buffer_range.start.to_point(buffer); let buffer_end = self.buffer_range.end.to_point(buffer); DiffHunkSummary { buffer_range: self.buffer_range.clone(), diff_base_byte_range: self.diff_base_byte_range.clone(), added_rows: buffer_end.row.saturating_sub(buffer_start.row), removed_rows: self .diff_base_point_range .end .row .saturating_sub(self.diff_base_point_range.start.row), } }}
impl sum_tree::Item for PendingHunk { type Summary = DiffHunkSummary;
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), diff_base_byte_range: self.diff_base_byte_range.clone(), added_rows: 0, removed_rows: 0, } }}
impl sum_tree::Summary for DiffHunkSummary { type Context<'a> = &'a text::BufferSnapshot;
fn zero(buffer: &text::BufferSnapshot) -> Self { DiffHunkSummary { buffer_range: Anchor::min_min_range_for_buffer(buffer.remote_id()), diff_base_byte_range: 0..0, added_rows: 0, removed_rows: 0, } }
fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) { self.buffer_range.start = *self .buffer_range .start .min(&other.buffer_range.start, buffer); self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
self.diff_base_byte_range.start = self .diff_base_byte_range .start .min(other.diff_base_byte_range.start); self.diff_base_byte_range.end = self .diff_base_byte_range .end .max(other.diff_base_byte_range.end);
self.added_rows += other.added_rows; self.removed_rows += other.removed_rows; }}
impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor { fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering { if self .cmp(&cursor_location.buffer_range.start, buffer) .is_lt() { Ordering::Less } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() { Ordering::Greater } else { Ordering::Equal } }}
impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for usize { fn cmp(&self, cursor_location: &DiffHunkSummary, _cx: &text::BufferSnapshot) -> Ordering { if *self < cursor_location.diff_base_byte_range.start { Ordering::Less } else if *self > cursor_location.diff_base_byte_range.end { Ordering::Greater } else { Ordering::Equal } }}
impl BufferDiffSnapshot { #[cfg(test)] fn new_sync( buffer: &text::BufferSnapshot, diff_base: String, cx: &mut gpui::TestAppContext, ) -> BufferDiffSnapshot { let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, buffer, cx)); buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx)) }
pub fn buffer_id(&self) -> BufferId { self.buffer_snapshot.remote_id() }
pub fn buffer_snapshot(&self) -> &text::BufferSnapshot { &self.buffer_snapshot }
pub fn is_empty(&self) -> bool { self.hunks.is_empty() }
pub fn changed_row_counts(&self) -> (u32, u32) { let summary = self.hunks.summary(); (summary.added_rows, summary.removed_rows) }
pub fn base_text_string(&self) -> Option<String> { self.base_text_exists.then(|| self.base_text.text()) }
pub fn base_text_exists(&self) -> bool { self.base_text_exists }
pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> { self.secondary_diff.as_deref() }
pub fn buffer_version(&self) -> &clock::Global { self.buffer_snapshot.version() }
fn original_buffer_snapshot(&self) -> &text::BufferSnapshot { &self.buffer_snapshot }
#[ztracing::instrument(skip_all)] pub fn hunks_intersecting_range<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let unstaged_counterpart = self.secondary_diff.as_deref(); let range = range.to_offset(buffer); let filter = move |summary: &DiffHunkSummary| { let summary_range = summary.buffer_range.to_offset(buffer); let before_start = summary_range.end < range.start; let after_end = summary_range.start > range.end; !before_start && !after_end }; self.hunks_intersecting_range_impl(filter, buffer, unstaged_counterpart) }
/// Like [`hunks_intersecting_range`], but ignores optimistic pending hunks /// (both secondary-status overrides and suppressions) and does not compute a /// secondary status. pub fn raw_hunks_intersecting_range<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let range = range.to_offset(buffer); let filter = move |summary: &DiffHunkSummary| { let summary_range = summary.buffer_range.to_offset(buffer); !(summary_range.end < range.start) && !(summary_range.start > range.end) }; self.hunks .filter::<_, DiffHunkSummary>(buffer, filter) .map(move |hunk| { let buffer_range = hunk.buffer_range.clone(); DiffHunk { range: buffer_range.to_point(buffer), diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range, secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk, base_word_diffs: hunk.base_word_diffs.clone(), buffer_word_diffs: hunk.buffer_word_diffs.clone(), } }) }
/// Maps a range in this diff's main buffer to the range it covers in the /// base text, expanding to whole hunks wherever the range endpoints fall /// inside or touch a hunk (`edit_for_old_position` is inclusive on both /// boundaries, matching `raw_hunks_intersecting_range`). Used by the /// index-write path to compute the index-coordinate footprint of a staging /// operation; like the raw hunks, the mapping ignores optimistic pending /// hunks. pub fn base_text_range_for_buffer_range( &self, range: Range<Anchor>, buffer: &text::BufferSnapshot, ) -> Range<usize> { let point_range = range.to_point(buffer); let patch = self.patch_for_buffer_range(point_range.start..=point_range.end, buffer); let start_point = patch.edit_for_old_position(point_range.start).new.start; let end_point = patch.edit_for_old_position(point_range.end).new.end; let base_text = self.base_text(); let start = base_text.point_to_offset(start_point.min(base_text.max_point())); let end = base_text.point_to_offset(end_point.min(base_text.max_point())); start.min(end)..end }
pub fn hunks_intersecting_range_rev<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let filter = move |summary: &DiffHunkSummary| { let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); !before_start && !after_end }; self.hunks_intersecting_range_rev_impl(filter, buffer) }
pub fn hunks_intersecting_base_text_range<'a>( &'a self, range: Range<usize>, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let unstaged_counterpart = self.secondary_diff.as_deref(); let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end < range.start; let after_end = summary.diff_base_byte_range.start > range.end; !before_start && !after_end }; self.hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart) }
pub fn hunks_intersecting_base_text_range_rev<'a>( &'a self, range: Range<usize>, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt(); let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt(); !before_start && !after_end }; self.hunks_intersecting_range_rev_impl(filter, main_buffer) }
pub fn hunks<'a>( &'a self, buffer_snapshot: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { self.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()), buffer_snapshot, ) }
pub fn hunks_in_row_range<'a>( &'a self, range: Range<u32>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let start = buffer.anchor_before(Point::new(range.start, 0)); let end = buffer.anchor_after(Point::new(range.end, 0)); self.hunks_intersecting_range(start..end, buffer) }
pub fn range_to_hunk_range( &self, range: Range<Anchor>, buffer: &text::BufferSnapshot, ) -> (Option<Range<Anchor>>, Option<Range<usize>>) { let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next(); let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next(); let range = first_hunk .as_ref() .zip(last_hunk.as_ref()) .map(|(first, last)| first.buffer_range.start..last.buffer_range.end); let base_text_range = first_hunk .zip(last_hunk) .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end); (range, base_text_range) }
pub fn base_text(&self) -> &language::BufferSnapshot { &self.base_text }
/// If this function returns `true`, the base texts are equal. If this /// function returns `false`, they might be equal, but might not. This /// result is used to avoid recalculating diffs in situations where we know /// nothing has changed. pub fn base_texts_definitely_eq(&self, other: &Self) -> bool { if self.base_text_exists != other.base_text_exists { return false; } let left = &self.base_text; let right = &other.base_text; let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty()); let (new_id, new_version, new_empty) = (right.remote_id(), right.version(), right.is_empty()); (new_id == old_id && new_version == old_version) || (new_empty && old_empty) }
/// Returns the last hunk whose start is less than or equal to the given position. fn hunk_before_base_text_offset<'a>( &self, target: usize, cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>, ) -> Option<&'a InternalDiffHunk> { cursor.seek_forward(&target, Bias::Left); if cursor .item() .is_none_or(|hunk| target < hunk.diff_base_byte_range.start) { cursor.prev(); } cursor .item() .filter(|hunk| target >= hunk.diff_base_byte_range.start) }
fn hunk_before_buffer_anchor<'a>( &self, target: Anchor, cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>, buffer: &text::BufferSnapshot, ) -> Option<&'a InternalDiffHunk> { cursor.seek_forward(&target, Bias::Left); if cursor .item() .is_none_or(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_lt()) { cursor.prev(); } cursor .item() .filter(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_ge()) }
/// Returns a patch mapping the provided main buffer snapshot to the base text of this diff. /// /// The returned patch is guaranteed to be accurate for all main buffer points in the provided range, /// but not necessarily for points outside that range. pub fn patch_for_buffer_range<'a>( &'a self, range: RangeInclusive<Point>, buffer: &'a text::BufferSnapshot, ) -> Patch<Point> { if !self.base_text_exists { return Patch::new(vec![Edit { old: Point::zero()..buffer.max_point(), new: Point::zero()..Point::zero(), }]); }
let mut edits_since_diff = Patch::new( buffer .edits_since::<Point>(&self.buffer_snapshot.version) .collect::<Vec<_>>(), ); edits_since_diff.invert();
let mut start_point = edits_since_diff.old_to_new(*range.start()); if let Some(first_edit) = edits_since_diff.edits().first() { start_point = start_point.min(first_edit.new.start); }
let original_snapshot = self.original_buffer_snapshot(); let base_text = self.base_text();
let mut cursor = self.hunks.cursor(original_snapshot); self.hunk_before_buffer_anchor( original_snapshot.anchor_before(start_point), &mut cursor, original_snapshot, ); if cursor.item().is_none() { cursor.next(); }
let mut prefix_edit = cursor.prev_item().map(|prev_hunk| Edit { old: Point::zero()..prev_hunk.buffer_range.end.to_point(original_snapshot), new: Point::zero()..prev_hunk.diff_base_byte_range.end.to_point(base_text), });
let mut range_end = edits_since_diff.old_to_new(*range.end()); if let Some(last_edit) = edits_since_diff.edits().last() { range_end = range_end.max(last_edit.new.end); } let range_end = original_snapshot.anchor_before(range_end);
let hunk_iter = std::iter::from_fn(move || { if let Some(edit) = prefix_edit.take() { return Some(edit); } let hunk = cursor.item()?; if hunk .buffer_range .start .cmp(&range_end, original_snapshot) .is_gt() { return None; } let edit = Edit { old: hunk.buffer_range.to_point(original_snapshot), new: hunk.diff_base_byte_range.to_point(base_text), }; cursor.next(); Some(edit) });
edits_since_diff.compose(hunk_iter) }
#[cfg(test)] pub(crate) fn patch_for_buffer_range_naive<'a>( &'a self, buffer: &'a text::BufferSnapshot, ) -> Patch<Point> { let original_snapshot = self.original_buffer_snapshot();
let edits_since: Vec<Edit<Point>> = buffer .edits_since::<Point>(original_snapshot.version()) .collect(); let mut inverted_edits_since = Patch::new(edits_since); inverted_edits_since.invert();
inverted_edits_since.compose( self.hunks .iter() .map(|hunk| { let old_start = hunk.buffer_range.start.to_point(original_snapshot); let old_end = hunk.buffer_range.end.to_point(original_snapshot); let new_start = self .base_text() .offset_to_point(hunk.diff_base_byte_range.start); let new_end = self .base_text() .offset_to_point(hunk.diff_base_byte_range.end); Edit { old: old_start..old_end, new: new_start..new_end, } }) .chain(if !self.base_text_exists && self.hunks.is_empty() { Some(Edit { old: Point::zero()..original_snapshot.max_point(), new: Point::zero()..Point::zero(), }) } else { None }), ) }
/// Returns a patch mapping the base text of this diff to the provided main buffer snapshot. /// /// The returned patch is guaranteed to be accurate for all base text points in the provided range, /// but not necessarily for points outside that range. pub fn patch_for_base_text_range<'a>( &'a self, range: RangeInclusive<Point>, buffer: &'a text::BufferSnapshot, ) -> Patch<Point> { if !self.base_text_exists { return Patch::new(vec![Edit { old: Point::zero()..Point::zero(), new: Point::zero()..buffer.max_point(), }]); }
let edits_since_diff = buffer .edits_since::<Point>(&self.buffer_snapshot.version) .collect::<Vec<_>>();
let mut hunk_patch = Vec::new(); let mut cursor = self.hunks.cursor(self.original_buffer_snapshot()); let hunk_before = self .hunk_before_base_text_offset(range.start().to_offset(self.base_text()), &mut cursor);
if let Some(hunk) = hunk_before && let Some(first_edit) = edits_since_diff.first() && hunk .buffer_range .start .to_point(self.original_buffer_snapshot()) > first_edit.old.start { cursor.reset(); self.hunk_before_buffer_anchor( self.original_buffer_snapshot() .anchor_before(first_edit.old.start), &mut cursor, self.original_buffer_snapshot(), ); } if cursor.item().is_none() { cursor.next(); } if let Some(prev_hunk) = cursor.prev_item() { hunk_patch.push(Edit { old: Point::zero() ..prev_hunk .diff_base_byte_range .end .to_point(self.base_text()), new: Point::zero() ..prev_hunk .buffer_range .end .to_point(self.original_buffer_snapshot()), }) } let range_end = range.end().to_offset(self.base_text()); while let Some(hunk) = cursor.item() && (hunk.diff_base_byte_range.start <= range_end || edits_since_diff.last().is_some_and(|last_edit| { hunk.buffer_range .start .to_point(self.original_buffer_snapshot()) <= last_edit.old.end })) { hunk_patch.push(Edit { old: hunk.diff_base_byte_range.to_point(self.base_text()), new: hunk.buffer_range.to_point(self.original_buffer_snapshot()), }); cursor.next(); }
Patch::new(hunk_patch).compose(edits_since_diff) }
#[cfg(test)] pub(crate) fn patch_for_base_text_range_naive<'a>( &'a self, buffer: &'a text::BufferSnapshot, ) -> Patch<Point> { let original_snapshot = self.original_buffer_snapshot();
let mut hunk_edits: Vec<Edit<Point>> = Vec::new(); for hunk in self.hunks.iter() { let old_start = self .base_text() .offset_to_point(hunk.diff_base_byte_range.start); let old_end = self .base_text() .offset_to_point(hunk.diff_base_byte_range.end); let new_start = hunk.buffer_range.start.to_point(original_snapshot); let new_end = hunk.buffer_range.end.to_point(original_snapshot); hunk_edits.push(Edit { old: old_start..old_end, new: new_start..new_end, }); } if !self.base_text_exists && hunk_edits.is_empty() { hunk_edits.push(Edit { old: Point::zero()..Point::zero(), new: Point::zero()..original_snapshot.max_point(), }) } let hunk_patch = Patch::new(hunk_edits);
hunk_patch.compose(buffer.edits_since::<Point>(original_snapshot.version())) }
pub fn buffer_point_to_base_text_range( &self, point: Point, buffer: &text::BufferSnapshot, ) -> Range<Point> { let patch = self.patch_for_buffer_range(point..=point, buffer); let edit = patch.edit_for_old_position(point); edit.new }
pub fn base_text_point_to_buffer_range( &self, point: Point, buffer: &text::BufferSnapshot, ) -> Range<Point> { let patch = self.patch_for_base_text_range(point..=point, buffer); let edit = patch.edit_for_old_position(point); edit.new }
pub fn buffer_point_to_base_text_point( &self, point: Point, buffer: &text::BufferSnapshot, ) -> Point { let patch = self.patch_for_buffer_range(point..=point, buffer); let edit = patch.edit_for_old_position(point); if point == edit.old.end { edit.new.end } else { edit.new.start } }
pub fn base_text_point_to_buffer_point( &self, point: Point, buffer: &text::BufferSnapshot, ) -> Point { let patch = self.patch_for_base_text_range(point..=point, buffer); let edit = patch.edit_for_old_position(point); if point == edit.old.end { edit.new.end } else { edit.new.start } }}
impl BufferDiffSnapshot { // Compute the edits to apply to the index, and the resulting pending hunks, // for a stage or unstage operation on the uncommitted diff. pub fn compute_uncommitted_index_edits( &self, unstaged_diff: &Self, stage: bool, hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, ) -> (Option<Vec<(Range<usize>, Arc<str>)>>, Vec<PendingHunk>) { let head_text = self .base_text_exists .then(|| self.base_text.as_rope().clone()); let index_text = unstaged_diff .base_text_exists .then(|| unstaged_diff.base_text.as_rope().clone()); let sense = PendingSense::SetSecondaryStatus { stage }; let version = buffer.version().clone();
// If the file doesn't exist in either HEAD or the index, then the // entire file must be either created or deleted in the index. let (index_text, head_text) = match (index_text, head_text) { (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text), (index_text, head_text) => { let index_len = index_text.as_ref().map_or(0, |rope| rope.len()); let new_index_text: Option<Rope> = if stage { log::debug!("stage all"); file_exists.then(|| buffer.as_rope().clone()) } else { log::debug!("unstage all"); head_text };
let pending = vec![PendingHunk::new( Anchor::min_max_range_for_buffer(buffer.remote_id()), 0..index_len, version, sense, )]; let edits = new_index_text.map(|rope| vec![(0..index_len, Arc::from(rope.to_string()))]); return (edits, pending); } };
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer); unstaged_hunk_cursor.next();
let mut prev_unstaged_hunk_buffer_end = 0; let mut prev_unstaged_hunk_base_text_end = 0; let mut edits = Vec::<(Range<usize>, Arc<str>)>::new(); let mut pending = Vec::<PendingHunk>::new();
// Process only the hunks the user acted on, skipping any already in the // desired state. let mut hunks_iter = hunks .iter() .filter(|hunk| { !((stage && hunk.secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk) || (!stage && hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)) }) .peekable();
while let Some(hunk) = hunks_iter.next() { let buffer_range = hunk.buffer_range.clone(); let diff_base_byte_range = hunk.diff_base_byte_range.clone(); pending.push(PendingHunk::new( buffer_range.clone(), diff_base_byte_range.clone(), version.clone(), sense, ));
// Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
if let Some(unstaged_hunk) = skipped_unstaged.last() { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer); }
// Find where this hunk is in the index if it doesn't overlap let mut buffer_offset_range = buffer_range.to_offset(buffer); let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end; let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
loop { // Merge this hunk with any overlapping unstaged hunks. if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() { let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer); if unstaged_hunk_offset_range.start <= buffer_offset_range.end { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start); buffer_offset_range.start = buffer_offset_range .start .min(unstaged_hunk_offset_range.start); buffer_offset_range.end = buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
unstaged_hunk_cursor.next(); continue; } }
// If any unstaged hunks were merged, then subsequent acted-on hunks // may now overlap this hunk. Merge them. if let Some(next_hunk) = hunks_iter.peek() { let next_hunk_offset_range = next_hunk.buffer_range.to_offset(buffer); if next_hunk_offset_range.start <= buffer_offset_range.end { buffer_offset_range.end = buffer_offset_range.end.max(next_hunk_offset_range.end); let merged_hunk = hunks_iter.next().expect("peeked hunk exists"); pending.push(PendingHunk::new( merged_hunk.buffer_range.clone(), merged_hunk.diff_base_byte_range.clone(), version.clone(), sense, )); continue; } }
break; }
let end_overshoot = buffer_offset_range .end .saturating_sub(prev_unstaged_hunk_buffer_end); let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
// Clamp to the index text bounds. The overshoot mapping assumes that // text between unstaged hunks is identical in the buffer and index. // When the buffer has been edited since the diff was computed, anchor // positions shift while diff_base_byte_range values don't, which can // cause index_end to exceed index_text.len(). // See `test_stage_all_with_stale_buffer` which would hit an assert // without these min calls let index_end = index_end.min(index_text.len()); let index_start = index_start.min(index_end); let index_byte_range = index_start..index_end;
let replacement_text: Arc<str> = if stage { log::debug!("staging hunk {:?}", buffer_offset_range); Arc::from( buffer .text_for_range(buffer_offset_range) .collect::<String>(), ) } else { log::debug!("unstaging hunk {:?}", buffer_offset_range); Arc::from( head_text .chunks_in_range(diff_base_byte_range.clone()) .collect::<String>(), ) };
// Distinct worktree hunks can project to touching index ranges // (e.g. a staged deletion ending exactly where the next hunk's // index position starts). Merge them so the edit list stays // strictly disjoint, which the pending-edit eviction logic relies // on to not evict one of these edits when the other is inserted. if let Some((last_range, last_text)) = edits.last_mut() && index_byte_range.start <= last_range.end { debug_assert!(index_byte_range.start == last_range.end); debug_assert!(index_byte_range.end >= last_range.end); last_range.end = index_byte_range.end; let mut merged_text = String::with_capacity(last_text.len() + replacement_text.len()); merged_text.push_str(last_text); merged_text.push_str(&replacement_text); *last_text = Arc::from(merged_text); } else { edits.push((index_byte_range, replacement_text)); } }
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted { for window in edits.windows(2) { let (range_a, range_b) = (&window[0].0, &window[1].0); debug_assert!( range_a.end < range_b.start, "index edits out of order or overlapping: {:?}", edits .iter() .map(|(range, text)| (range.clone(), text.len())) .collect::<Vec<_>>() ); } }
(Some(edits), pending) }}
impl BufferDiffSnapshot { fn hunks_intersecting_range_impl<'a>( &'a self, filter: impl 'a + Fn(&DiffHunkSummary) -> bool, buffer: &'a text::BufferSnapshot, secondary: Option<&'a Self>, ) -> impl 'a + Iterator<Item = DiffHunk> { let anchor_iter = self .hunks .filter::<_, DiffHunkSummary>(buffer, filter) .flat_map(move |hunk| { [ ( hunk.buffer_range.start, ( hunk.buffer_range.start, hunk.diff_base_byte_range.start, hunk, ), ), ( hunk.buffer_range.end, (hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk), ), ] });
let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer); pending_hunks_cursor.next();
let mut secondary_cursor = None; if let Some(secondary) = secondary.as_ref() { let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer); cursor.next(); secondary_cursor = Some(cursor); }
let max_point = buffer.max_point(); let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter); iter::from_fn(move || { loop { let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?; let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
let base_word_diffs = hunk.base_word_diffs.clone(); let buffer_word_diffs = hunk.buffer_word_diffs.clone();
if !start_anchor.is_valid(buffer) { continue; }
if end_point.column > 0 && end_point < max_point { end_point.row += 1; end_point.column = 0; end_anchor = buffer.anchor_before(end_point); }
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut has_pending = false; if start_anchor .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer) .is_gt() { pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left); }
if let Some(pending_hunk) = pending_hunks_cursor.item() { let mut pending_range = pending_hunk.buffer_range.to_point(buffer); if pending_range.end.column > 0 { pending_range.end.row += 1; pending_range.end.column = 0; }
if pending_range == (start_point..end_point) && !buffer.has_edits_since_in_range( &pending_hunk.buffer_version, start_anchor..end_anchor, ) { match pending_hunk.sense { PendingSense::SetSecondaryStatus { stage } => { has_pending = true; secondary_status = if stage { DiffHunkSecondaryStatus::SecondaryHunkRemovalPending } else { DiffHunkSecondaryStatus::SecondaryHunkAdditionPending }; } PendingSense::Suppress => continue, } } }
if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) { if start_anchor .cmp(&secondary_cursor.start().buffer_range.start, buffer) .is_gt() { secondary_cursor.seek_forward(&start_anchor, Bias::Left); }
if let Some(secondary_hunk) = secondary_cursor.item() { let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer); if secondary_range.end.column > 0 { secondary_range.end.row += 1; secondary_range.end.column = 0; } if secondary_range.is_empty() && secondary_hunk.diff_base_byte_range.is_empty() { // ignore } else if secondary_range == (start_point..end_point) { secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; } else if secondary_range.start <= end_point { secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk; } } }
return Some(DiffHunk { range: start_point..end_point, diff_base_byte_range: start_base..end_base, buffer_range: start_anchor..end_anchor, base_word_diffs, buffer_word_diffs, secondary_status, }); } }) }
fn hunks_intersecting_range_rev_impl<'a>( &'a self, filter: impl 'a + Fn(&DiffHunkSummary) -> bool, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
iter::from_fn(move || { cursor.prev();
let hunk = cursor.item()?; let range = hunk.buffer_range.to_point(buffer);
Some(DiffHunk { range, diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), // The secondary status is not used by callers of this method. secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk, base_word_diffs: hunk.base_word_diffs.clone(), buffer_word_diffs: hunk.buffer_word_diffs.clone(), }) }) }}
fn build_diff_options( language: Option<LanguageName>, language_scope: Option<language::LanguageScope>, cx: &App,) -> Option<DiffOptions> { #[cfg(any(test, feature = "test-support"))] { if !cx.has_global::<settings::SettingsStore>() { return Some(DiffOptions { language_scope, max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT, ..Default::default() }); } }
LanguageSettings::resolve(None, language.as_ref(), cx) .word_diff_enabled .then_some(DiffOptions { language_scope, max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT, ..Default::default() })}
fn compute_hunks( diff_base: Option<(Arc<str>, Rope)>, buffer: &text::BufferSnapshot, diff_options: Option<DiffOptions>,) -> SumTree<InternalDiffHunk> { let mut tree = SumTree::new(buffer);
if let Some((diff_base, diff_base_rope)) = diff_base { let buffer_text = buffer.as_rope().to_string();
// A common case in Zed is that the empty buffer is represented as just a newline, // but if we just compute a naive diff you get a "preserved" line in the middle, // which is a bit odd. if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 { tree.push( InternalDiffHunk { buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0), diff_base_byte_range: 0..diff_base.len() - 1, diff_base_point_range: Point::new(0, 0) ..diff_base_rope.offset_to_point(diff_base.len() - 1), base_word_diffs: Vec::default(), buffer_word_diffs: Vec::default(), }, buffer, ); return tree; }
let input = InternedInput::new(lines(diff_base.as_ref()), lines(buffer_text.as_str())); let mut diff = Diff::compute(Algorithm::Histogram, &input); // Canonicalize the placement of ambiguous hunks (git's slider/indent // heuristic). Without this, diffs of the same buffer against different // base texts (e.g. HEAD vs index) can anchor the same logical change at // different rows, and code that correlates hunks across those diffs // misbehaves: hunks render as staged when they aren't, and staging or // unstaging them corrupts the index. diff.postprocess_lines(&input); let mut sink = HunkSink::new(&diff_base, &diff_base_rope, buffer, diff_options.as_ref()); for hunk in diff.hunks() { sink.process_change(hunk.before, hunk.after); } for hunk in sink.finish() { tree.push(hunk, buffer); } } else { tree.push( InternalDiffHunk { buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()), diff_base_byte_range: 0..0, diff_base_point_range: Point::new(0, 0)..Point::new(0, 0), base_word_diffs: Vec::default(), buffer_word_diffs: Vec::default(), }, buffer, ); }
tree}struct HunkSink<'a> { diff_base_rope: &'a Rope, buffer: &'a text::BufferSnapshot, diff_options: Option<&'a DiffOptions>, old_line_offsets: Vec<usize>, hunks: Vec<InternalDiffHunk>,}
impl<'a> HunkSink<'a> { fn new( diff_base: &'a str, diff_base_rope: &'a Rope, buffer: &'a text::BufferSnapshot, diff_options: Option<&'a DiffOptions>, ) -> Self { let old_line_offsets = Self::compute_line_offsets(diff_base); Self { diff_base_rope, buffer, diff_options, old_line_offsets, hunks: Vec::new(), } }
fn compute_line_offsets(text: &str) -> Vec<usize> { let mut offsets = vec![0]; let mut offset = 0; for line in lines(text) { offset += line.len(); offsets.push(offset); } offsets }}
impl HunkSink<'_> { fn process_change(&mut self, before: Range<u32>, after: Range<u32>) { let old_start = before.start as usize; let old_end = before.end as usize; let new_start = after.start as usize; let new_end = after.end as usize;
let diff_base_byte_range = self.old_line_offsets[old_start]..self.old_line_offsets[old_end];
let buffer_row_range = (new_start as u32)..(new_end as u32);
let start = Point::new(buffer_row_range.start, 0); let end = Point::new(buffer_row_range.end, 0); let buffer_range = self.buffer.anchor_before(start)..self.buffer.anchor_before(end);
let base_line_count = old_end - old_start; let buffer_line_count = new_end - new_start;
let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = self.diff_options && !buffer_row_range.is_empty() && base_line_count == buffer_line_count && diff_options.max_word_diff_line_count >= base_line_count { let base_text: String = self .diff_base_rope .chunks_in_range(diff_base_byte_range.clone()) .collect(); let buffer_text: String = self.buffer.text_for_range(buffer_range.clone()).collect();
let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges( &base_text, &buffer_text, DiffOptions { language_scope: diff_options.language_scope.clone(), ..*diff_options }, );
let buffer_start_offset = buffer_range.start.to_offset(self.buffer); let buffer_word_diffs = buffer_word_diffs_relative .into_iter() .map(|range| { let start = self.buffer.anchor_after(buffer_start_offset + range.start); let end = self.buffer.anchor_after(buffer_start_offset + range.end); start..end }) .collect();
(base_word_diffs, buffer_word_diffs) } else { (Vec::default(), Vec::default()) };
self.hunks.push(InternalDiffHunk { buffer_range, diff_base_byte_range: diff_base_byte_range.clone(), diff_base_point_range: self .diff_base_rope .offset_to_point(diff_base_byte_range.start) ..self .diff_base_rope .offset_to_point(diff_base_byte_range.end), base_word_diffs, buffer_word_diffs, }); }
fn finish(self) -> Vec<InternalDiffHunk> { self.hunks }}
fn compare_hunks( new_hunks: &SumTree<InternalDiffHunk>, old_hunks: &SumTree<InternalDiffHunk>, old_snapshot: &text::BufferSnapshot, new_snapshot: &text::BufferSnapshot, old_base_text: &text::BufferSnapshot, new_base_text: &text::BufferSnapshot,) -> DiffChanged { let mut new_cursor = new_hunks.cursor::<()>(new_snapshot); let mut old_cursor = old_hunks.cursor::<()>(new_snapshot); old_cursor.next(); new_cursor.next(); let mut start = None; let mut end = None; let mut base_text_start: Option<Anchor> = None; let mut base_text_end: Option<Anchor> = None;
let mut last_unchanged_new_hunk_end: Option<text::Anchor> = None; let mut has_changes = false; let mut extended_end_candidate: Option<text::Anchor> = None;
loop { match (new_cursor.item(), old_cursor.item()) { (Some(new_hunk), Some(old_hunk)) => { match new_hunk .buffer_range .start .cmp(&old_hunk.buffer_range.start, new_snapshot) { Ordering::Less => { has_changes = true; extended_end_candidate = None; start.get_or_insert(new_hunk.buffer_range.start); base_text_start.get_or_insert( new_base_text.anchor_before(new_hunk.diff_base_byte_range.start), ); end.replace(new_hunk.buffer_range.end); let new_diff_range_end = new_base_text.anchor_after(new_hunk.diff_base_byte_range.end); if base_text_end.is_none_or(|base_text_end| { new_diff_range_end .cmp(&base_text_end, &new_base_text) .is_gt() }) { base_text_end = Some(new_diff_range_end) } new_cursor.next(); } Ordering::Equal => { if new_hunk != old_hunk { has_changes = true; extended_end_candidate = None; start.get_or_insert(new_hunk.buffer_range.start); base_text_start.get_or_insert( new_base_text.anchor_before(new_hunk.diff_base_byte_range.start), ); if old_hunk .buffer_range .end .cmp(&new_hunk.buffer_range.end, new_snapshot) .is_ge() { end.replace(old_hunk.buffer_range.end); } else { end.replace(new_hunk.buffer_range.end); }
let old_hunk_diff_base_range_end = old_base_text.anchor_after(old_hunk.diff_base_byte_range.end); let new_hunk_diff_base_range_end = new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
base_text_end.replace( *old_hunk_diff_base_range_end .max(&new_hunk_diff_base_range_end, new_base_text), ); } else { if !has_changes { last_unchanged_new_hunk_end = Some(new_hunk.buffer_range.end); } else if extended_end_candidate.is_none() { extended_end_candidate = Some(new_hunk.buffer_range.start); } }
new_cursor.next(); old_cursor.next(); } Ordering::Greater => { has_changes = true; extended_end_candidate = None; start.get_or_insert(old_hunk.buffer_range.start); base_text_start.get_or_insert( old_base_text.anchor_after(old_hunk.diff_base_byte_range.start), ); end.replace(old_hunk.buffer_range.end); let old_diff_range_end = old_base_text.anchor_after(old_hunk.diff_base_byte_range.end); if base_text_end.is_none_or(|base_text_end| { old_diff_range_end .cmp(&base_text_end, new_base_text) .is_gt() }) { base_text_end = Some(old_diff_range_end) } old_cursor.next(); } } } (Some(new_hunk), None) => { has_changes = true; extended_end_candidate = None; start.get_or_insert(new_hunk.buffer_range.start); base_text_start .get_or_insert(new_base_text.anchor_after(new_hunk.diff_base_byte_range.start)); if end.is_none_or(|end| end.cmp(&new_hunk.buffer_range.end, &new_snapshot).is_le()) { end.replace(new_hunk.buffer_range.end); } let new_base_text_end = new_base_text.anchor_after(new_hunk.diff_base_byte_range.end); if base_text_end.is_none_or(|base_text_end| { new_base_text_end.cmp(&base_text_end, new_base_text).is_gt() }) { base_text_end = Some(new_base_text_end) } new_cursor.next(); } (None, Some(old_hunk)) => { has_changes = true; extended_end_candidate = None; start.get_or_insert(old_hunk.buffer_range.start); base_text_start .get_or_insert(old_base_text.anchor_after(old_hunk.diff_base_byte_range.start)); if end.is_none_or(|end| end.cmp(&old_hunk.buffer_range.end, &new_snapshot).is_le()) { end.replace(old_hunk.buffer_range.end); } let old_base_text_end = old_base_text.anchor_after(old_hunk.diff_base_byte_range.end); if base_text_end.is_none_or(|base_text_end| { old_base_text_end.cmp(&base_text_end, new_base_text).is_gt() }) { base_text_end = Some(old_base_text_end); } old_cursor.next(); } (None, None) => break, } }
let changed_range = start.zip(end).map(|(start, end)| start..end); let base_text_changed_range = base_text_start .zip(base_text_end) .map(|(start, end)| (start..end).to_offset(new_base_text));
let extended_range = if has_changes && let Some(changed_range) = changed_range.clone() { let extended_start = *last_unchanged_new_hunk_end .unwrap_or(text::Anchor::min_for_buffer(new_snapshot.remote_id())) .min(&changed_range.start, new_snapshot); let extended_start = new_snapshot .anchored_edits_since_in_range::<usize>( &old_snapshot.version(), extended_start..changed_range.start, ) .map(|(_, anchors)| anchors.start) .min_by(|a, b| a.cmp(b, new_snapshot)) .unwrap_or(changed_range.start);
let extended_end = *extended_end_candidate .unwrap_or(text::Anchor::max_for_buffer(new_snapshot.remote_id())) .max(&changed_range.end, new_snapshot); let extended_end = new_snapshot .anchored_edits_since_in_range::<usize>( &old_snapshot.version(), changed_range.end..extended_end, ) .map(|(_, anchors)| anchors.end) .max_by(|a, b| a.cmp(b, new_snapshot)) .unwrap_or(changed_range.end);
Some(extended_start..extended_end) } else { None };
DiffChanged { changed_range, base_text_changed_range, extended_range, base_text_changed: false, }}
impl std::fmt::Debug for BufferDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferChangeSet") .field("buffer_id", &self.buffer_id) .finish() }}
#[derive(Clone, Debug, Default)]pub struct DiffChanged { pub changed_range: Option<Range<text::Anchor>>, pub base_text_changed_range: Option<Range<usize>>, pub extended_range: Option<Range<text::Anchor>>, pub base_text_changed: bool,}
#[derive(Clone, Debug)]pub enum BufferDiffEvent { BaseTextChanged, DiffChanged(DiffChanged),}
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
impl BufferDiff { pub fn new( buffer: &text::BufferSnapshot, language: Option<Arc<Language>>, language_registry: Option<Arc<LanguageRegistry>>, base_kind: DiffBaseKind, cx: &mut App, ) -> Self { let base_text = cx.new(|cx| { let mut base_buffer = language::Buffer::local("", cx); base_buffer.set_capability(Capability::ReadOnly, cx); if let Some(language_registry) = language_registry { base_buffer.set_language_registry(language_registry); } base_buffer.set_language_async(language, cx); base_buffer });
BufferDiff { buffer_id: buffer.remote_id(), base_text_buffer: base_text, diff_snapshot: None, buffer_snapshot: buffer.clone(), secondary_diff: None, base_kind, } }
pub fn new_with_base_text_buffer( buffer: &text::BufferSnapshot, base_text_buffer: Entity<language::Buffer>, base_kind: DiffBaseKind, _cx: &mut App, ) -> Self { BufferDiff { buffer_id: buffer.remote_id(), base_text_buffer, diff_snapshot: None, buffer_snapshot: buffer.clone(), secondary_diff: None, base_kind, } }
pub fn new_unchanged( buffer: &text::BufferSnapshot, language: Option<Arc<Language>>, language_registry: Option<Arc<LanguageRegistry>>, base_kind: DiffBaseKind, cx: &mut Context<Self>, ) -> Self { let base_text = buffer.text(); let base_text = cx.new(|cx| { let mut base_buffer = language::Buffer::local(base_text, cx); base_buffer.set_capability(Capability::ReadOnly, cx); if let Some(language_registry) = language_registry { base_buffer.set_language_registry(language_registry); } base_buffer.set_language_async(language, cx); base_buffer });
let base_text_snapshot = base_text.read(cx).snapshot();
let diff_snapshot = BufferDiffSnapshot { hunks: SumTree::new(buffer), pending_hunks: SumTree::new(buffer), base_text: base_text_snapshot, base_text_exists: true, buffer_snapshot: buffer.clone(), secondary_diff: None, };
BufferDiff { buffer_id: buffer.remote_id(), base_text_buffer: base_text, diff_snapshot: Some(diff_snapshot), buffer_snapshot: buffer.clone(), secondary_diff: None, base_kind, } }
#[cfg(any(test, feature = "test-support"))] pub fn new_with_base_text( base_text: &str, buffer: &text::BufferSnapshot, cx: &mut Context<Self>, ) -> Self { let mut this = BufferDiff::new(buffer, None, None, DiffBaseKind::Head, cx); let mut base_text = base_text.to_owned(); text::LineEnding::normalize(&mut base_text); let base_text_buffer = cx.new(|cx| { let mut buffer = language::Buffer::local(base_text, cx); buffer.set_capability(Capability::ReadOnly, cx); buffer }); let base_text = base_text_buffer.read(cx).snapshot(); this.base_text_buffer = base_text_buffer; let update = cx.foreground_executor().block_on(this.update_diff( buffer.clone(), &base_text, Some(Arc::from(base_text.text())), cx, )); this.set_snapshot(update, cx); this }
pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) { self.secondary_diff = Some(diff); }
pub fn base_kind(&self) -> DiffBaseKind { self.base_kind }
/// Whether hunks in this diff can be staged or restored: true only when /// the diff's base is HEAD. pub fn is_stageable(&self) -> bool { self.base_kind == DiffBaseKind::Head }
pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> { self.secondary_diff.clone() }
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) { let Some(diff_snapshot) = &mut self.diff_snapshot else { return; }; let Some((first, last)) = diff_snapshot .pending_hunks .first() .zip(diff_snapshot.pending_hunks.last()) else { return; }; let changed_range = first.buffer_range.start..last.buffer_range.end; let base_text_changed_range = first.diff_base_byte_range.start..last.diff_base_byte_range.end; let buffer = diff_snapshot.buffer_snapshot.clone(); diff_snapshot.pending_hunks = SumTree::new(&buffer); cx.emit(BufferDiffEvent::DiffChanged(DiffChanged { changed_range: Some(changed_range.clone()), base_text_changed_range: Some(base_text_changed_range), extended_range: Some(changed_range), base_text_changed: false, })); }
/// Installs optimistic pending hunks in this diff, merging them with any /// existing pending hunks (newest wins on overlap) and emitting a /// `DiffChanged` covering both the new hunks and any existing pending hunks /// they replace. `hunks` must be sorted by `buffer_range.start` and /// non-overlapping. /// /// `buffer` must be a current snapshot of this diff's main buffer: the /// incoming hunks carry anchors minted from the current buffer, which this /// diff's internal snapshot (from the last settled recalculation) may not /// have observed yet. pub fn set_pending_hunks( &mut self, hunks: &[PendingHunk], buffer: &text::BufferSnapshot, cx: &mut Context<Self>, ) { if hunks.is_empty() { return; } let Some(diff_snapshot) = self.diff_snapshot.as_mut() else { return; };
let mut new_pending = SumTree::new(buffer); let mut old = diff_snapshot .pending_hunks .cursor::<DiffHunkSummary>(buffer); let mut changed_start: Option<Anchor> = None; let mut changed_end: Option<Anchor> = None; let mut base_start = usize::MAX; let mut base_end = 0usize; let mut extend_changed_range = |buffer_range: &Range<Anchor>, base_range: &Range<usize>| { changed_start = Some(changed_start.map_or(buffer_range.start, |start| { *start.min(&buffer_range.start, buffer) })); changed_end = Some( changed_end.map_or(buffer_range.end, |end| *end.max(&buffer_range.end, buffer)), ); base_start = base_start.min(base_range.start); base_end = base_end.max(base_range.end); }; for hunk in hunks { let preceding = old.slice(&hunk.buffer_range.start, Bias::Left); new_pending.append(preceding, buffer);
// Drop any overlapping or adjacent existing pending hunks, folding // them into the changed range so that views repaint their full // extent (a replaced hunk can be wider than its replacement). while let Some(old_hunk) = old.item() { if old_hunk .buffer_range .start .cmp(&hunk.buffer_range.end, buffer) .is_gt() { break; } extend_changed_range(&old_hunk.buffer_range, &old_hunk.diff_base_byte_range); old.next(); }
extend_changed_range(&hunk.buffer_range, &hunk.diff_base_byte_range); new_pending.push(hunk.clone(), buffer); } new_pending.append(old.suffix(), buffer); drop(old); diff_snapshot.pending_hunks = new_pending;
if let (Some(start), Some(end)) = (changed_start, changed_end) { let changed_range = Some(start..end); cx.emit(BufferDiffEvent::DiffChanged(DiffChanged { changed_range: changed_range.clone(), base_text_changed_range: Some(base_start..base_end), extended_range: changed_range, base_text_changed: false, })); } }
/// Optimistically marks every stageable (resp. unstageable) hunk in this diff /// as staging (resp. unstaging). Used by whole-file staging from the git /// panel, where the actual index change is performed by `git add`/`reset` /// rather than the optimistic index patch. pub fn mark_all_hunks_pending( &mut self, stage: bool, buffer: &text::BufferSnapshot, cx: &mut Context<Self>, ) { let sense = PendingSense::SetSecondaryStatus { stage }; let version = buffer.version().clone(); let hunks = self .snapshot(cx) .hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer) .filter(|hunk| { !((stage && hunk.secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk) || (!stage && hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)) }) .map(|hunk| { PendingHunk::new( hunk.buffer_range, hunk.diff_base_byte_range, version.clone(), sense, ) }) .collect::<Vec<_>>(); self.set_pending_hunks(&hunks, buffer, cx); }
pub fn suppress_all_hunks_pending( &mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>, ) { let version = buffer.version().clone(); let hunks = self .snapshot(cx) .raw_hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer, ) .map(|hunk| { PendingHunk::new( hunk.buffer_range, hunk.diff_base_byte_range, version.clone(), PendingSense::Suppress, ) }) .collect::<Vec<_>>(); self.set_pending_hunks(&hunks, buffer, cx); }
/// Computes the index-text edits for unstaging the given staged (HEAD-vs-index) /// hunks. `index_buffer` is this diff's main buffer (the index text). The /// returned edits are in index coordinates. pub fn unstage_staged_hunks( &self, hunks: &[DiffHunk], index_buffer: &text::BufferSnapshot, ) -> Option<Vec<(Range<usize>, Arc<str>)>> { let Some(diff_snapshot) = self.diff_snapshot.as_ref() else { return Some(Vec::new()); }; // With no HEAD, the whole file is one staged addition; unstaging it // removes the file from the index entirely. if !diff_snapshot.base_text_exists { return None; } let head_text = diff_snapshot.base_text.as_rope(); let mut edits = hunks .iter() .map(|hunk| { let index_range = hunk.buffer_range.to_offset(index_buffer); let replacement_text: Arc<str> = Arc::from( head_text .chunks_in_range(hunk.diff_base_byte_range.clone()) .collect::<String>(), ); (index_range, replacement_text) }) .collect::<Vec<_>>(); edits.sort_by_key(|(range, _)| range.start); Some(edits) }
#[cfg(any(test, feature = "test-support"))] pub fn stage_or_unstage_hunks( &mut self, stage: bool, hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, cx: &mut Context<Self>, ) -> Option<Rope> { let secondary_diff = self.secondary_diff.clone()?; let unstaged_diff_snapshot = secondary_diff.read_with(cx, |secondary_diff, _cx| { secondary_diff.diff_snapshot.clone() })?; let diff_snapshot = self.diff_snapshot.clone()?; let (edits, pending) = diff_snapshot.compute_uncommitted_index_edits( &unstaged_diff_snapshot, stage, hunks, buffer, file_exists, ); self.set_pending_hunks(&pending, buffer, cx); edits.map(|edits| { let mut index_text = unstaged_diff_snapshot.base_text.as_rope().clone(); for (old_range, replacement_text) in edits.iter().rev() { index_text.replace(old_range.clone(), replacement_text); } index_text }) }
#[cfg(any(test, feature = "test-support"))] pub fn stage_or_unstage_all_hunks( &mut self, stage: bool, buffer: &text::BufferSnapshot, file_exists: bool, cx: &mut Context<Self>, ) -> Option<Rope> { let hunks = self .snapshot(cx) .hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer) .collect::<Vec<_>>(); self.stage_or_unstage_hunks(stage, &hunks, buffer, file_exists, cx) }
pub fn update_diff( &self, buffer: text::BufferSnapshot, base_text_snapshot: &language::BufferSnapshot, base_text: Option<Arc<str>>, cx: &App, ) -> Task<BufferDiffUpdate> { let base_text = base_text.map(|t| text::LineEnding::normalize_arc(t)); debug_assert_eq!( base_text.as_deref().unwrap_or_default(), &base_text_snapshot.text() ); debug_assert_eq!( base_text_snapshot.remote_id(), self.base_text_buffer.read(cx).remote_id() );
let language = base_text_snapshot.language(); let diff_options = build_diff_options( language.map(|l| l.name()), language.map(|l| l.default_scope()), cx, ); let buffer_snapshot = buffer.clone(); let base_text_snapshot = base_text_snapshot.clone(); let base_text_exists = base_text.is_some(); let unchanged_hunks = self.diff_snapshot.as_ref().and_then(|diff_snapshot| { if diff_snapshot.base_text_exists == base_text_exists && diff_snapshot.base_text.version() == base_text_snapshot.version() && diff_snapshot.buffer_snapshot.version() == buffer_snapshot.version() { Some(diff_snapshot.hunks.clone()) } else { None } });
cx.background_executor().spawn(async move { let hunks = if let Some(unchanged_hunks) = unchanged_hunks { unchanged_hunks } else if let Some(base_text) = base_text { compute_hunks( Some((base_text, base_text_snapshot.as_rope().clone())), &buffer, diff_options, ) } else { compute_hunks(None, &buffer, diff_options) };
BufferDiffUpdate { hunks, base_text: base_text_snapshot, base_text_exists, buffer_snapshot, } }) }
pub fn set_snapshot_with_secondary( &mut self, update: BufferDiffUpdate, secondary_diff_change: Option<Range<Anchor>>, clear_pending_hunks: bool, cx: &mut Context<Self>, ) -> Option<Range<Anchor>> { log::debug!("set snapshot with secondary {secondary_diff_change:?}");
let BufferDiffUpdate { hunks: new_hunks, base_text: new_base_text, base_text_exists: new_base_text_exists, buffer_snapshot: new_buffer_snapshot, } = update; let buffer = &new_buffer_snapshot; let old_snapshot = self .diff_snapshot .clone() .unwrap_or_else(|| BufferDiffSnapshot { hunks: SumTree::new(buffer), pending_hunks: SumTree::new(buffer), base_text: new_base_text.clone(), base_text_exists: false, buffer_snapshot: new_buffer_snapshot.clone(), secondary_diff: None, }); let mut new_snapshot = BufferDiffSnapshot { hunks: new_hunks.clone(), base_text: new_base_text.clone(), base_text_exists: new_base_text_exists, buffer_snapshot: new_buffer_snapshot.clone(), pending_hunks: old_snapshot.pending_hunks.clone(), secondary_diff: None, };
let old_base_text_exists = old_snapshot.base_text_exists; let old_buffer_snapshot = &old_snapshot.buffer_snapshot; let old_base_text = &old_snapshot.base_text; let base_text_changed = old_base_text_exists != new_base_text_exists || (new_base_text_exists && (old_base_text.remote_id() != new_base_text.remote_id() || new_base_text .version() .changed_since(old_base_text.version()))); let DiffChanged { mut changed_range, mut base_text_changed_range, mut extended_range, base_text_changed: _, } = match (old_base_text_exists, new_base_text_exists) { (false, false) if self.diff_snapshot.is_some() => DiffChanged::default(), (true, true) => compare_hunks( &new_hunks, &old_snapshot.hunks, old_buffer_snapshot, buffer, old_base_text, &new_base_text, ), _ => { let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id); let full_base_range = 0..new_base_text.len(); DiffChanged { changed_range: Some(full_range.clone()), base_text_changed_range: Some(full_base_range), extended_range: Some(full_range), base_text_changed: false, } } };
if base_text_changed || clear_pending_hunks { if let Some((first, last)) = old_snapshot .pending_hunks .first() .zip(old_snapshot.pending_hunks.last()) { let pending_range = first.buffer_range.start..last.buffer_range.end; if let Some(range) = &mut changed_range { range.start = *range.start.min(&pending_range.start, buffer); range.end = *range.end.max(&pending_range.end, buffer); } else { changed_range = Some(pending_range.clone()); }
if let Some(base_text_range) = base_text_changed_range.as_mut() { base_text_range.start = base_text_range.start.min(first.diff_base_byte_range.start); base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end); } else { base_text_changed_range = Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end); }
if let Some(ext) = &mut extended_range { ext.start = *ext.start.min(&pending_range.start, buffer); ext.end = *ext.end.max(&pending_range.end, buffer); } else { extended_range = Some(pending_range); } } new_snapshot.pending_hunks = SumTree::new(buffer); }
if let Some(secondary_changed_range) = secondary_diff_change && let (Some(secondary_hunk_range), Some(secondary_base_range)) = old_snapshot.range_to_hunk_range(secondary_changed_range, buffer) { if let Some(range) = &mut changed_range { range.start = *secondary_hunk_range.start.min(&range.start, buffer); range.end = *secondary_hunk_range.end.max(&range.end, buffer); } else { changed_range = Some(secondary_hunk_range.clone()); }
if let Some(base_text_range) = base_text_changed_range.as_mut() { base_text_range.start = secondary_base_range.start.min(base_text_range.start); base_text_range.end = secondary_base_range.end.max(base_text_range.end); } else { base_text_changed_range = Some(secondary_base_range); }
if let Some(ext) = &mut extended_range { ext.start = *ext.start.min(&secondary_hunk_range.start, buffer); ext.end = *ext.end.max(&secondary_hunk_range.end, buffer); } else { extended_range = Some(secondary_hunk_range); } }
self.diff_snapshot = Some(new_snapshot); self.buffer_snapshot = new_buffer_snapshot;
let result = DiffChanged { changed_range, base_text_changed_range, extended_range, base_text_changed, }; if result.base_text_changed { cx.emit(BufferDiffEvent::BaseTextChanged); } let changed_range = result.changed_range.clone(); cx.emit(BufferDiffEvent::DiffChanged(result)); changed_range }
pub fn set_snapshot( &mut self, new_state: BufferDiffUpdate, cx: &mut Context<Self>, ) -> Option<Range<Anchor>> { self.set_snapshot_with_secondary(new_state, None, false, cx) }
pub fn base_text(&self, cx: &App) -> language::BufferSnapshot { self.base_text_buffer.read(cx).snapshot() }
pub fn base_text_exists(&self) -> bool { self.diff_snapshot .as_ref() .is_some_and(|diff_snapshot| diff_snapshot.base_text_exists) }
pub fn changed_row_counts(&self) -> (u32, u32) { self.diff_snapshot .as_ref() .map_or((0, 0), |diff_snapshot| diff_snapshot.changed_row_counts()) }
pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot { let mut snapshot = self.diff_snapshot.clone().unwrap_or_else(|| { let base_text = self.base_text_buffer.read(cx).snapshot(); BufferDiffSnapshot { hunks: SumTree::new(&self.buffer_snapshot), pending_hunks: SumTree::new(&self.buffer_snapshot), base_text, base_text_exists: false, buffer_snapshot: self.buffer_snapshot.clone(), secondary_diff: None, } }); snapshot.secondary_diff = self.secondary_diff.as_ref().map(|diff| { debug_assert!(diff.read(cx).secondary_diff.is_none()); Arc::new(diff.read(cx).snapshot(cx)) }); snapshot }
/// Used in cases where the change set isn't derived from git. /// /// Dropping the returned task cancels the update, leaving the diff /// unchanged. Calls must not overlap; to re-run this when the buffer or /// base text changes, store the task somewhere that the next call will /// overwrite, so that the previous call is cancelled. pub fn set_base_text( &mut self, base_text: Option<Arc<str>>, buffer: text::BufferSnapshot, cx: &mut Context<Self>, ) -> Task<()> { cx.spawn(async move |this, cx| { let base_text_exists = base_text.is_some(); let base_text = base_text.unwrap_or_default(); let Some(base_text_diff) = this .update(cx, |this, cx| { this.base_text_buffer.update(cx, |base_text_buffer, cx| { base_text_buffer.diff(base_text.clone(), cx) }) }) .log_err() else { return; }; let base_text_diff = base_text_diff.await; let Some(edited_base_text) = this .update(cx, |this, cx| { if this.base_text_buffer.read(cx).version() != base_text_diff.base_version { log::warn!("dropping concurrent diff update"); debug_panic!("incorrect concurrent call to set_base_text"); return None; } let edited_base_text = this.base_text_buffer.update(cx, |base_text_buffer, cx| { base_text_buffer.set_line_ending(base_text_diff.line_ending, cx); assert!(base_text_buffer.version() == base_text_diff.base_version); base_text_buffer.snapshot_with_edits(base_text_diff.edits, cx) }); Some(edited_base_text) }) .log_err() .flatten() else { return; }; let edited_base_text = edited_base_text.await; let base_text_snapshot = edited_base_text.snapshot().clone(); let Some(state) = this .update(cx, |this, cx| { this.update_diff( buffer.clone(), &base_text_snapshot, base_text_exists.then(|| base_text.clone()), cx, ) }) .log_err() else { return; }; let state = state.await; this.update(cx, |this, cx| { if &this.base_text_buffer.read(cx).version() != edited_base_text.base_version() { log::warn!("dropping concurrent diff update"); debug_panic!("incorrect concurrent call to set_base_text"); return; }
this.base_text_buffer.update(cx, |base_text_buffer, cx| { base_text_buffer.fast_forward(edited_base_text, cx) }); this.set_snapshot(state, cx); }) .log_err(); }) }
pub fn base_text_string(&self, _cx: &App) -> Option<String> { self.diff_snapshot.as_ref().and_then(|diff_snapshot| { if diff_snapshot.base_text_exists { Some(diff_snapshot.base_text.text()) } else { None } }) }
#[cfg(any(test, feature = "test-support"))] pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>) { let base_text = self.base_text(cx); let fut = self.update_diff( buffer.clone(), &base_text, self.base_text_exists().then(|| Arc::from(base_text.text())), cx, ); let fg_executor = cx.foreground_executor().clone(); let snapshot = fg_executor.block_on(fut); let _changed_range = self.set_snapshot(snapshot, cx); }
pub fn base_text_buffer(&self) -> &Entity<language::Buffer> { &self.base_text_buffer }}
impl DiffHunk { pub fn is_created_file(&self) -> bool { self.diff_base_byte_range == (0..0) && self.buffer_range.start.is_min() && self.buffer_range.end.is_max() }
pub fn status(&self) -> DiffHunkStatus { let kind = if self.buffer_range.start == self.buffer_range.end { DiffHunkStatusKind::Deleted } else if self.diff_base_byte_range.is_empty() { DiffHunkStatusKind::Added } else { DiffHunkStatusKind::Modified }; DiffHunkStatus { kind, secondary: self.secondary_status, } }}
impl DiffHunkStatus { pub fn has_secondary_hunk(&self) -> bool { matches!( self.secondary, DiffHunkSecondaryStatus::HasSecondaryHunk | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk ) }
pub fn is_pending(&self) -> bool { matches!( self.secondary, DiffHunkSecondaryStatus::SecondaryHunkAdditionPending | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending ) }
pub fn is_deleted(&self) -> bool { self.kind == DiffHunkStatusKind::Deleted }
pub fn is_added(&self) -> bool { self.kind == DiffHunkStatusKind::Added }
pub fn is_modified(&self) -> bool { self.kind == DiffHunkStatusKind::Modified }
pub fn added(secondary: DiffHunkSecondaryStatus) -> Self { Self { kind: DiffHunkStatusKind::Added, secondary, } }
pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self { Self { kind: DiffHunkStatusKind::Modified, secondary, } }
pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self { Self { kind: DiffHunkStatusKind::Deleted, secondary, } }
pub fn deleted_none() -> Self { Self { kind: DiffHunkStatusKind::Deleted, secondary: DiffHunkSecondaryStatus::NoSecondaryHunk, } }
pub fn added_none() -> Self { Self { kind: DiffHunkStatusKind::Added, secondary: DiffHunkSecondaryStatus::NoSecondaryHunk, } }
pub fn modified_none() -> Self { Self { kind: DiffHunkStatusKind::Modified, secondary: DiffHunkSecondaryStatus::NoSecondaryHunk, } }}
#[cfg(any(test, feature = "test-support"))]#[track_caller]pub fn assert_hunks<ExpectedText, HunkIter>( diff_hunks: HunkIter, buffer: &text::BufferSnapshot, diff_base: &str, // Line range, deleted, added, status expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],) where HunkIter: Iterator<Item = DiffHunk>, ExpectedText: AsRef<str>,{ let actual_hunks = diff_hunks .map(|hunk| { ( hunk.range.clone(), &diff_base[hunk.diff_base_byte_range.clone()], buffer .text_for_range(hunk.range.clone()) .collect::<String>(), hunk.status(), ) }) .collect::<Vec<_>>();
let expected_hunks: Vec<_> = expected_hunks .iter() .map(|(line_range, deleted_text, added_text, status)| { ( Point::new(line_range.start, 0)..Point::new(line_range.end, 0), deleted_text.as_ref(), added_text.as_ref().to_string(), *status, ) }) .collect();
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);}
#[cfg(test)]mod tests { use std::{fmt::Write as _, sync::mpsc};
use super::*; use gpui::TestAppContext; use pretty_assertions::{assert_eq, assert_ne}; use rand::{Rng as _, rngs::StdRng}; use text::{Buffer, BufferId, ReplicaId, Rope}; use unindent::Unindent as _; use util::test::marked_text_ranges;
#[ctor::ctor(unsafe)] fn init_logger() { zlog::init_test(); }
#[gpui::test] async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) { let diff_base = " one two three " .unindent();
let buffer_text = " one HELLO three " .unindent();
let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let mut diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx); assert_hunks( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), &buffer, ), &buffer, &diff_base, &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())], );
buffer.edit([(0..0, "point five\n")]); diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx); assert_hunks( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), &buffer, ), &buffer, &diff_base, &[ (0..1, "", "point five\n", DiffHunkStatus::added_none()), (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()), ], );
diff = cx .update(|cx| BufferDiff::new(&buffer, None, None, DiffBaseKind::Head, cx).snapshot(cx)); assert_hunks::<&str, _>( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), &buffer, ), &buffer, &diff_base, &[], ); }
#[gpui::test] async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) { let head_text = " zero one two three four five six seven eight nine " .unindent();
let index_text = " zero one TWO three FOUR five six seven eight NINE " .unindent();
let buffer_text = " zero one TWO three FOUR FIVE six SEVEN eight nine " .unindent();
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let unstaged_diff = BufferDiffSnapshot::new_sync(&buffer, index_text, cx); let mut uncommitted_diff = BufferDiffSnapshot::new_sync(&buffer, head_text.clone(), cx); uncommitted_diff.secondary_diff = Some(Arc::new(unstaged_diff));
let expected_hunks = vec![ (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()), ( 4..6, "four\nfive\n", "FOUR\nFIVE\n", DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk), ), ( 7..8, "seven\n", "SEVEN\n", DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk), ), ];
assert_hunks( uncommitted_diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), &buffer, ), &buffer, &head_text, &expected_hunks, ); }
#[gpui::test] async fn test_buffer_diff_range(cx: &mut TestAppContext) { let diff_base = " one two three four five six seven eight nine ten " .unindent();
let buffer_text = " A one B two C three HELLO four five SIXTEEN seven eight WORLD nine
ten
" .unindent();
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx); assert_eq!( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), &buffer ) .count(), 8 );
assert_hunks( diff.hunks_intersecting_range( buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)), &buffer, ), &buffer, &diff_base, &[ (6..7, "", "HELLO\n", DiffHunkStatus::added_none()), (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()), (12..13, "", "WORLD\n", DiffHunkStatus::added_none()), ], ); }
#[gpui::test] async fn test_stage_hunk(cx: &mut TestAppContext) { struct Example { name: &'static str, head_text: String, index_text: String, buffer_marked_text: String, final_index_text: String, }
let table = [ Example { name: "uncommitted hunk straddles end of unstaged hunk", head_text: " one two three four five " .unindent(), index_text: " one TWO_HUNDRED three FOUR_HUNDRED five " .unindent(), buffer_marked_text: " ZERO one two «THREE_HUNDRED FOUR_HUNDRED» five SIX " .unindent(), final_index_text: " one two THREE_HUNDRED FOUR_HUNDRED five " .unindent(), }, Example { name: "uncommitted hunk straddles start of unstaged hunk", head_text: " one two three four five " .unindent(), index_text: " one TWO_HUNDRED three FOUR_HUNDRED five " .unindent(), buffer_marked_text: " ZERO one «TWO_HUNDRED THREE_HUNDRED» four five SIX " .unindent(), final_index_text: " one TWO_HUNDRED THREE_HUNDRED four five " .unindent(), }, Example { name: "uncommitted hunk strictly contains unstaged hunks", head_text: " one two three four five six seven " .unindent(), index_text: " one TWO THREE FOUR FIVE SIX seven " .unindent(), buffer_marked_text: " one TWO «THREE_HUNDRED FOUR FIVE_HUNDRED» SIX seven " .unindent(), final_index_text: " one TWO THREE_HUNDRED FOUR FIVE_HUNDRED SIX seven " .unindent(), }, Example { name: "uncommitted deletion hunk", head_text: " one two three four five " .unindent(), index_text: " one two three four five " .unindent(), buffer_marked_text: " one ˇfive " .unindent(), final_index_text: " one five " .unindent(), }, Example { name: "one unstaged hunk that contains two uncommitted hunks", head_text: " one two
three four " .unindent(), index_text: " one two three four " .unindent(), buffer_marked_text: " «one
three // modified four» " .unindent(), final_index_text: " one
three // modified four " .unindent(), }, Example { name: "one uncommitted hunk that contains two unstaged hunks", head_text: " one two three four five " .unindent(), index_text: " ZERO one TWO THREE FOUR five " .unindent(), buffer_marked_text: " «one TWO_HUNDRED THREE FOUR_HUNDRED five» " .unindent(), final_index_text: " ZERO one TWO_HUNDRED THREE FOUR_HUNDRED five " .unindent(), }, ];
for example in table { let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false); let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let hunk_range = buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx));
let uncommitted_diff = cx.new(|cx| { let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx); diff.set_secondary_diff(unstaged_diff); diff });
uncommitted_diff.update(cx, |diff, cx| { let hunks = diff .snapshot(cx) .hunks_intersecting_range(hunk_range.clone(), &buffer) .collect::<Vec<_>>(); for hunk in &hunks { assert_ne!( hunk.secondary_status, DiffHunkSecondaryStatus::NoSecondaryHunk ) }
let new_index_text = diff .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx) .unwrap() .to_string();
let hunks = diff .snapshot(cx) .hunks_intersecting_range(hunk_range.clone(), &buffer) .collect::<Vec<_>>(); for hunk in &hunks { assert_eq!( hunk.secondary_status, DiffHunkSecondaryStatus::SecondaryHunkRemovalPending ) }
pretty_assertions::assert_eq!( new_index_text, example.final_index_text, "example: {}", example.name ); }); } }
#[gpui::test] async fn test_stage_all_with_nested_hunks(cx: &mut TestAppContext) { // This test reproduces a crash where staging all hunks would cause an underflow // when there's one large unstaged hunk containing multiple uncommitted hunks. let head_text = " aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll " .unindent();
let index_text = " aaa bbb CCC-index DDD-index EEE-index FFF-index GGG-index HHH-index III-index JJJ-index kkk lll " .unindent();
let buffer_text = " aaa bbb ccc-modified ddd eee-modified fff ggg hhh-modified iii jjj kkk lll " .unindent();
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx)); let uncommitted_diff = cx.new(|cx| { let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx); diff.set_secondary_diff(unstaged_diff); diff });
uncommitted_diff.update(cx, |diff, cx| { diff.stage_or_unstage_all_hunks(true, &buffer, true, cx); }); }
#[gpui::test] async fn test_stage_all_with_stale_buffer(cx: &mut TestAppContext) { // Regression test for ZED-5R2: when the buffer is edited after the diff is // computed but before staging, anchor positions shift while diff_base_byte_range // values don't. If the primary (HEAD) hunk extends past the unstaged (index) // hunk, an edit in the extension region shifts the primary hunk end without // shifting the unstaged hunk end. The overshoot calculation then produces an // index_end that exceeds index_text.len(). // // Setup: // HEAD: "aaa\nbbb\nccc\n" (primary hunk covers lines 1-2) // Index: "aaa\nbbb\nCCC\n" (unstaged hunk covers line 1 only) // Buffer: "aaa\nBBB\nCCC\n" (both lines differ from HEAD) // // The primary hunk spans buffer offsets 4..12, but the unstaged hunk only // spans 4..8. The pending hunk extends 4 bytes past the unstaged hunk. // An edit at offset 9 (inside "CCC") shifts the primary hunk end from 12 // to 13 but leaves the unstaged hunk end at 8, making index_end = 13 > 12. let head_text = "aaa\nbbb\nccc\n"; let index_text = "aaa\nbbb\nCCC\n"; let buffer_text = "aaa\nBBB\nCCC\n";
let mut buffer = Buffer::new( ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text.to_string(), );
let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(index_text, &buffer, cx)); let uncommitted_diff = cx.new(|cx| { let mut diff = BufferDiff::new_with_base_text(head_text, &buffer, cx); diff.set_secondary_diff(unstaged_diff); diff });
// Edit the buffer in the region between the unstaged hunk end (offset 8) // and the primary hunk end (offset 12). This shifts the primary hunk end // but not the unstaged hunk end. buffer.edit([(9..9, "Z")]);
uncommitted_diff.update(cx, |diff, cx| { diff.stage_or_unstage_all_hunks(true, &buffer, true, cx); }); }
#[gpui::test] async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) { let head_text = " one two three " .unindent(); let index_text = head_text.clone(); let buffer_text = " one three " .unindent();
let buffer = Buffer::new( ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text.clone(), ); let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx)); let uncommitted_diff = cx.new(|cx| { let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx); diff.set_secondary_diff(unstaged_diff.clone()); diff });
uncommitted_diff.update(cx, |diff, cx| { let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
let new_index_text = diff .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx) .unwrap() .to_string(); assert_eq!(new_index_text, buffer_text);
let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap(); assert_eq!( hunk.secondary_status, DiffHunkSecondaryStatus::SecondaryHunkRemovalPending );
let index_text = diff .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx) .unwrap() .to_string(); assert_eq!(index_text, head_text);
let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap(); // optimistically unstaged (fine, could also be HasSecondaryHunk) assert_eq!( hunk.secondary_status, DiffHunkSecondaryStatus::SecondaryHunkAdditionPending ); }); }
#[gpui::test] async fn test_set_pending_hunks_change_covers_replaced_hunks(cx: &mut TestAppContext) { let base_text = " zero one two three four five " .unindent(); let buffer_text = " ZERO one two THREE four FIVE " .unindent(); let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
// Install a whole-file pending hunk, as the no-HEAD staging paths do. let version = buffer.version(); diff.update(cx, |diff, cx| { diff.set_pending_hunks( &[PendingHunk::new( Anchor::min_max_range_for_buffer(buffer.remote_id()), 0..base_text.len(), version.clone(), PendingSense::SetSecondaryStatus { stage: true }, )], &buffer, cx, ) });
let (tx, rx) = mpsc::channel(); let subscription = cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
// Replace it with a narrower hunk; the emitted change must still cover // the whole extent of the replaced hunk. diff.update(cx, |diff, cx| { diff.set_pending_hunks( &[PendingHunk::new( buffer.anchor_before(Point::new(3, 0))..buffer.anchor_before(Point::new(4, 0)), base_text.find("three").unwrap()..base_text.find("four").unwrap(), version, PendingSense::Suppress, )], &buffer, cx, ) });
drop(subscription); let events = rx.into_iter().collect::<Vec<_>>(); match events.as_slice() { [ BufferDiffEvent::DiffChanged(DiffChanged { changed_range: Some(changed_range), .. }), ] => { assert_eq!( changed_range.to_point(&buffer), Point::zero()..buffer.max_point(), ); } _ => panic!("unexpected events: {:?}", events), } }
#[gpui::test] async fn test_buffer_diff_compare(cx: &mut TestAppContext) { let base_text = " zero one two three four five six seven eight nine " .unindent();
let buffer_text_1 = " one three four five SIX seven eight NINE " .unindent();
let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
let empty_diff = cx .update(|cx| BufferDiff::new(&buffer, None, None, DiffBaseKind::Head, cx).snapshot(cx)); let diff_1 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_1.hunks, &empty_diff.hunks, &buffer, &buffer, &diff_1.base_text(), &diff_1.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_1.base_text()), Point::new(0, 0)..Point::new(10, 0) );
// Edit does affects the diff because it recalculates word diffs. buffer.edit_via_marked_text( &" one three four five «SIX.5» seven eight NINE " .unindent(), ); let diff_2 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_2.hunks, &diff_1.hunks, &buffer, &buffer, diff_2.base_text(), diff_2.base_text(), ); assert_eq!( changed_range.unwrap().to_point(&buffer), Point::new(4, 0)..Point::new(5, 0), ); assert_eq!( base_text_changed_range .unwrap() .to_point(diff_2.base_text()), Point::new(6, 0)..Point::new(7, 0), );
// Edit turns a deletion hunk into a modification. buffer.edit_via_marked_text( &" one «THREE» four five SIX.5 seven eight NINE " .unindent(), ); let diff_3 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_3.hunks, &diff_2.hunks, &buffer, &buffer, diff_3.base_text(), diff_3.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_3.base_text()), Point::new(2, 0)..Point::new(4, 0) );
// Edit turns a modification hunk into a deletion. buffer.edit_via_marked_text( &" one THREE four five«» seven eight NINE " .unindent(), ); let diff_4 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_4.hunks, &diff_3.hunks, &buffer, &buffer, diff_4.base_text(), diff_4.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_4.base_text()), Point::new(6, 0)..Point::new(7, 0) );
// Edit introduces a new insertion hunk. buffer.edit_via_marked_text( &" one THREE four« FOUR.5 »five seven eight NINE " .unindent(), ); let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_5.hunks, &diff_4.hunks, &buffer, &buffer, diff_5.base_text(), diff_5.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_5.base_text()), Point::new(5, 0)..Point::new(5, 0) );
// Edit removes a hunk. buffer.edit_via_marked_text( &" one THREE four FOUR.5 five seven eight «nine» " .unindent(), ); let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_6.hunks, &diff_5.hunks, &buffer, &buffer, diff_6.base_text(), diff_6.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_6.base_text()), Point::new(9, 0)..Point::new(10, 0) );
buffer.edit_via_marked_text( &" one THREE four«» five seven eight «NINE» " .unindent(), );
let diff_7 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_7.hunks, &diff_6.hunks, &buffer, &buffer, diff_7.base_text(), diff_7.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(2, 4)..Point::new(7, 0)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_7.base_text()), Point::new(5, 0)..Point::new(10, 0) );
buffer.edit_via_marked_text( &" one THREE four five«»seven eight NINE " .unindent(), );
let diff_8 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx); let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &diff_8.hunks, &diff_7.hunks, &buffer, &buffer, diff_8.base_text(), diff_8.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(3, 4)); let base_text_range = base_text_changed_range.unwrap(); assert_eq!( base_text_range.to_point(diff_8.base_text()), Point::new(5, 0)..Point::new(8, 0) ); }
#[gpui::test(iterations = 100)] async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) { fn gen_line(rng: &mut StdRng) -> String { if rng.random_bool(0.2) { "\n".to_owned() } else { let c = rng.random_range('A'..='Z'); format!("{c}{c}{c}\n") } }
fn gen_working_copy(rng: &mut StdRng, head: &str) -> String { let mut old_lines = { let mut old_lines = Vec::new(); let old_lines_iter = head.lines(); for line in old_lines_iter { assert!(!line.ends_with("\n")); old_lines.push(line.to_owned()); } if old_lines.last().is_some_and(|line| line.is_empty()) { old_lines.pop(); } old_lines.into_iter() }; let mut result = String::new(); let unchanged_count = rng.random_range(0..=old_lines.len()); result += &old_lines .by_ref() .take(unchanged_count) .fold(String::new(), |mut s, line| { writeln!(&mut s, "{line}").unwrap(); s }); while old_lines.len() > 0 { let deleted_count = rng.random_range(0..=old_lines.len()); let _advance = old_lines .by_ref() .take(deleted_count) .map(|line| line.len() + 1) .sum::<usize>(); let minimum_added = if deleted_count == 0 { 1 } else { 0 }; let added_count = rng.random_range(minimum_added..=5); let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>(); result += &addition;
if old_lines.len() > 0 { let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count(); if blank_lines == old_lines.len() { break; }; let unchanged_count = rng.random_range((blank_lines + 1).max(1)..=old_lines.len()); result += &old_lines.by_ref().take(unchanged_count).fold( String::new(), |mut s, line| { writeln!(&mut s, "{line}").unwrap(); s }, ); } } result }
fn uncommitted_diff( working_copy: &language::BufferSnapshot, index_text: &Rope, head_text: String, cx: &mut TestAppContext, ) -> Entity<BufferDiff> { let secondary = cx.new(|cx| { BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx) }); cx.new(|cx| { let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx); diff.secondary_diff = Some(secondary); diff }) }
let operations = std::env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10);
let rng = &mut rng; let head_text = ('a'..='z').fold(String::new(), |mut s, c| { writeln!(&mut s, "{c}{c}{c}").unwrap(); s }); let working_copy = gen_working_copy(rng, &head_text); let working_copy = cx.new(|cx| { language::Buffer::local_normalized( Rope::from(working_copy.as_str()), text::LineEnding::default(), cx, ) }); let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot()); let mut index_text = if rng.random() { Rope::from(head_text.as_str()) } else { working_copy.as_rope().clone() };
let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx); let mut hunks = diff.update(cx, |diff, cx| { diff.snapshot(cx) .hunks_intersecting_range( Anchor::min_max_range_for_buffer(diff.buffer_id), &working_copy, ) .collect::<Vec<_>>() }); if hunks.is_empty() { return; }
for _ in 0..operations { let i = rng.random_range(0..hunks.len()); let hunk = &mut hunks[i]; let hunk_to_change = hunk.clone(); let stage = match hunk.secondary_status { DiffHunkSecondaryStatus::HasSecondaryHunk => { hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk; true } DiffHunkSecondaryStatus::NoSecondaryHunk => { hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; false } _ => unreachable!(), };
index_text = diff.update(cx, |diff, cx| { diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx) .unwrap() });
diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx); let found_hunks = diff.update(cx, |diff, cx| { diff.snapshot(cx) .hunks_intersecting_range( Anchor::min_max_range_for_buffer(diff.buffer_id), &working_copy, ) .collect::<Vec<_>>() }); assert_eq!(hunks.len(), found_hunks.len());
for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) { assert_eq!( expected_hunk.buffer_range.to_point(&working_copy), found_hunk.buffer_range.to_point(&working_copy) ); assert_eq!( expected_hunk.diff_base_byte_range, found_hunk.diff_base_byte_range ); assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status); } hunks = found_hunks; } }
#[gpui::test] async fn test_changed_ranges(cx: &mut gpui::TestAppContext) { let base_text = " one two three four five six " .unindent(); let buffer_text = " one TWO three four FIVE six " .unindent(); let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx)); let diff = cx.new(|cx| { BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx) }); cx.run_until_parked(); let (tx, rx) = mpsc::channel(); let subscription = cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
let snapshot = buffer.update(cx, |buffer, cx| { buffer.set_text( " ONE TWO THREE FOUR FIVE SIX " .unindent(), cx, ); buffer.text_snapshot() }); let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx)); let update = diff .update(cx, |diff, cx| { diff.update_diff( snapshot.clone(), &base_text_snapshot, Some(Arc::from(base_text_snapshot.text())), cx, ) }) .await; diff.update(cx, |diff, cx| diff.set_snapshot(update, cx)); cx.run_until_parked(); drop(subscription); let events = rx.into_iter().collect::<Vec<_>>(); match events.as_slice() { [ BufferDiffEvent::DiffChanged(DiffChanged { changed_range: _, base_text_changed_range, extended_range: _, base_text_changed: _, }), ] => { // TODO(cole) this seems like it should pass but currently fails (see compare_hunks) // assert_eq!( // *changed_range, // Some(Anchor::min_max_range_for_buffer( // buffer.read_with(cx, |buffer, _| buffer.remote_id()) // )) // ); assert_eq!(*base_text_changed_range, Some(0..base_text.len())); } _ => panic!("unexpected events: {:?}", events), } }
#[gpui::test] async fn test_extended_range(cx: &mut TestAppContext) { let base_text = " aaa bbb
ccc ddd " .unindent();
let buffer_text = " aaa bbb
CCC ddd " .unindent();
let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let old_buffer = buffer.snapshot().clone(); let diff_a = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")]); let diff_b = BufferDiffSnapshot::new_sync(&buffer, base_text, cx);
let DiffChanged { changed_range, base_text_changed_range: _, extended_range, base_text_changed: _, } = compare_hunks( &diff_b.hunks, &diff_a.hunks, &old_buffer, &buffer, &diff_a.base_text(), &diff_a.base_text(), );
let changed_range = changed_range.unwrap(); assert_eq!( changed_range.to_point(&buffer), Point::new(7, 0)..Point::new(9, 0), "changed_range should span from old hunk position to new hunk end" );
let extended_range = extended_range.unwrap(); assert_eq!( extended_range.start.to_point(&buffer), Point::new(1, 3), "extended_range.start should extend to include the edit outside changed_range" ); assert_eq!( extended_range.end.to_point(&buffer), Point::new(9, 0), "extended_range.end should collapse to changed_range.end when no edits in end margin" );
let base_text_2 = " one two three four five six seven eight " .unindent();
let buffer_text_2 = " ONE two THREE four FIVE six SEVEN eight " .unindent();
let mut buffer_2 = Buffer::new(ReplicaId::LOCAL, BufferId::new(2).unwrap(), buffer_text_2); let old_buffer_2 = buffer_2.snapshot().clone(); let diff_2a = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2.clone(), cx);
buffer_2.edit([(Point::new(4, 0)..Point::new(4, 4), "FIVE_CHANGED")]); let diff_2b = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2, cx);
let DiffChanged { changed_range, base_text_changed_range: _, extended_range, base_text_changed: _, } = compare_hunks( &diff_2b.hunks, &diff_2a.hunks, &old_buffer_2, &buffer_2, &diff_2a.base_text(), &diff_2a.base_text(), );
let changed_range = changed_range.unwrap(); assert_eq!( changed_range.to_point(&buffer_2), Point::new(4, 0)..Point::new(5, 0), "changed_range should be just the hunk that changed (FIVE)" );
let extended_range = extended_range.unwrap(); assert_eq!( extended_range.to_point(&buffer_2), Point::new(4, 0)..Point::new(5, 0), "extended_range should equal changed_range when edit is within the hunk" ); }
#[gpui::test] async fn test_buffer_diff_compare_with_base_text_change(_cx: &mut TestAppContext) { // Use a shared base text buffer so that anchors from old and new snapshots // share the same remote_id and resolve correctly across versions. let initial_base = "aaa\nbbb\nccc\nddd\neee\n"; let mut base_text_buffer = Buffer::new( ReplicaId::LOCAL, BufferId::new(99).unwrap(), initial_base.to_string(), );
// --- Scenario 1: Base text gains a line, producing a new deletion hunk --- // // Buffer has a modification (ccc → CCC). When the base text gains // a new line "XXX" after "aaa", the diff now also contains a // deletion for that line, and the modification hunk shifts in the // base text. let buffer_text_1 = "aaa\nbbb\nCCC\nddd\neee\n"; let buffer = Buffer::new( ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1.to_string(), );
let old_base_snapshot_1 = base_text_buffer.snapshot().clone(); let old_hunks_1 = compute_hunks( Some((Arc::from(initial_base), Rope::from(initial_base))), buffer.snapshot(), None, );
// Insert "XXX\n" after "aaa\n" in the base text. base_text_buffer.edit([(4..4, "XXX\n")]); let new_base_str_1: Arc<str> = Arc::from(base_text_buffer.text().as_str()); let new_base_snapshot_1 = base_text_buffer.snapshot();
let new_hunks_1 = compute_hunks( Some((new_base_str_1.clone(), Rope::from(new_base_str_1.as_ref()))), buffer.snapshot(), None, );
let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &new_hunks_1, &old_hunks_1, &buffer.snapshot(), &buffer.snapshot(), &old_base_snapshot_1, &new_base_snapshot_1, );
// The new deletion hunk (XXX) starts at buffer row 1 and the // modification hunk (ccc → CCC) now has a different // diff_base_byte_range, so the changed range spans both. let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(3, 0),); let base_range = base_text_changed_range.unwrap(); assert_eq!( base_range.to_point(&new_base_snapshot_1), Point::new(1, 0)..Point::new(4, 0), );
// --- Scenario 2: Base text changes to match the buffer (hunk disappears) --- // // Start fresh with a simple base text. let simple_base = "one\ntwo\nthree\n"; let mut base_buf_2 = Buffer::new( ReplicaId::LOCAL, BufferId::new(100).unwrap(), simple_base.to_string(), );
let buffer_text_2 = "one\nTWO\nthree\n"; let buffer_2 = Buffer::new( ReplicaId::LOCAL, BufferId::new(2).unwrap(), buffer_text_2.to_string(), );
let old_base_snapshot_2 = base_buf_2.snapshot().clone(); let old_hunks_2 = compute_hunks( Some((Arc::from(simple_base), Rope::from(simple_base))), buffer_2.snapshot(), None, );
// The base text is edited so "two" becomes "TWO", now matching the buffer. base_buf_2.edit([(4..7, "TWO")]); let new_base_str_2: Arc<str> = Arc::from(base_buf_2.text().as_str()); let new_base_snapshot_2 = base_buf_2.snapshot();
let new_hunks_2 = compute_hunks( Some((new_base_str_2.clone(), Rope::from(new_base_str_2.as_ref()))), buffer_2.snapshot(), None, );
let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &new_hunks_2, &old_hunks_2, &buffer_2.snapshot(), &buffer_2.snapshot(), &old_base_snapshot_2, &new_base_snapshot_2, );
// The old modification hunk (two → TWO) is now gone because the // base text matches the buffer. The changed range covers where the // old hunk used to be. let range = changed_range.unwrap(); assert_eq!( range.to_point(&buffer_2), Point::new(1, 0)..Point::new(2, 0), ); let base_range = base_text_changed_range.unwrap(); // The old hunk's diff_base_byte_range covered "two\n" (bytes 4..8). // anchor_after(4) is right-biased at the start of the deleted "two", // so after the edit replacing "two" with "TWO" it resolves past the // insertion to Point(1, 3). assert_eq!( base_range.to_point(&new_base_snapshot_2), Point::new(1, 3)..Point::new(2, 0), );
// --- Scenario 3: Base text edit changes one hunk but not another --- // // Two modification hunks exist. Only one of them is resolved by // the base text change; the other remains identical. let base_3 = "aaa\nbbb\nccc\nddd\neee\n"; let mut base_buf_3 = Buffer::new( ReplicaId::LOCAL, BufferId::new(101).unwrap(), base_3.to_string(), );
let buffer_text_3 = "aaa\nBBB\nccc\nDDD\neee\n"; let buffer_3 = Buffer::new( ReplicaId::LOCAL, BufferId::new(3).unwrap(), buffer_text_3.to_string(), );
let old_base_snapshot_3 = base_buf_3.snapshot().clone(); let old_hunks_3 = compute_hunks( Some((Arc::from(base_3), Rope::from(base_3))), buffer_3.snapshot(), None, );
// Change "ddd" to "DDD" in the base text so that hunk disappears, // but "bbb" stays, so its hunk remains. base_buf_3.edit([(12..15, "DDD")]); let new_base_str_3: Arc<str> = Arc::from(base_buf_3.text().as_str()); let new_base_snapshot_3 = base_buf_3.snapshot();
let new_hunks_3 = compute_hunks( Some((new_base_str_3.clone(), Rope::from(new_base_str_3.as_ref()))), buffer_3.snapshot(), None, );
let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &new_hunks_3, &old_hunks_3, &buffer_3.snapshot(), &buffer_3.snapshot(), &old_base_snapshot_3, &new_base_snapshot_3, );
// Only the second hunk (ddd → DDD) disappeared; the first hunk // (bbb → BBB) is unchanged, so the changed range covers only line 3. let range = changed_range.unwrap(); assert_eq!( range.to_point(&buffer_3), Point::new(3, 0)..Point::new(4, 0), ); let base_range = base_text_changed_range.unwrap(); // anchor_after(12) is right-biased at the start of deleted "ddd", // so after the edit replacing "ddd" with "DDD" it resolves past // the insertion to Point(3, 3). assert_eq!( base_range.to_point(&new_base_snapshot_3), Point::new(3, 3)..Point::new(4, 0), );
// --- Scenario 4: Both buffer and base text change simultaneously --- // // The buffer gains an edit that introduces a new hunk while the // base text also changes. let base_4 = "alpha\nbeta\ngamma\ndelta\n"; let mut base_buf_4 = Buffer::new( ReplicaId::LOCAL, BufferId::new(102).unwrap(), base_4.to_string(), );
let buffer_text_4 = "alpha\nBETA\ngamma\ndelta\n"; let mut buffer_4 = Buffer::new( ReplicaId::LOCAL, BufferId::new(4).unwrap(), buffer_text_4.to_string(), );
let old_base_snapshot_4 = base_buf_4.snapshot().clone(); let old_buffer_snapshot_4 = buffer_4.snapshot().clone(); let old_hunks_4 = compute_hunks( Some((Arc::from(base_4), Rope::from(base_4))), buffer_4.snapshot(), None, );
// Edit the buffer: change "delta" to "DELTA" (new modification hunk). buffer_4.edit_via_marked_text( &" alpha BETA gamma «DELTA» " .unindent(), );
// Edit the base text: change "beta" to "BETA" (resolves that hunk). base_buf_4.edit([(6..10, "BETA")]); let new_base_str_4: Arc<str> = Arc::from(base_buf_4.text().as_str()); let new_base_snapshot_4 = base_buf_4.snapshot();
let new_hunks_4 = compute_hunks( Some((new_base_str_4.clone(), Rope::from(new_base_str_4.as_ref()))), buffer_4.snapshot(), None, );
let DiffChanged { changed_range, base_text_changed_range, extended_range: _, base_text_changed: _, } = compare_hunks( &new_hunks_4, &old_hunks_4, &old_buffer_snapshot_4, &buffer_4.snapshot(), &old_base_snapshot_4, &new_base_snapshot_4, );
// The old BETA hunk (line 1) is gone and a new DELTA hunk (line 3) // appeared, so the changed range spans from line 1 through line 4. let range = changed_range.unwrap(); assert_eq!( range.to_point(&buffer_4), Point::new(1, 0)..Point::new(4, 0), ); let base_range = base_text_changed_range.unwrap(); // The old BETA hunk's base range started at byte 6 ("beta"). After // the base text edit replacing "beta" with "BETA", anchor_after(6) // resolves past the insertion to Point(1, 4). assert_eq!( base_range.to_point(&new_base_snapshot_4), Point::new(1, 4)..Point::new(4, 0), ); }
#[gpui::test(iterations = 100)] async fn test_patch_for_range_random(cx: &mut TestAppContext, mut rng: StdRng) { fn gen_line(rng: &mut StdRng) -> String { if rng.random_bool(0.2) { "\n".to_owned() } else { let c = rng.random_range('A'..='Z'); format!("{c}{c}{c}\n") } }
fn gen_text(rng: &mut StdRng, line_count: usize) -> String { (0..line_count).map(|_| gen_line(rng)).collect() }
fn gen_edits_from(rng: &mut StdRng, base: &str) -> String { let mut old_lines: Vec<&str> = base.lines().collect(); let mut result = String::new();
while !old_lines.is_empty() { let unchanged_count = rng.random_range(0..=old_lines.len()); for _ in 0..unchanged_count { if old_lines.is_empty() { break; } result.push_str(old_lines.remove(0)); result.push('\n'); }
if old_lines.is_empty() { break; }
let deleted_count = rng.random_range(0..=old_lines.len().min(3)); for _ in 0..deleted_count { if old_lines.is_empty() { break; } old_lines.remove(0); }
let minimum_added = if deleted_count == 0 { 1 } else { 0 }; let added_count = rng.random_range(minimum_added..=3); for _ in 0..added_count { result.push_str(&gen_line(rng)); } }
result }
fn random_point_in_text(rng: &mut StdRng, lines: &[&str]) -> Point { if lines.is_empty() { return Point::zero(); } let row = rng.random_range(0..lines.len() as u32); let line = lines[row as usize]; let col = if line.is_empty() { 0 } else { rng.random_range(0..=line.len() as u32) }; Point::new(row, col) }
fn random_range_in_text(rng: &mut StdRng, lines: &[&str]) -> RangeInclusive<Point> { let start = random_point_in_text(rng, lines); let end = random_point_in_text(rng, lines); if start <= end { start..=end } else { end..=start } }
fn points_in_range(range: &RangeInclusive<Point>, lines: &[&str]) -> Vec<Point> { let mut points = Vec::new(); for row in range.start().row..=range.end().row { if row as usize >= lines.len() { points.push(Point::new(row, 0)); continue; } let line = lines[row as usize]; let start_col = if row == range.start().row { range.start().column } else { 0 }; let end_col = if row == range.end().row { range.end().column } else { line.len() as u32 }; for col in start_col..=end_col { points.push(Point::new(row, col)); } } points }
let rng = &mut rng;
let line_count = rng.random_range(5..20); let base_text = gen_text(rng, line_count); let initial_buffer_text = gen_edits_from(rng, &base_text);
let mut buffer = Buffer::new( ReplicaId::LOCAL, BufferId::new(1).unwrap(), initial_buffer_text.clone(), );
let diff = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
let edit_count = rng.random_range(1..=5); for _ in 0..edit_count { let buffer_text = buffer.text(); if buffer_text.is_empty() { buffer.edit([(0..0, gen_line(rng))]); } else { let lines: Vec<&str> = buffer_text.lines().collect(); let start_row = rng.random_range(0..lines.len()); let end_row = rng.random_range(start_row..=lines.len().min(start_row + 3));
let start_col = if start_row < lines.len() { rng.random_range(0..=lines[start_row].len()) } else { 0 }; let end_col = if end_row < lines.len() { rng.random_range(0..=lines[end_row].len()) } else { 0 };
let start_offset = buffer .point_to_offset(Point::new(start_row as u32, start_col as u32)) .min(buffer.len()); let end_offset = buffer .point_to_offset(Point::new(end_row as u32, end_col as u32)) .min(buffer.len());
let (start, end) = if start_offset <= end_offset { (start_offset, end_offset) } else { (end_offset, start_offset) };
let new_text = if rng.random_bool(0.3) { String::new() } else { let line_count = rng.random_range(0..=2); gen_text(rng, line_count) };
buffer.edit([(start..end, new_text)]); } }
let buffer_snapshot = buffer.snapshot();
let buffer_text = buffer_snapshot.text(); let buffer_lines: Vec<&str> = buffer_text.lines().collect(); let base_lines: Vec<&str> = base_text.lines().collect();
let test_count = 10; for _ in 0..test_count { let range = random_range_in_text(rng, &buffer_lines); let points = points_in_range(&range, &buffer_lines);
let optimized_patch = diff.patch_for_buffer_range(range.clone(), &buffer_snapshot); let naive_patch = diff.patch_for_buffer_range_naive(&buffer_snapshot);
for point in points { let optimized_edit = optimized_patch.edit_for_old_position(point); let naive_edit = naive_patch.edit_for_old_position(point);
assert_eq!( optimized_edit, naive_edit, "patch_for_buffer_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}", point, range, base_text, initial_buffer_text, buffer_snapshot.text() ); } }
for _ in 0..test_count { let range = random_range_in_text(rng, &base_lines); let points = points_in_range(&range, &base_lines);
let optimized_patch = diff.patch_for_base_text_range(range.clone(), &buffer_snapshot); let naive_patch = diff.patch_for_base_text_range_naive(&buffer_snapshot);
for point in points { let optimized_edit = optimized_patch.edit_for_old_position(point); let naive_edit = naive_patch.edit_for_old_position(point);
assert_eq!( optimized_edit, naive_edit, "patch_for_base_text_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}", point, range, base_text, initial_buffer_text, buffer_snapshot.text() ); } } }
#[gpui::test] async fn test_set_base_text_with_crlf(cx: &mut gpui::TestAppContext) { let base_text_crlf = "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\n"; let base_text_lf = "one\ntwo\nthree\nfour\nfive\n"; assert_ne!(base_text_crlf.len(), base_text_lf.len());
let buffer_text = "one\nTWO\nthree\nfour\nfive\n"; let buffer = Buffer::new( ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text.to_string(), ); let buffer_snapshot = buffer.snapshot();
let diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, None, None, DiffBaseKind::Head, cx)); diff.update(cx, |diff, cx| { diff.set_base_text(Some(Arc::from(base_text_crlf)), buffer_snapshot.clone(), cx) }) .await; cx.run_until_parked();
let snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx)); snapshot.buffer_point_to_base_text_range(Point::new(0, 0), &buffer_snapshot); snapshot.buffer_point_to_base_text_range(Point::new(1, 0), &buffer_snapshot); }}