Something went wrong. Try again.
fork of gpui
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179pub(super) mod blame;
use super::*;use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind};
#[derive(Clone)]pub struct ResolvedDiffHunk { pub buffer_range: Range<text::Anchor>, pub diff_base_byte_range: Range<usize>, pub status: DiffHunkStatus,}
#[derive(Clone)]pub struct ResolvedDiffHunks { pub diff: Entity<BufferDiff>, pub buffer_id: BufferId, pub buffer: Option<Entity<Buffer>>, pub hunks: Vec<ResolvedDiffHunk>,}
pub trait DiffHunkDelegate { fn toggle( &self, hunks: Vec<ResolvedDiffHunks>, editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>, );
fn stage_or_unstage( &self, stage: bool, hunks: Vec<ResolvedDiffHunks>, editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>, );
fn restore( &self, hunks: Vec<ResolvedDiffHunks>, editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>, ) { if hunks.is_empty() || editor.read_only(cx) { return; } self.stage_or_unstage(false, hunks.clone(), editor, window, cx); editor.transact(window, cx, |editor, window, cx| { editor.restore_diff_hunks(hunks, cx); let selections = editor .selections .all::<MultiBufferOffset>(&editor.display_snapshot(cx)); editor.change_selections( SelectionEffects::no_scroll(), window, cx, |selections_state| { selections_state.select(selections); }, ); }); }
fn render_hunk_controls( &self, row: u32, status: &DiffHunkStatus, hunk_range: Range<Anchor>, is_created_file: bool, line_height: Pixels, editor: &Entity<Editor>, window: &mut Window, cx: &mut App, ) -> AnyElement;
fn render_hunk_as_staged(&self, status: &DiffHunkStatus, _cx: &App) -> bool { !status.has_secondary_hunk() }}
pub struct UncommittedDiffHunkDelegate;
impl DiffHunkDelegate for UncommittedDiffHunkDelegate { fn toggle( &self, hunks: Vec<ResolvedDiffHunks>, editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>, ) { let stage = hunks .iter() .flat_map(|hunks| hunks.hunks.iter()) .any(|hunk| hunk.status.has_secondary_hunk()); self.stage_or_unstage(stage, hunks, editor, window, cx); }
fn stage_or_unstage( &self, stage: bool, hunks: Vec<ResolvedDiffHunks>, editor: &mut Editor, _window: &mut Window, cx: &mut Context<Editor>, ) { let Some(project) = editor.project() else { return; }; for hunks in hunks { let Some(buffer) = hunks.buffer else { continue; };
let ranges = hunks .hunks .into_iter() .map(|hunk| hunk.buffer_range) .collect::<Vec<_>>(); if ranges.is_empty() { continue; } let secondary_diff = hunks.diff.read(cx).secondary_diff(); project .update(cx, |project, cx| { if stage { let Some(secondary_diff) = secondary_diff else { return Err(anyhow::anyhow!("diff has no unstaged secondary")); }; project.stage_hunks(buffer, secondary_diff, ranges, cx) } else { project.unstage_uncommitted_hunks(buffer, hunks.diff, ranges, cx) } }) .log_err(); } }
fn render_hunk_controls( &self, row: u32, status: &DiffHunkStatus, hunk_range: Range<Anchor>, is_created_file: bool, line_height: Pixels, editor: &Entity<Editor>, window: &mut Window, cx: &mut App, ) -> AnyElement { render_diff_hunk_controls( row, status, hunk_range, is_created_file, line_height, editor, window, cx, ) }}
pub struct RestoreOnlyDiffHunkDelegate;
impl DiffHunkDelegate for RestoreOnlyDiffHunkDelegate { fn toggle( &self, _hunks: Vec<ResolvedDiffHunks>, _editor: &mut Editor, _window: &mut Window, _cx: &mut Context<Editor>, ) { }
fn stage_or_unstage( &self, _stage: bool, _hunks: Vec<ResolvedDiffHunks>, _editor: &mut Editor, _window: &mut Window, _cx: &mut Context<Editor>, ) { }
fn restore( &self, _hunks: Vec<ResolvedDiffHunks>, _editor: &mut Editor, _window: &mut Window, _cx: &mut Context<Editor>, ) { }
fn render_hunk_controls( &self, _row: u32, _status: &DiffHunkStatus, _hunk_range: Range<Anchor>, _is_created_file: bool, _line_height: Pixels, _editor: &Entity<Editor>, _window: &mut Window, _cx: &mut App, ) -> AnyElement { gpui::Empty.into_any_element() }}
pub struct RestoreOnlyUnstagedDiffHunkDelegate;
impl DiffHunkDelegate for RestoreOnlyUnstagedDiffHunkDelegate { fn toggle( &self, _hunks: Vec<ResolvedDiffHunks>, _editor: &mut Editor, _window: &mut Window, _cx: &mut Context<Editor>, ) { }
fn stage_or_unstage( &self, _stage: bool, _hunks: Vec<ResolvedDiffHunks>, _editor: &mut Editor, _window: &mut Window, _cx: &mut Context<Editor>, ) { }
fn render_hunk_controls( &self, _row: u32, _status: &DiffHunkStatus, _hunk_range: Range<Anchor>, _is_created_file: bool, _line_height: Pixels, _editor: &Entity<Editor>, _window: &mut Window, _cx: &mut App, ) -> AnyElement { gpui::Empty.into_any_element() }
fn render_hunk_as_staged(&self, _status: &DiffHunkStatus, _cx: &App) -> bool { false }}
#[derive(Debug, Clone, PartialEq, Eq)]pub(super) enum DisplayDiffHunk { Folded { display_row: DisplayRow, }, Unfolded { is_created_file: bool, diff_base_byte_range: Range<usize>, display_row_range: Range<DisplayRow>, multi_buffer_range: Range<Anchor>, status: DiffHunkStatus, word_diffs: Vec<Range<MultiBufferOffset>>, },}
#[derive(Clone)]pub(super) struct InlineBlamePopoverState { pub(super) scroll_handle: ScrollHandle, pub(super) commit_message: Option<ParsedCommitMessage>, pub(super) markdown: Entity<Markdown>,}
pub(super) struct InlineBlamePopover { pub(super) position: gpui::Point<Pixels>, pub(super) hide_task: Option<Task<()>>, pub(super) popover_bounds: Option<Bounds<Pixels>>, pub(super) popover_state: InlineBlamePopoverState, pub(super) keyboard_grace: bool,}
/// Represents a diff review button indicator that shows up when hovering over lines in the gutter/// in diff view mode.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub(super) struct PhantomDiffReviewIndicator { /// The starting anchor of the selection (or the only row if not dragging). pub(super) start: Anchor, /// The ending anchor of the selection. Equal to start_anchor for single-line selection. pub(super) end: Anchor, /// There's a small debounce between hovering over the line and showing the indicator. /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel. pub(super) is_active: bool,}
#[derive(Clone, Debug)]pub(super) struct DiffReviewDragState { start_anchor: Anchor, current_anchor: Anchor,}
/// Identifies a specific hunk in the diff buffer./// Used as a key to group comments by their location.#[derive(Clone, Debug)]pub(super) struct DiffHunkKey { /// The file path (relative to worktree) this hunk belongs to. pub(super) file_path: Arc<util::rel_path::RelPath>, /// An anchor at the start of the hunk. This tracks position as the buffer changes. pub(super) hunk_start_anchor: Anchor,}
/// A review comment stored locally before being sent to the Agent panel.#[derive(Clone)]pub(super) struct StoredReviewComment { /// Unique identifier for this comment (for edit/delete operations). pub(super) id: usize, /// The comment text entered by the user. pub(super) comment: String, /// Anchors for the code range being reviewed. pub(super) range: Range<Anchor>, /// Whether this comment is currently being edited inline. pub(super) is_editing: bool,}
/// Represents an active diff review overlay that appears when clicking the "Add Review" button.pub(super) struct DiffReviewOverlay { pub(super) anchor_range: Range<Anchor>, /// The block ID for the overlay. pub(super) block_id: CustomBlockId, /// The editor entity for the review input. pub(super) prompt_editor: Entity<Editor>, /// The hunk key this overlay belongs to. pub(super) hunk_key: DiffHunkKey, /// Whether the comments section is expanded. pub(super) comments_expanded: bool, /// Editors for comments currently being edited inline. /// Key: comment ID, Value: Editor entity for inline editing. pub(super) inline_edit_editors: HashMap<usize, Entity<Editor>>, /// Subscriptions for inline edit editors' action handlers. /// Key: comment ID, Value: Subscription keeping the Newline action handler alive. pub(super) inline_edit_subscriptions: HashMap<usize, Subscription>, /// The current user's avatar URI for display in comment rows. pub(super) user_avatar_uri: Option<SharedUri>, /// Subscription to keep the action handler alive. _subscription: Subscription,}
impl DiffReviewDragState { pub(super) fn row_range( &self, snapshot: &DisplaySnapshot, ) -> std::ops::RangeInclusive<DisplayRow> { let start = self.start_anchor.to_display_point(snapshot).row(); let current = self.current_anchor.to_display_point(snapshot).row();
(start..=current).sorted() }}
impl StoredReviewComment { fn new(id: usize, comment: String, anchor_range: Range<Anchor>) -> Self { Self { id, comment, range: anchor_range, is_editing: false, } }}
impl Editor { pub fn diff_hunks_in_ranges<'a>( &'a self, ranges: &'a [Range<Anchor>], buffer: &'a MultiBufferSnapshot, ) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> { ranges.iter().flat_map(move |range| { let end_excerpt = buffer.excerpt_containing(range.end..range.end); let range = range.to_point(buffer); let mut peek_end = range.end; if range.end.row < buffer.max_row().0 { peek_end = Point::new(range.end.row + 1, 0); } buffer .diff_hunks_in_range(range.start..peek_end) .filter(move |hunk| { if let Some((_, excerpt_range)) = &end_excerpt && let Some(end_anchor) = buffer.anchor_in_excerpt(excerpt_range.context.end) && let Some(hunk_end_anchor) = buffer.anchor_in_excerpt(hunk.excerpt_range.context.end) && hunk_end_anchor.cmp(&end_anchor, buffer).is_gt() { false } else { true } }) }) }
fn resolve_diff_hunks( &self, hunks: Vec<MultiBufferDiffHunk>, cx: &App, ) -> Vec<ResolvedDiffHunks> { let multibuffer = self.buffer().read(cx); let chunk_by = hunks.into_iter().chunk_by(|hunk| hunk.buffer_id); let mut resolved = Vec::new();
for (source_buffer_id, hunks) in &chunk_by { let Some(diff) = multibuffer.diff_for(source_buffer_id) else { continue; }; let diff_snapshot = diff.read(cx).snapshot(cx); let main_buffer_id = diff_snapshot.buffer_id(); let buffer = multibuffer.buffer(main_buffer_id).or_else(|| { self.project .as_ref() .and_then(|project| project.read(cx).buffer_for_id(main_buffer_id, cx)) }); let mut resolved_hunks = Vec::new();
for hunk in hunks { if hunk.buffer_id == main_buffer_id { resolved_hunks.push(ResolvedDiffHunk { buffer_range: hunk.buffer_range, diff_base_byte_range: hunk.diff_base_byte_range.start.0 ..hunk.diff_base_byte_range.end.0, status: hunk.status, }); } else { let diff_base_byte_range = hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0; let Some(hunk) = diff_snapshot .hunks_intersecting_base_text_range( diff_base_byte_range.clone(), diff_snapshot.buffer_snapshot(), ) .find(|hunk| hunk.diff_base_byte_range == diff_base_byte_range) else { continue; }; let kind = if hunk.buffer_range.start == hunk.buffer_range.end { DiffHunkStatusKind::Deleted } else if hunk.diff_base_byte_range.is_empty() { DiffHunkStatusKind::Added } else { DiffHunkStatusKind::Modified }; resolved_hunks.push(ResolvedDiffHunk { buffer_range: hunk.buffer_range, diff_base_byte_range: hunk.diff_base_byte_range, status: DiffHunkStatus { kind, secondary: hunk.secondary_status, }, }); } }
if !resolved_hunks.is_empty() { resolved.push(ResolvedDiffHunks { diff, buffer_id: main_buffer_id, buffer, hunks: resolved_hunks, }); } }
resolved }
pub fn diff_hunk_delegate(&self) -> Arc<dyn DiffHunkDelegate> { self.diff_hunk_delegate .clone() .unwrap_or_else(|| Arc::new(UncommittedDiffHunkDelegate)) }
pub fn set_diff_hunk_delegate( &mut self, delegate: Option<Arc<dyn DiffHunkDelegate>>, cx: &mut Context<Self>, ) { let had_delegate = self.diff_hunk_delegate.is_some(); let has_delegate = delegate.is_some(); self.diff_hunk_delegate = delegate;
if !had_delegate && has_delegate { self.load_diff_task.take(); } else if had_delegate && !has_delegate { self.buffer.update(cx, |buffer, cx| { buffer.set_all_diff_hunks_collapsed(cx); });
if let Some(project) = self.project.clone() { self.load_diff_task = Some( self.update_uncommitted_diff_for_buffer( &project, self.buffer.read(cx).all_buffers(), cx, ) .shared(), ); } }
cx.notify(); }
pub fn git_blame_inline_enabled(&self) -> bool { self.git_blame_inline_enabled }
pub fn blame(&self) -> Option<&Entity<GitBlame>> { self.blame.as_ref() }
pub fn active_git_blame_entry(&self, cx: &mut App) -> Option<BlameEntry> { if !self.show_git_blame_inline || self.newest_selection_head_on_empty_line(cx) || !self.has_blame_entries(cx) { return None; }
let blame = self.blame.as_ref()?; let snapshot = self.display_snapshot(cx); let cursor = self.selections.newest::<Point>(&snapshot).head(); let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?;
blame .update(cx, |blame, cx| { blame .blame_for_rows( &[RowInfo { buffer_id: Some(buffer.remote_id()), buffer_row: Some(point.row), ..Default::default() }], cx, ) .next() }) .flatten() .map(|(_, entry)| entry) }
pub fn show_git_blame_gutter(&self) -> bool { self.show_git_blame_gutter }
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) { let ranges: Vec<_> = self .selections .disjoint_anchors() .iter() .map(|s| s.range()) .collect(); self.buffer .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) }
pub fn toggle_git_blame( &mut self, _: &::git::Blame, window: &mut Window, cx: &mut Context<Self>, ) { self.show_git_blame_gutter = !self.show_git_blame_gutter;
if self.show_git_blame_gutter && !self.has_blame_entries(cx) { self.start_git_blame(true, window, cx); }
cx.notify(); }
pub fn toggle_git_blame_inline( &mut self, _: &ToggleGitBlameInline, window: &mut Window, cx: &mut Context<Self>, ) { self.toggle_git_blame_inline_internal(true, window, cx); cx.notify(); }
/// Hides the inline blame popover element, in case it's already visible, or /// interrupts the task meant to show it, in case the task is running. /// /// When `ignore_timeout` is set to `true`, the popover is hidden /// immediately, otherwise it'll be hidden after a short delay. /// /// Returns `true` if the popover was visible and was hidden, `false` /// otherwise. pub fn hide_blame_popover(&mut self, ignore_timeout: bool, cx: &mut Context<Self>) -> bool { self.inline_blame_popover_show_task.take();
if let Some(state) = &mut self.inline_blame_popover { if ignore_timeout { self.inline_blame_popover.take(); cx.notify(); } else { state.hide_task = Some(cx.spawn(async move |editor, cx| { cx.background_executor() .timer(std::time::Duration::from_millis(100)) .await;
editor .update(cx, |editor, cx| { editor.inline_blame_popover.take(); cx.notify(); }) .ok(); })); }
true } else { false } }
pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context<Self>) { if self.read_only(cx) { return; } let selections = self .selections .all(&self.display_snapshot(cx)) .into_iter() .map(|s| s.range()) .collect(); self.restore_hunks_in_ranges(selections, window, cx); }
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> { if let Some(status) = self .addons .iter() .find_map(|(_, addon)| addon.override_status_for_buffer_id(buffer_id, cx)) { return Some(status); } self.project .as_ref()? .read(cx) .status_for_buffer_id(buffer_id, cx) }
pub fn go_to_hunk_before_or_after_position( &mut self, snapshot: &EditorSnapshot, position: Point, direction: Direction, wrap_around: bool, window: &mut Window, cx: &mut Context<Editor>, ) { let row = if direction == Direction::Next { self.hunk_after_position(snapshot, position, wrap_around) .map(|hunk| hunk.row_range.start) } else { self.hunk_before_position(snapshot, position, wrap_around) };
if let Some(row) = row { let destination = Point::new(row.0, 0); let autoscroll = Autoscroll::center();
self.unfold_ranges(&[destination..destination], false, false, cx); self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { s.select_ranges([destination..destination]); }); } }
pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { self.buffer.update(cx, |buffer, cx| { buffer.set_all_diff_hunks_expanded(cx); }); }
pub fn expand_all_diff_hunks( &mut self, _: &ExpandAllDiffHunks, _window: &mut Window, cx: &mut Context<Self>, ) { self.buffer.update(cx, |buffer, cx| { buffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx) }); }
pub fn show_diff_review_overlay( &mut self, display_range: Range<DisplayRow>, window: &mut Window, cx: &mut Context<Self>, ) { let Range { start, end } = display_range.sorted();
let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let editor_snapshot = self.snapshot(window, cx);
// Convert display rows to multibuffer points let start_point = editor_snapshot .display_snapshot .display_point_to_point(start.as_display_point(), Bias::Left); let end_point = editor_snapshot .display_snapshot .display_point_to_point(end.as_display_point(), Bias::Left); let end_multi_buffer_row = MultiBufferRow(end_point.row);
// Create anchor range for the selected lines (start of first line to end of last line) let line_end = Point::new( end_point.row, buffer_snapshot.line_len(end_multi_buffer_row), ); let anchor_range = buffer_snapshot.anchor_after(start_point)..buffer_snapshot.anchor_before(line_end);
// Compute the hunk key for this display row let file_path = buffer_snapshot .file_at(start_point) .map(|file: &Arc<dyn language::File>| file.path().clone()) .unwrap_or_else(|| Arc::from(util::rel_path::RelPath::empty())); let hunk_start_anchor = buffer_snapshot.anchor_before(start_point); let new_hunk_key = DiffHunkKey { file_path, hunk_start_anchor, };
// Check if we already have an overlay for this hunk if let Some(existing_overlay) = self.diff_review_overlays.iter().find(|overlay| { Self::hunk_keys_match(&overlay.hunk_key, &new_hunk_key, &buffer_snapshot) }) { // Just focus the existing overlay's prompt editor let focus_handle = existing_overlay.prompt_editor.focus_handle(cx); window.focus(&focus_handle, cx); return; }
// Dismiss overlays that have no comments for their hunks self.dismiss_overlays_without_comments(cx);
// Get the current user's avatar URI from the project's user_store let user_avatar_uri = self.project.as_ref().and_then(|project| { let user_store = project.read(cx).user_store(); user_store .read(cx) .current_user() .map(|user| user.avatar_uri.clone()) });
// Create anchor at the end of the last row so the block appears immediately below it // Use multibuffer coordinates for anchor creation let line_len = buffer_snapshot.line_len(end_multi_buffer_row); let anchor = buffer_snapshot.anchor_after(Point::new(end_multi_buffer_row.0, line_len));
// Use the hunk key we already computed let hunk_key = new_hunk_key;
// Create the prompt editor for the review input let prompt_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Add a review comment...", window, cx); editor });
// Register the Newline action on the prompt editor to submit the review let parent_editor = cx.entity().downgrade(); let subscription = prompt_editor.update(cx, |prompt_editor, _cx| { prompt_editor.register_action({ let parent_editor = parent_editor.clone(); move |_: &crate::actions::Newline, window, cx| { if let Some(editor) = parent_editor.upgrade() { editor.update(cx, |editor, cx| { editor.submit_diff_review_comment(window, cx); }); } } }) });
// Calculate initial height based on existing comments for this hunk let initial_height = self.calculate_overlay_height(&hunk_key, true, &buffer_snapshot);
// Create the overlay block let prompt_editor_for_render = prompt_editor.clone(); let hunk_key_for_render = hunk_key.clone(); let editor_handle = cx.entity().downgrade(); let block = BlockProperties { style: BlockStyle::Sticky, placement: BlockPlacement::Below(anchor), height: Some(initial_height), render: Arc::new(move |cx| { Self::render_diff_review_overlay( &prompt_editor_for_render, &hunk_key_for_render, &editor_handle, cx, ) }), priority: 0, };
let block_ids = self.insert_blocks([block], None, cx); let Some(block_id) = block_ids.into_iter().next() else { log::error!("Failed to insert diff review overlay block"); return; };
self.diff_review_overlays.push(DiffReviewOverlay { anchor_range, block_id, prompt_editor: prompt_editor.clone(), hunk_key, comments_expanded: true, inline_edit_editors: HashMap::default(), inline_edit_subscriptions: HashMap::default(), user_avatar_uri, _subscription: subscription, });
// Focus the prompt editor let focus_handle = prompt_editor.focus_handle(cx); window.focus(&focus_handle, cx);
cx.notify(); }
/// Stores the diff review comment locally. /// Comments are stored per-hunk and can later be batch-submitted to the Agent panel. pub fn submit_diff_review_comment(&mut self, window: &mut Window, cx: &mut Context<Self>) { // Find the overlay that currently has focus let overlay_index = self .diff_review_overlays .iter() .position(|overlay| overlay.prompt_editor.focus_handle(cx).is_focused(window)); let Some(overlay_index) = overlay_index else { return; }; let overlay = &self.diff_review_overlays[overlay_index];
let comment_text = overlay.prompt_editor.read(cx).text(cx).trim().to_string(); if comment_text.is_empty() { return; }
let anchor_range = overlay.anchor_range.clone(); let hunk_key = overlay.hunk_key.clone();
self.add_review_comment(hunk_key.clone(), comment_text, anchor_range, cx);
// Clear the prompt editor but keep the overlay open if let Some(overlay) = self.diff_review_overlays.get(overlay_index) { overlay.prompt_editor.update(cx, |editor, cx| { editor.clear(window, cx); }); }
// Refresh the overlay to update the block height for the new comment self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
cx.notify(); }
/// Returns the prompt editor for the diff review overlay, if one is active. /// This is primarily used for testing. pub fn diff_review_prompt_editor(&self) -> Option<&Entity<Editor>> { self.diff_review_overlays .first() .map(|overlay| &overlay.prompt_editor) }
/// Sets whether the comments section is expanded in the diff review overlay. /// This is primarily used for testing. pub fn set_diff_review_comments_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) { for overlay in &mut self.diff_review_overlays { overlay.comments_expanded = expanded; } cx.notify(); }
/// Returns the total count of stored review comments across all hunks. pub(super) fn total_review_comment_count(&self) -> usize { self.stored_review_comments .iter() .map(|(_, v)| v.len()) .sum() }
/// Adds a new review comment to a specific hunk. pub(super) fn add_review_comment( &mut self, hunk_key: DiffHunkKey, comment: String, anchor_range: Range<Anchor>, cx: &mut Context<Self>, ) -> usize { let id = self.next_review_comment_id; self.next_review_comment_id += 1;
let stored_comment = StoredReviewComment::new(id, comment, anchor_range);
let snapshot = self.buffer.read(cx).snapshot(cx); let key_point = hunk_key.hunk_start_anchor.to_point(&snapshot);
// Find existing entry for this hunk or add a new one if let Some((_, comments)) = self.stored_review_comments.iter_mut().find(|(k, _)| { k.file_path == hunk_key.file_path && k.hunk_start_anchor.to_point(&snapshot) == key_point }) { comments.push(stored_comment); } else { self.stored_review_comments .push((hunk_key, vec![stored_comment])); }
cx.emit(EditorEvent::ReviewCommentsChanged { total_count: self.total_review_comment_count(), }); cx.notify(); id }
pub(super) fn blame_hover( &mut self, _: &BlameHover, window: &mut Window, cx: &mut Context<Self>, ) { let just_started = self.blame.is_none(); if just_started { self.start_git_blame(true, window, cx); } let Some(blame) = self.blame.as_ref() else { return; };
if just_started && !blame.read(cx).has_generated_entries() { let subscription = cx.observe_in(blame, window, |editor, blame, window, cx| { if blame.read(cx).has_generated_entries() { editor.pending_blame_hover_observation.take(); editor.show_blame_hover_popover(window, cx); } }); self.pending_blame_hover_observation = Some(subscription); return; }
self.show_blame_hover_popover(window, cx); }
fn show_blame_hover_popover(&mut self, window: &mut Window, cx: &mut Context<Self>) { let snapshot = self.snapshot(window, cx); let cursor = self .selections .newest::<Point>(&snapshot.display_snapshot) .head(); let Some((buffer, point)) = snapshot.buffer_snapshot().point_to_buffer_point(cursor) else { return; };
let Some(blame) = self.blame.as_ref() else { return; };
let row_info = RowInfo { buffer_id: Some(buffer.remote_id()), buffer_row: Some(point.row), ..Default::default() }; let Some((buffer, blame_entry)) = blame .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next()) .flatten() else { return; };
let anchor = self.selections.newest_anchor().head(); let position = self.to_pixel_point(anchor, &snapshot, window, cx); if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) { self.show_blame_popover( buffer, &blame_entry, position + last_bounds.origin, true, cx, ); }; }
pub(super) fn restore_file( &mut self, _: &::git::RestoreFile, window: &mut Window, cx: &mut Context<Self>, ) { if self.read_only(cx) { return; } let mut buffer_ids = HashSet::default(); let snapshot = self.buffer().read(cx).snapshot(cx); for selection in self .selections .all::<MultiBufferOffset>(&self.display_snapshot(cx)) { buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) }
let ranges = buffer_ids .into_iter() .flat_map(|buffer_id| snapshot.range_for_buffer(buffer_id)) .collect::<Vec<_>>();
self.restore_hunks_in_ranges(ranges, window, cx); }
/// Restores the diff hunks in the editor's selections and moves the cursor /// to the next diff hunk. Wraps around to the beginning of the buffer if /// not all diff hunks are expanded. pub(super) fn restore_and_next( &mut self, _: &::git::RestoreAndNext, window: &mut Window, cx: &mut Context<Self>, ) { if self.read_only(cx) { return; } let selections = self .selections .all(&self.display_snapshot(cx)) .into_iter() .map(|selection| selection.range()) .collect();
self.restore_hunks_in_ranges(selections, window, cx);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); let wrap_around = !all_diff_hunks_expanded; let snapshot = self.snapshot(window, cx); let position = self .selections .newest::<Point>(&snapshot.display_snapshot) .head();
self.go_to_hunk_before_or_after_position( &snapshot, position, Direction::Next, wrap_around, window, cx, ); }
pub fn restore_diff_hunks(&mut self, hunks: Vec<ResolvedDiffHunks>, cx: &mut Context<Self>) { let mut revert_changes = Vec::new(); for hunks in hunks { let Some(buffer) = hunks.buffer else { continue; }; let diff_snapshot = hunks.diff.read(cx).snapshot(cx); let changes = hunks .hunks .into_iter() .filter_map(|hunk| { if hunk.diff_base_byte_range == (0..0) && hunk.buffer_range.start.is_min() && hunk.buffer_range.end.is_max() { return None; } let original_text = diff_snapshot .base_text() .as_rope() .slice(hunk.diff_base_byte_range.start..hunk.diff_base_byte_range.end); Some((hunk.buffer_range, original_text)) }) .collect::<Vec<_>>(); if !changes.is_empty() { revert_changes.push((buffer, changes)); } }
for (buffer, changes) in revert_changes { buffer.update(cx, |buffer, cx| { buffer.edit( changes .into_iter() .map(|(range, text)| (range, text.to_string())), None, cx, ); }); } }
pub(super) fn go_to_next_hunk( &mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>, ) { let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::<Point>(&self.display_snapshot(cx)); self.go_to_hunk_before_or_after_position( &snapshot, selection.head(), Direction::Next, true, window, cx, ); }
pub(super) fn collapse_all_diff_hunks( &mut self, _: &CollapseAllDiffHunks, _window: &mut Window, cx: &mut Context<Self>, ) { self.buffer.update(cx, |buffer, cx| { buffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx) }); }
pub fn toggle_all_diff_hunks( &mut self, _: &ToggleAllDiffHunks, window: &mut Window, cx: &mut Context<Self>, ) { if self.has_any_expanded_diff_hunks(cx) { self.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx); } else { self.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx); } }
pub(super) fn toggle_selected_diff_hunks( &mut self, _: &ToggleSelectedDiffHunks, _window: &mut Window, cx: &mut Context<Self>, ) { let ranges: Vec<_> = self .selections .disjoint_anchors() .iter() .map(|s| s.range()) .collect(); self.toggle_diff_hunks_in_ranges(ranges, cx); }
pub(super) fn show_diff_review_button(&self) -> bool { self.show_diff_review_button }
pub(super) fn render_diff_review_button( &self, display_row: DisplayRow, width: Pixels, cx: &mut Context<Self>, ) -> impl IntoElement { let text_color = cx.theme().colors().text; let icon_color = cx.theme().colors().icon_accent;
h_flex() .id("diff_review_button") .cursor_pointer() .w(width - px(1.)) .h(relative(0.9)) .justify_center() .rounded_sm() .border_1() .border_color(text_color.opacity(0.1)) .bg(text_color.opacity(0.15)) .hover(|s| { s.bg(icon_color.opacity(0.4)) .border_color(icon_color.opacity(0.5)) }) .child(Icon::new(IconName::Plus).size(IconSize::Small)) .tooltip(Tooltip::text("Add Review (drag to select multiple lines)")) .on_mouse_down( gpui::MouseButton::Left, cx.listener(move |editor, _event: &gpui::MouseDownEvent, window, cx| { editor.start_diff_review_drag(display_row, window, cx); }), ) }
pub(super) fn start_diff_review_drag( &mut self, display_row: DisplayRow, window: &mut Window, cx: &mut Context<Self>, ) { let snapshot = self.snapshot(window, cx); let point = snapshot .display_snapshot .display_point_to_point(DisplayPoint::new(display_row, 0), Bias::Left); let anchor = snapshot.buffer_snapshot().anchor_before(point); self.diff_review_drag_state = Some(DiffReviewDragState { start_anchor: anchor, current_anchor: anchor, }); cx.notify(); }
pub(super) fn update_diff_review_drag( &mut self, display_row: DisplayRow, window: &mut Window, cx: &mut Context<Self>, ) { if self.diff_review_drag_state.is_none() { return; } let snapshot = self.snapshot(window, cx); let point = snapshot .display_snapshot .display_point_to_point(display_row.as_display_point(), Bias::Left); let anchor = snapshot.buffer_snapshot().anchor_before(point); if let Some(drag_state) = &mut self.diff_review_drag_state { drag_state.current_anchor = anchor; cx.notify(); } }
pub(super) fn end_diff_review_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) { if let Some(drag_state) = self.diff_review_drag_state.take() { let snapshot = self.snapshot(window, cx); let range = drag_state.row_range(&snapshot.display_snapshot); self.show_diff_review_overlay(*range.start()..*range.end(), window, cx); } cx.notify(); }
pub(super) fn cancel_diff_review_drag(&mut self, cx: &mut Context<Self>) { self.diff_review_drag_state = None; cx.notify(); }
/// Dismisses all diff review overlays. pub(super) fn dismiss_all_diff_review_overlays(&mut self, cx: &mut Context<Self>) { if self.diff_review_overlays.is_empty() { return; } let block_ids: HashSet<_> = self .diff_review_overlays .drain(..) .map(|overlay| overlay.block_id) .collect(); self.remove_blocks(block_ids, None, cx); cx.notify(); }
/// Action handler for SubmitDiffReviewComment. pub(super) fn submit_diff_review_comment_action( &mut self, _: &SubmitDiffReviewComment, window: &mut Window, cx: &mut Context<Self>, ) { self.submit_diff_review_comment(window, cx); }
/// Returns comments for a specific hunk, ordered by creation time. pub(super) fn comments_for_hunk<'a>( &'a self, key: &DiffHunkKey, snapshot: &MultiBufferSnapshot, ) -> &'a [StoredReviewComment] { let key_point = key.hunk_start_anchor.to_point(snapshot); self.stored_review_comments .iter() .find(|(k, _)| { k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point }) .map(|(_, comments)| comments.as_slice()) .unwrap_or(&[]) }
/// Returns the count of comments for a specific hunk. pub(super) fn hunk_comment_count( &self, key: &DiffHunkKey, snapshot: &MultiBufferSnapshot, ) -> usize { let key_point = key.hunk_start_anchor.to_point(snapshot); self.stored_review_comments .iter() .find(|(k, _)| { k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point }) .map(|(_, v)| v.len()) .unwrap_or(0) }
/// Removes a review comment by ID from any hunk. pub(super) fn remove_review_comment(&mut self, id: usize, cx: &mut Context<Self>) -> bool { for (_, comments) in self.stored_review_comments.iter_mut() { if let Some(index) = comments.iter().position(|c| c.id == id) { comments.remove(index); cx.emit(EditorEvent::ReviewCommentsChanged { total_count: self.total_review_comment_count(), }); cx.notify(); return true; } } false }
/// Updates a review comment's text by ID. pub(super) fn update_review_comment( &mut self, id: usize, new_comment: String, cx: &mut Context<Self>, ) -> bool { for (_, comments) in self.stored_review_comments.iter_mut() { if let Some(comment) = comments.iter_mut().find(|c| c.id == id) { comment.comment = new_comment; comment.is_editing = false; cx.emit(EditorEvent::ReviewCommentsChanged { total_count: self.total_review_comment_count(), }); cx.notify(); return true; } } false }
/// Sets a comment's editing state. pub(super) fn set_comment_editing( &mut self, id: usize, is_editing: bool, cx: &mut Context<Self>, ) { for (_, comments) in self.stored_review_comments.iter_mut() { if let Some(comment) = comments.iter_mut().find(|c| c.id == id) { comment.is_editing = is_editing; cx.notify(); return; } } }
/// Removes review comments whose anchors are no longer valid or whose /// associated diff hunks no longer exist. /// /// This should be called when the buffer changes to prevent orphaned comments /// from accumulating. pub(super) fn cleanup_orphaned_review_comments(&mut self, cx: &mut Context<Self>) { let snapshot = self.buffer.read(cx).snapshot(cx); let original_count = self.total_review_comment_count();
// Remove comments with invalid hunk anchors self.stored_review_comments .retain(|(hunk_key, _)| hunk_key.hunk_start_anchor.is_valid(&snapshot));
// Also clean up individual comments with invalid anchor ranges for (_, comments) in &mut self.stored_review_comments { comments.retain(|comment| { comment.range.start.is_valid(&snapshot) && comment.range.end.is_valid(&snapshot) }); }
// Remove empty hunk entries self.stored_review_comments .retain(|(_, comments)| !comments.is_empty());
let new_count = self.total_review_comment_count(); if new_count != original_count { cx.emit(EditorEvent::ReviewCommentsChanged { total_count: new_count, }); cx.notify(); } }
/// Toggles the expanded state of the comments section in the overlay. pub(super) fn toggle_review_comments_expanded( &mut self, _: &ToggleReviewCommentsExpanded, window: &mut Window, cx: &mut Context<Self>, ) { // Find the overlay that currently has focus, or use the first one let overlay_info = self.diff_review_overlays.iter_mut().find_map(|overlay| { if overlay.prompt_editor.focus_handle(cx).is_focused(window) { overlay.comments_expanded = !overlay.comments_expanded; Some(overlay.hunk_key.clone()) } else { None } });
// If no focused overlay found, toggle the first one let hunk_key = overlay_info.or_else(|| { self.diff_review_overlays.first_mut().map(|overlay| { overlay.comments_expanded = !overlay.comments_expanded; overlay.hunk_key.clone() }) });
if let Some(hunk_key) = hunk_key { self.refresh_diff_review_overlay_height(&hunk_key, window, cx); cx.notify(); } }
/// Handles the EditReviewComment action - sets a comment into editing mode. pub(super) fn edit_review_comment( &mut self, action: &EditReviewComment, window: &mut Window, cx: &mut Context<Self>, ) { let comment_id = action.id;
// Set the comment to editing mode self.set_comment_editing(comment_id, true, cx);
// Find the overlay that contains this comment and create an inline editor if needed // First, find which hunk this comment belongs to let hunk_key = self .stored_review_comments .iter() .find_map(|(key, comments)| { if comments.iter().any(|c| c.id == comment_id) { Some(key.clone()) } else { None } });
let snapshot = self.buffer.read(cx).snapshot(cx); if let Some(hunk_key) = hunk_key { if let Some(overlay) = self .diff_review_overlays .iter_mut() .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot)) { if let std::collections::hash_map::Entry::Vacant(entry) = overlay.inline_edit_editors.entry(comment_id) { // Find the comment text let comment_text = self .stored_review_comments .iter() .flat_map(|(_, comments)| comments) .find(|c| c.id == comment_id) .map(|c| c.comment.clone()) .unwrap_or_default();
// Create inline editor let parent_editor = cx.entity().downgrade(); let inline_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_text(&*comment_text, window, cx); // Select all text for easy replacement editor.select_all(&crate::actions::SelectAll, window, cx); editor });
// Register the Newline action to confirm the edit let subscription = inline_editor.update(cx, |inline_editor, _cx| { inline_editor.register_action({ let parent_editor = parent_editor.clone(); move |_: &crate::actions::Newline, window, cx| { if let Some(editor) = parent_editor.upgrade() { editor.update(cx, |editor, cx| { editor.confirm_edit_review_comment(comment_id, window, cx); }); } } }) });
// Store the subscription to keep the action handler alive overlay .inline_edit_subscriptions .insert(comment_id, subscription);
// Focus the inline editor let focus_handle = inline_editor.focus_handle(cx); window.focus(&focus_handle, cx);
entry.insert(inline_editor); } } }
cx.notify(); }
/// Confirms an inline edit of a review comment. pub(super) fn confirm_edit_review_comment( &mut self, comment_id: usize, _window: &mut Window, cx: &mut Context<Self>, ) { // Get the new text from the inline editor // Find the overlay containing this comment's inline editor let snapshot = self.buffer.read(cx).snapshot(cx); let hunk_key = self .stored_review_comments .iter() .find_map(|(key, comments)| { if comments.iter().any(|c| c.id == comment_id) { Some(key.clone()) } else { None } });
let new_text = hunk_key .as_ref() .and_then(|hunk_key| { self.diff_review_overlays .iter() .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot)) }) .as_ref() .and_then(|overlay| overlay.inline_edit_editors.get(&comment_id)) .map(|editor| editor.read(cx).text(cx).trim().to_string());
if let Some(new_text) = new_text { if !new_text.is_empty() { self.update_review_comment(comment_id, new_text, cx); } }
// Remove the inline editor and its subscription if let Some(hunk_key) = hunk_key { if let Some(overlay) = self .diff_review_overlays .iter_mut() .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot)) { overlay.inline_edit_editors.remove(&comment_id); overlay.inline_edit_subscriptions.remove(&comment_id); } }
// Clear editing state self.set_comment_editing(comment_id, false, cx); }
/// Cancels an inline edit of a review comment. pub(super) fn cancel_edit_review_comment( &mut self, comment_id: usize, _window: &mut Window, cx: &mut Context<Self>, ) { // Find which hunk this comment belongs to let hunk_key = self .stored_review_comments .iter() .find_map(|(key, comments)| { if comments.iter().any(|c| c.id == comment_id) { Some(key.clone()) } else { None } });
// Remove the inline editor and its subscription if let Some(hunk_key) = hunk_key { let snapshot = self.buffer.read(cx).snapshot(cx); if let Some(overlay) = self .diff_review_overlays .iter_mut() .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot)) { overlay.inline_edit_editors.remove(&comment_id); overlay.inline_edit_subscriptions.remove(&comment_id); } }
// Clear editing state self.set_comment_editing(comment_id, false, cx); }
/// Action handler for ConfirmEditReviewComment. pub(super) fn confirm_edit_review_comment_action( &mut self, action: &ConfirmEditReviewComment, window: &mut Window, cx: &mut Context<Self>, ) { self.confirm_edit_review_comment(action.id, window, cx); }
/// Action handler for CancelEditReviewComment. pub(super) fn cancel_edit_review_comment_action( &mut self, action: &CancelEditReviewComment, window: &mut Window, cx: &mut Context<Self>, ) { self.cancel_edit_review_comment(action.id, window, cx); }
/// Handles the DeleteReviewComment action - removes a comment. pub(super) fn delete_review_comment( &mut self, action: &DeleteReviewComment, window: &mut Window, cx: &mut Context<Self>, ) { // Get the hunk key before removing the comment // Find the hunk key from the comment itself let comment_id = action.id; let hunk_key = self .stored_review_comments .iter() .find_map(|(key, comments)| { if comments.iter().any(|c| c.id == comment_id) { Some(key.clone()) } else { None } });
// Also get it from the overlay for refresh purposes let overlay_hunk_key = self .diff_review_overlays .first() .map(|o| o.hunk_key.clone());
self.remove_review_comment(action.id, cx);
// Refresh the overlay height after removing a comment if let Some(hunk_key) = hunk_key.or(overlay_hunk_key) { self.refresh_diff_review_overlay_height(&hunk_key, window, cx); } }
pub(super) fn copy_permalink_to_line( &mut self, _: &CopyPermalinkToLine, window: &mut Window, cx: &mut Context<Self>, ) { let permalink_task = self.get_permalink_to_line(cx); let workspace = self.workspace();
cx.spawn_in(window, async move |_, cx| match permalink_task.await { Ok(permalink) => { cx.update(|_, cx| { cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); }) .ok(); } Err(err) => { let message = format!("Failed to copy permalink: {err}");
anyhow::Result::<()>::Err(err).log_err();
if let Some(workspace) = workspace { workspace .update_in(cx, |workspace, _, cx| { struct CopyPermalinkToLine;
workspace.show_toast( Toast::new( NotificationId::unique::<CopyPermalinkToLine>(), message, ), cx, ) }) .ok(); } } }) .detach(); }
pub(super) fn open_permalink_to_line( &mut self, _: &OpenPermalinkToLine, window: &mut Window, cx: &mut Context<Self>, ) { let permalink_task = self.get_permalink_to_line(cx); let workspace = self.workspace();
cx.spawn_in(window, async move |_, cx| match permalink_task.await { Ok(permalink) => { cx.update(|_, cx| { cx.open_url(permalink.as_ref()); }) .ok(); } Err(err) => { let message = format!("Failed to open permalink: {err}");
anyhow::Result::<()>::Err(err).log_err();
if let Some(workspace) = workspace { workspace.update(cx, |workspace, cx| { struct OpenPermalinkToLine;
workspace.show_toast( Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message), cx, ) }); } } }) .detach(); }
pub(super) fn toggle_staged_selected_diff_hunks( &mut self, _: &::git::ToggleStaged, window: &mut Window, cx: &mut Context<Self>, ) { let ranges: Vec<_> = self .selections .disjoint_anchors() .iter() .map(|s| s.range()) .collect(); let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); cx.spawn_in(window, async move |this, cx| { task.await?; this.update_in(cx, |this, window, cx| { let snapshot = this.buffer.read(cx).snapshot(cx); let hunks = this.diff_hunks_in_ranges(&ranges, &snapshot).collect(); this.apply_toggle(hunks, window, cx); }) }) .detach_and_log_err(cx); }
pub(super) fn stage_and_next( &mut self, _: &::git::StageAndNext, window: &mut Window, cx: &mut Context<Self>, ) { self.do_stage_or_unstage_and_next(true, window, cx); }
pub(super) fn unstage_and_next( &mut self, _: &::git::UnstageAndNext, window: &mut Window, cx: &mut Context<Self>, ) { self.do_stage_or_unstage_and_next(false, window, cx); }
pub fn apply_toggle( &mut self, hunks: Vec<MultiBufferDiffHunk>, window: &mut Window, cx: &mut Context<Self>, ) { let mut hunks = self.resolve_diff_hunks(hunks, cx); if self.diff_hunk_delegate.is_none() { hunks.retain(|hunks| hunks.diff.read(cx).is_stageable()); } if hunks.is_empty() { return; } let delegate = self.diff_hunk_delegate(); delegate.toggle(hunks, self, window, cx); }
pub fn apply_stage_or_unstage( &mut self, stage: bool, hunks: Vec<MultiBufferDiffHunk>, window: &mut Window, cx: &mut Context<Self>, ) { let mut hunks = self.resolve_diff_hunks(hunks, cx); if self.diff_hunk_delegate.is_none() { hunks.retain(|hunks| hunks.diff.read(cx).is_stageable()); } if hunks.is_empty() { return; } let delegate = self.diff_hunk_delegate(); delegate.stage_or_unstage(stage, hunks, self, window, cx); }
pub fn apply_restore( &mut self, hunks: Vec<MultiBufferDiffHunk>, window: &mut Window, cx: &mut Context<Self>, ) { let mut hunks = self.resolve_diff_hunks(hunks, cx); if self.diff_hunk_delegate.is_none() { hunks.retain(|hunks| hunks.diff.read(cx).is_stageable()); } if hunks.is_empty() { return; } let delegate = self.diff_hunk_delegate(); delegate.restore(hunks, self, window, cx); }
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context<Self>) -> bool { self.buffer.update(cx, |buffer, cx| { let ranges = vec![Anchor::Min..Anchor::Max]; if !buffer.all_diff_hunks_expanded() && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) { buffer.collapse_diff_hunks(ranges, cx); true } else { false } }) }
pub(super) fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool { if self.buffer.read(cx).all_diff_hunks_expanded() { return true; } let ranges = vec![Anchor::Min..Anchor::Max]; self.buffer .read(cx) .has_expanded_diff_hunks_in_ranges(&ranges, cx) }
pub(super) fn toggle_single_diff_hunk(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) { self.buffer.update(cx, |buffer, cx| { buffer.toggle_single_diff_hunk(range, cx); }) }
pub(super) fn apply_all_diff_hunks( &mut self, _: &ApplyAllDiffHunks, window: &mut Window, cx: &mut Context<Self>, ) { if self.read_only(cx) { return; }
let buffers = self.buffer.read(cx).all_buffers(); for branch_buffer in buffers { branch_buffer.update(cx, |branch_buffer, cx| { branch_buffer.merge_into_base(Vec::new(), cx); }); }
if let Some(project) = self.project.clone() { self.save( SaveOptions { format: true, force_format: false, autosave: false, }, project, window, cx, ) .detach_and_log_err(cx); } }
pub(super) fn apply_selected_diff_hunks( &mut self, _: &ApplyDiffHunk, window: &mut Window, cx: &mut Context<Self>, ) { if self.read_only(cx) { return; } let snapshot = self.snapshot(window, cx); let hunks = snapshot.hunks_for_ranges( self.selections .all(&snapshot.display_snapshot) .into_iter() .map(|selection| selection.range()), ); let mut ranges_by_buffer = HashMap::default(); self.transact(window, cx, |editor, _window, cx| { for hunk in hunks { if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { ranges_by_buffer .entry(buffer.clone()) .or_insert_with(Vec::new) .push(hunk.buffer_range.to_offset(buffer.read(cx))); } }
for (buffer, ranges) in ranges_by_buffer { buffer.update(cx, |buffer, cx| { buffer.merge_into_base(ranges, cx); }); } });
if let Some(project) = self.project.clone() { self.save( SaveOptions { format: true, force_format: false, autosave: false, }, project, window, cx, ) .detach_and_log_err(cx); } }
pub(super) fn open_git_blame_commit( &mut self, _: &OpenGitBlameCommit, window: &mut Window, cx: &mut Context<Self>, ) { self.open_git_blame_commit_internal(window, cx); }
pub(super) fn toggle_git_blame_inline_internal( &mut self, user_triggered: bool, window: &mut Window, cx: &mut Context<Self>, ) { if self.git_blame_inline_enabled { self.git_blame_inline_enabled = false; self.show_git_blame_inline = false; self.show_git_blame_inline_delay_task.take(); } else { self.git_blame_inline_enabled = true; self.start_git_blame_inline(user_triggered, window, cx); }
cx.notify(); }
pub(super) fn start_git_blame_inline( &mut self, user_triggered: bool, window: &mut Window, cx: &mut Context<Self>, ) { self.start_git_blame(user_triggered, window, cx);
if ProjectSettings::get_global(cx) .git .inline_blame_delay() .is_some() { self.start_inline_blame_timer(window, cx); } else { self.show_git_blame_inline = true } }
pub(super) fn render_git_blame_gutter(&self, cx: &App) -> bool { !self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx) }
pub(super) fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { ProjectSettings::get_global(cx).git.inline_blame.location == project::project_settings::InlineBlameLocation::Inline && self.show_git_blame_inline && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some()) && !self.newest_selection_head_on_empty_line(cx) && self.has_blame_entries(cx) }
pub(super) fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) { if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() { self.show_git_blame_inline = false;
self.show_git_blame_inline_delay_task = Some(cx.spawn_in(window, async move |this, cx| { cx.background_executor().timer(delay).await;
this.update(cx, |this, cx| { this.show_git_blame_inline = true; cx.notify(); }) .log_err(); })); } }
pub(super) fn show_blame_popover( &mut self, buffer: BufferId, blame_entry: &BlameEntry, position: gpui::Point<Pixels>, ignore_timeout: bool, cx: &mut Context<Self>, ) { if let Some(state) = &mut self.inline_blame_popover { state.hide_task.take(); } else { let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0; let blame_entry = blame_entry.clone(); let show_task = cx.spawn(async move |editor, cx| { if !ignore_timeout { cx.background_executor() .timer(std::time::Duration::from_millis(blame_popover_delay)) .await; } editor .update(cx, |editor, cx| { editor.inline_blame_popover_show_task.take(); let Some(blame) = editor.blame.as_ref() else { return; }; let blame = blame.read(cx); let details = blame.details_for_entry(buffer, &blame_entry); let markdown = cx.new(|cx| { Markdown::new( details .as_ref() .map(|message| message.message.clone()) .unwrap_or_default(), None, None, cx, ) }); editor.inline_blame_popover = Some(InlineBlamePopover { position, hide_task: None, popover_bounds: None, popover_state: InlineBlamePopoverState { scroll_handle: ScrollHandle::new(), commit_message: details, markdown, }, keyboard_grace: ignore_timeout, }); cx.notify(); }) .ok(); }); self.inline_blame_popover_show_task = Some(show_task); } }
pub(super) fn go_to_prev_hunk( &mut self, _: &GoToPreviousHunk, window: &mut Window, cx: &mut Context<Self>, ) { let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::<Point>(&snapshot.display_snapshot); self.go_to_hunk_before_or_after_position( &snapshot, selection.head(), Direction::Prev, true, window, cx, ); }
/// Calculates the appropriate block height for the diff review overlay. /// Height is in lines: 2 for input row, 1 for header when comments exist, /// and 2 lines per comment when expanded. pub(super) fn calculate_overlay_height( &self, hunk_key: &DiffHunkKey, comments_expanded: bool, snapshot: &MultiBufferSnapshot, ) -> u32 { let comment_count = self.hunk_comment_count(hunk_key, snapshot); let base_height: u32 = 2; // Input row with avatar and buttons
if comment_count == 0 { base_height } else if comments_expanded { // Header (1 line) + 2 lines per comment base_height + 1 + (comment_count as u32 * 2) } else { // Just header when collapsed base_height + 1 } }
pub fn stage_or_unstage_diff_hunks( &mut self, stage: bool, ranges: Vec<Range<Anchor>>, window: &mut Window, cx: &mut Context<Self>, ) { let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); cx.spawn_in(window, async move |this, cx| { task.await?; this.update_in(cx, |this, window, cx| { let snapshot = this.buffer.read(cx).snapshot(cx); let hunks = this.diff_hunks_in_ranges(&ranges, &snapshot).collect(); this.apply_stage_or_unstage(stage, hunks, window, cx); }) }) .detach_and_log_err(cx); }
pub fn restore_diff_hunks_in_ranges( &mut self, ranges: Vec<Range<Anchor>>, window: &mut Window, cx: &mut Context<Self>, ) { let snapshot = self.buffer.read(cx).snapshot(cx); let hunks = self.diff_hunks_in_ranges(&ranges, &snapshot).collect(); self.apply_restore(hunks, window, cx); }
fn toggle_diff_hunks_in_ranges( &mut self, ranges: Vec<Range<Anchor>>, cx: &mut Context<Editor>, ) { self.buffer.update(cx, |buffer, cx| { let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx); buffer.expand_or_collapse_diff_hunks(ranges, expand, cx); }) }
fn start_git_blame( &mut self, user_triggered: bool, window: &mut Window, cx: &mut Context<Self>, ) { if let Some(project) = self.project() { if let Some(buffer) = self.buffer().read(cx).as_singleton() && buffer.read(cx).file().is_none() { return; }
let focused = self.focus_handle(cx).contains_focused(window, cx);
let project = project.clone(); let blame = cx .new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx)); self.blame_subscription = Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); self.blame = Some(blame); } }
fn restore_hunks_in_ranges( &mut self, ranges: Vec<Range<Point>>, window: &mut Window, cx: &mut Context<Editor>, ) { let hunks = self.snapshot(window, cx).hunks_for_ranges(ranges); self.apply_restore(hunks, window, cx); }
fn save_buffers_for_ranges_if_needed( &mut self, ranges: &[Range<Anchor>], cx: &mut Context<Editor>, ) -> Task<Result<()>> { let multibuffer = self.buffer.read(cx); let snapshot = multibuffer.read(cx); let buffer_ids: HashSet<_> = ranges .iter() .flat_map(|range| snapshot.buffer_ids_for_range(range.clone())) .collect(); drop(snapshot);
let mut buffers = HashSet::default(); for buffer_id in buffer_ids { if let Some(buffer_entity) = multibuffer.buffer(buffer_id) { let buffer = buffer_entity.read(cx); if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty() { buffers.insert(buffer_entity); } } }
if let Some(project) = &self.project { project.update(cx, |project, cx| project.save_buffers(buffers, cx)) } else { Task::ready(Ok(())) } }
fn do_stage_or_unstage_and_next( &mut self, stage: bool, window: &mut Window, cx: &mut Context<Self>, ) { let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
if ranges.iter().any(|range| range.start != range.end) { self.stage_or_unstage_diff_hunks(stage, ranges, window, cx); return; }
self.stage_or_unstage_diff_hunks(stage, ranges, window, cx);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); let wrap_around = !all_diff_hunks_expanded; let snapshot = self.snapshot(window, cx); let position = self .selections .newest::<Point>(&snapshot.display_snapshot) .head();
self.go_to_hunk_before_or_after_position( &snapshot, position, Direction::Next, wrap_around, window, cx, ); }
fn open_git_blame_commit_internal( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> Option<()> { let blame = self.blame.as_ref()?; let snapshot = self.snapshot(window, cx); let cursor = self .selections .newest::<Point>(&snapshot.display_snapshot) .head(); let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?; let (_, blame_entry) = blame .update(cx, |blame, cx| { blame .blame_for_rows( &[RowInfo { buffer_id: Some(buffer.remote_id()), buffer_row: Some(point.row), ..Default::default() }], cx, ) .next() }) .flatten()?; let renderer = cx.global::<GlobalBlameRenderer>().0.clone(); let repo = blame.read(cx).repository(cx, buffer.remote_id())?; let workspace = self.workspace()?.downgrade(); renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); None }
fn has_blame_entries(&self, cx: &App) -> bool { self.blame() .is_some_and(|blame| blame.read(cx).has_generated_entries()) }
fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { let cursor_anchor = self.selections.newest_anchor().head();
let snapshot = self.buffer.read(cx).snapshot(cx); let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row);
snapshot.line_len(buffer_row) == 0 } fn hunk_after_position( &mut self, snapshot: &EditorSnapshot, position: Point, wrap_around: bool, ) -> Option<MultiBufferDiffHunk> { let result = snapshot .buffer_snapshot() .diff_hunks_in_range(position..snapshot.buffer_snapshot().max_point()) .find(|hunk| hunk.row_range.start.0 > position.row);
if wrap_around { result.or_else(|| { snapshot .buffer_snapshot() .diff_hunks_in_range(Point::zero()..position) .find(|hunk| hunk.row_range.end.0 < position.row) }) } else { result } }
fn hunk_before_position( &mut self, snapshot: &EditorSnapshot, position: Point, wrap_around: bool, ) -> Option<MultiBufferRow> { let result = snapshot.buffer_snapshot().diff_hunk_before(position);
if wrap_around { result.or_else(|| snapshot.buffer_snapshot().diff_hunk_before(Point::MAX)) } else { result } }
/// Dismisses overlays that have no comments stored for their hunks. /// Keeps overlays that have at least one comment. fn dismiss_overlays_without_comments(&mut self, cx: &mut Context<Self>) { let snapshot = self.buffer.read(cx).snapshot(cx);
// First, compute which overlays have comments (to avoid borrow issues with retain) let overlays_with_comments: Vec<bool> = self .diff_review_overlays .iter() .map(|overlay| self.hunk_comment_count(&overlay.hunk_key, &snapshot) > 0) .collect();
// Now collect block IDs to remove and retain overlays let mut block_ids_to_remove = HashSet::default(); let mut index = 0; self.diff_review_overlays.retain(|overlay| { let has_comments = overlays_with_comments[index]; index += 1; if !has_comments { block_ids_to_remove.insert(overlay.block_id); } has_comments });
if !block_ids_to_remove.is_empty() { self.remove_blocks(block_ids_to_remove, None, cx); cx.notify(); } }
/// Refreshes the diff review overlay block to update its height and render function. /// Uses resize_blocks and replace_blocks to avoid visual flicker from remove+insert. fn refresh_diff_review_overlay_height( &mut self, hunk_key: &DiffHunkKey, _window: &mut Window, cx: &mut Context<Self>, ) { // Extract all needed data from overlay first to avoid borrow conflicts let snapshot = self.buffer.read(cx).snapshot(cx); let (comments_expanded, block_id, prompt_editor) = { let Some(overlay) = self .diff_review_overlays .iter() .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot)) else { return; };
( overlay.comments_expanded, overlay.block_id, overlay.prompt_editor.clone(), ) };
// Calculate new height let snapshot = self.buffer.read(cx).snapshot(cx); let new_height = self.calculate_overlay_height(hunk_key, comments_expanded, &snapshot);
// Update the block height using resize_blocks (avoids flicker) let mut heights = HashMap::default(); heights.insert(block_id, new_height); self.resize_blocks(heights, None, cx);
// Update the render function using replace_blocks (avoids flicker) let hunk_key_for_render = hunk_key.clone(); let editor_handle = cx.entity().downgrade(); let render: Arc<dyn Fn(&mut BlockContext) -> AnyElement + Send + Sync> = Arc::new(move |cx| { Self::render_diff_review_overlay( &prompt_editor, &hunk_key_for_render, &editor_handle, cx, ) });
let mut renderers = HashMap::default(); renderers.insert(block_id, render); self.replace_blocks(renderers, None, cx); }
/// Compares two DiffHunkKeys for equality by resolving their anchors. fn hunk_keys_match(a: &DiffHunkKey, b: &DiffHunkKey, snapshot: &MultiBufferSnapshot) -> bool { a.file_path == b.file_path && a.hunk_start_anchor.to_point(snapshot) == b.hunk_start_anchor.to_point(snapshot) }
fn render_diff_review_overlay( prompt_editor: &Entity<Editor>, hunk_key: &DiffHunkKey, editor_handle: &WeakEntity<Editor>, cx: &mut BlockContext, ) -> AnyElement { fn format_line_ranges(ranges: &[(u32, u32)]) -> Option<String> { if ranges.is_empty() { return None; } let formatted: Vec<String> = ranges .iter() .map(|(start, end)| { let start_line = start + 1; let end_line = end + 1; if start_line == end_line { format!("Line {start_line}") } else { format!("Lines {start_line}-{end_line}") } }) .collect(); // Don't show label for single line in single excerpt if ranges.len() == 1 && ranges[0].0 == ranges[0].1 { return None; } Some(formatted.join(" ⋯ ")) }
let theme = cx.theme(); let colors = theme.colors();
let (comments, comments_expanded, inline_editors, user_avatar_uri, line_ranges) = editor_handle .upgrade() .map(|editor| { let editor = editor.read(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); let comments = editor.comments_for_hunk(hunk_key, &snapshot).to_vec(); let (expanded, editors, avatar_uri, line_ranges) = editor .diff_review_overlays .iter() .find(|overlay| { Editor::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot) }) .map(|o| { let start_point = o.anchor_range.start.to_point(&snapshot); let end_point = o.anchor_range.end.to_point(&snapshot); // Get line ranges per excerpt to detect discontinuities let buffer_ranges = snapshot.range_to_buffer_ranges(start_point..end_point); let ranges: Vec<(u32, u32)> = buffer_ranges .iter() .map(|(buffer_snapshot, range, _)| { let start = buffer_snapshot.offset_to_point(range.start.0).row; let end = buffer_snapshot.offset_to_point(range.end.0).row; (start, end) }) .collect(); ( o.comments_expanded, o.inline_edit_editors.clone(), o.user_avatar_uri.clone(), if ranges.is_empty() { None } else { Some(ranges) }, ) }) .unwrap_or((true, HashMap::default(), None, None)); (comments, expanded, editors, avatar_uri, line_ranges) }) .unwrap_or((Vec::new(), true, HashMap::default(), None, None));
let comment_count = comments.len(); let avatar_size = px(20.); let action_icon_size = IconSize::XSmall;
v_flex() .w_full() .bg(colors.editor_background) .border_b_1() .border_color(colors.border) .px_2() .pb_2() .gap_2() // Line range indicator (only shown for multi-line selections or multiple excerpts) .when_some(line_ranges, |el, ranges| { let label = format_line_ranges(&ranges); if let Some(label) = label { el.child( h_flex() .w_full() .px_2() .child(Label::new(label).size(LabelSize::Small).color(Color::Muted)), ) } else { el } }) // Top row: editable input with user's avatar .child( h_flex() .w_full() .items_center() .gap_2() .px_2() .py_1p5() .rounded_md() .bg(colors.surface_background) .child( div() .size(avatar_size) .flex_shrink_0() .rounded_full() .overflow_hidden() .child(if let Some(ref avatar_uri) = user_avatar_uri { Avatar::new(avatar_uri.clone()) .size(avatar_size) .into_any_element() } else { Icon::new(IconName::Person) .size(IconSize::Small) .color(ui::Color::Muted) .into_any_element() }), ) .child( div() .flex_1() .border_1() .border_color(colors.border) .rounded_md() .bg(colors.editor_background) .px_2() .py_1() .child(prompt_editor.clone()), ) .child( h_flex() .flex_shrink_0() .gap_1() .child( IconButton::new("diff-review-close", IconName::Close) .icon_color(ui::Color::Muted) .icon_size(action_icon_size) .tooltip(Tooltip::text("Close")) .on_click(|_, window, cx| { window .dispatch_action(Box::new(crate::actions::Cancel), cx); }), ) .child( IconButton::new("diff-review-add", IconName::Return) .icon_color(ui::Color::Muted) .icon_size(action_icon_size) .tooltip(Tooltip::text("Add comment")) .on_click(|_, window, cx| { window.dispatch_action( Box::new(crate::actions::SubmitDiffReviewComment), cx, ); }), ), ), ) // Expandable comments section (only shown when there are comments) .when(comment_count > 0, |el| { el.child(Self::render_comments_section( comments, comments_expanded, inline_editors, user_avatar_uri, avatar_size, action_icon_size, colors, )) }) .into_any_element() }
fn render_comments_section( comments: Vec<StoredReviewComment>, expanded: bool, inline_editors: HashMap<usize, Entity<Editor>>, user_avatar_uri: Option<SharedUri>, avatar_size: Pixels, action_icon_size: IconSize, colors: &theme::ThemeColors, ) -> impl IntoElement { let comment_count = comments.len();
v_flex() .w_full() .gap_1() // Header with expand/collapse toggle .child( h_flex() .id("review-comments-header") .w_full() .items_center() .gap_1() .px_2() .py_1() .cursor_pointer() .rounded_md() .hover(|style| style.bg(colors.ghost_element_hover)) .on_click(|_, window: &mut Window, cx| { window.dispatch_action( Box::new(crate::actions::ToggleReviewCommentsExpanded), cx, ); }) .child( Icon::new(if expanded { IconName::ChevronDown } else { IconName::ChevronRight }) .size(IconSize::Small) .color(ui::Color::Muted), ) .child( Label::new(format!( "{} Comment{}", comment_count, if comment_count == 1 { "" } else { "s" } )) .size(LabelSize::Small) .color(Color::Muted), ), ) // Comments list (when expanded) .when(expanded, |el| { el.children(comments.into_iter().map(|comment| { let inline_editor = inline_editors.get(&comment.id).cloned(); Self::render_comment_row( comment, inline_editor, user_avatar_uri.clone(), avatar_size, action_icon_size, colors, ) })) }) }
fn render_comment_row( comment: StoredReviewComment, inline_editor: Option<Entity<Editor>>, user_avatar_uri: Option<SharedUri>, avatar_size: Pixels, action_icon_size: IconSize, colors: &theme::ThemeColors, ) -> impl IntoElement { let comment_id = comment.id; let is_editing = inline_editor.is_some();
h_flex() .w_full() .items_center() .gap_2() .px_2() .py_1p5() .rounded_md() .bg(colors.surface_background) .child( div() .size(avatar_size) .flex_shrink_0() .rounded_full() .overflow_hidden() .child(if let Some(ref avatar_uri) = user_avatar_uri { Avatar::new(avatar_uri.clone()) .size(avatar_size) .into_any_element() } else { Icon::new(IconName::Person) .size(IconSize::Small) .color(ui::Color::Muted) .into_any_element() }), ) .child(if let Some(editor) = inline_editor { // Inline edit mode: show an editable text field div() .flex_1() .border_1() .border_color(colors.border) .rounded_md() .bg(colors.editor_background) .px_2() .py_1() .child(editor) .into_any_element() } else { // Display mode: show the comment text div() .flex_1() .text_sm() .text_color(colors.text) .child(comment.comment) .into_any_element() }) .child(if is_editing { // Editing mode: show close and confirm buttons h_flex() .gap_1() .child( IconButton::new( format!("diff-review-cancel-edit-{comment_id}"), IconName::Close, ) .icon_color(ui::Color::Muted) .icon_size(action_icon_size) .tooltip(Tooltip::text("Cancel")) .on_click(move |_, window, cx| { window.dispatch_action( Box::new(crate::actions::CancelEditReviewComment { id: comment_id, }), cx, ); }), ) .child( IconButton::new( format!("diff-review-confirm-edit-{comment_id}"), IconName::Return, ) .icon_color(ui::Color::Muted) .icon_size(action_icon_size) .tooltip(Tooltip::text("Confirm")) .on_click(move |_, window, cx| { window.dispatch_action( Box::new(crate::actions::ConfirmEditReviewComment { id: comment_id, }), cx, ); }), ) .into_any_element() } else { // Display mode: no action buttons for now (edit/delete not yet implemented) gpui::Empty.into_any_element() }) }
fn get_permalink_to_line(&self, cx: &mut Context<Self>) -> Task<Result<url::Url>> { let buffer_and_selection = maybe!({ let selection = self.selections.newest::<Point>(&self.display_snapshot(cx)); let selection_range = selection.range();
let multi_buffer = self.buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let buffer_ranges = multi_buffer_snapshot .range_to_buffer_ranges(selection_range.start..selection_range.end);
let (buffer_snapshot, range, _) = if selection.reversed { buffer_ranges.first() } else { buffer_ranges.last() }?;
let buffer_range = range.to_point(buffer_snapshot); let buffer = multi_buffer.buffer(buffer_snapshot.remote_id())?;
let Some(buffer_diff) = multi_buffer.diff_for(buffer_snapshot.remote_id()) else { return Some((buffer, buffer_range.start.row..buffer_range.end.row)); };
let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx); let start = buffer_diff_snapshot .buffer_point_to_base_text_point(buffer_range.start, &buffer_snapshot); let end = buffer_diff_snapshot .buffer_point_to_base_text_point(buffer_range.end, &buffer_snapshot);
Some((buffer, start.row..end.row)) });
let Some((buffer, selection)) = buffer_and_selection else { return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); };
let Some(project) = self.project() else { return Task::ready(Err(anyhow!("editor does not have project"))); };
project.update(cx, |project, cx| { project.get_permalink_to_line(&buffer, selection, cx) }) }}
#[cfg(test)]impl Editor { /// Returns the line range for the first diff review overlay, if one is active. /// Returns (start_row, end_row) as physical line numbers in the underlying file. pub(super) fn diff_review_line_range(&self, cx: &App) -> Option<(u32, u32)> { let overlay = self.diff_review_overlays.first()?; let snapshot = self.buffer.read(cx).snapshot(cx); let start_point = overlay.anchor_range.start.to_point(&snapshot); let end_point = overlay.anchor_range.end.to_point(&snapshot); let start_row = snapshot .point_to_buffer_point(start_point) .map(|(_, p)| p.row) .unwrap_or(start_point.row); let end_row = snapshot .point_to_buffer_point(end_point) .map(|(_, p)| p.row) .unwrap_or(end_point.row); Some((start_row, end_row)) }
/// Takes all stored comments from all hunks, clearing the storage. /// Returns a Vec of (hunk_key, comments) pairs. pub(super) fn take_all_review_comments( &mut self, cx: &mut Context<Self>, ) -> Vec<(DiffHunkKey, Vec<StoredReviewComment>)> { // Dismiss all overlays when taking comments (e.g., when sending to agent) self.dismiss_all_diff_review_overlays(cx); let comments = std::mem::take(&mut self.stored_review_comments); // Reset the ID counter since all comments have been taken self.next_review_comment_id = 0; cx.emit(EditorEvent::ReviewCommentsChanged { total_count: 0 }); cx.notify(); comments }}
impl EditorSnapshot { pub(super) fn display_diff_hunks_for_rows<'a>( &'a self, display_rows: Range<DisplayRow>, folded_buffers: &'a HashSet<BufferId>, ) -> impl 'a + Iterator<Item = DisplayDiffHunk> { let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self); let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self);
self.buffer_snapshot() .diff_hunks_in_range(buffer_start..buffer_end) .filter_map(|hunk| { if folded_buffers.contains(&hunk.buffer_id) || (hunk.row_range.is_empty() && self.buffer.all_diff_hunks_expanded()) { return None; }
let hunk_start_point = Point::new(hunk.row_range.start.0, 0); let hunk_end_point = if hunk.row_range.end > hunk.row_range.start { let last_row = MultiBufferRow(hunk.row_range.end.0 - 1); let line_len = self.buffer_snapshot().line_len(last_row); Point::new(last_row.0, line_len) } else { Point::new(hunk.row_range.end.0, 0) };
let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left); let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right);
let display_hunk = if hunk_display_start.column() != 0 { DisplayDiffHunk::Folded { display_row: hunk_display_start.row(), } } else { let mut end_row = hunk_display_end.row(); if hunk.row_range.end > hunk.row_range.start || hunk_display_end.column() > 0 { end_row.0 += 1; } let is_created_file = hunk.is_created_file(); let multi_buffer_range = hunk.multi_buffer_range.clone();
DisplayDiffHunk::Unfolded { status: hunk.status(), diff_base_byte_range: hunk.diff_base_byte_range.start.0 ..hunk.diff_base_byte_range.end.0, word_diffs: hunk.word_diffs, display_row_range: hunk_display_start.row()..end_row, multi_buffer_range, is_created_file, } };
Some(display_hunk) }) }
fn hunks_for_ranges( &self, ranges: impl IntoIterator<Item = Range<Point>>, ) -> Vec<MultiBufferDiffHunk> { let mut hunks = Vec::new(); let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> = HashMap::default(); for query_range in ranges { let query_rows = MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); for hunk in self.buffer_snapshot().diff_hunks_in_range( Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), ) { // Include deleted hunks that are adjacent to the query range, because // otherwise they would be missed. let mut intersects_range = hunk.row_range.overlaps(&query_rows); if hunk.status().is_deleted() { intersects_range |= hunk.row_range.start == query_rows.end; intersects_range |= hunk.row_range.end == query_rows.start; } if intersects_range { if !processed_buffer_rows .entry(hunk.buffer_id) .or_default() .insert(hunk.buffer_range.start..hunk.buffer_range.end) { continue; } hunks.push(hunk); } } }
hunks }}
pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) { cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));}
pub fn render_diff_hunk_controls( row: u32, status: &DiffHunkStatus, hunk_range: Range<Anchor>, is_created_file: bool, line_height: Pixels, editor: &Entity<Editor>, _window: &mut Window, cx: &mut App,) -> AnyElement { let stageable = hunk_range .start .buffer_id() .and_then(|buffer_id| editor.read(cx).buffer().read(cx).diff_for(buffer_id)) .is_some_and(|diff| diff.read(cx).is_stageable()); let show_stage_restore = stageable && ProjectSettings::get_global(cx) .git .show_stage_restore_buttons;
h_flex() .h(line_height) .mr_1() .gap_1() .px_0p5() .pb_1() .border_x_1() .border_b_1() .border_color(cx.theme().colors().border_variant) .rounded_b_lg() .bg(cx.theme().colors().editor_background) .gap_1() .block_mouse_except_scroll() .shadow_md() .when(show_stage_restore, |el| { el.child(if status.has_secondary_hunk() { Button::new(("stage", row as u64), "Stage") .alpha(if status.is_pending() { 0.66 } else { 1.0 }) .tooltip({ let focus_handle = editor.focus_handle(cx); move |_window, cx| { Tooltip::for_action_in( "Stage Hunk", &::git::ToggleStaged, &focus_handle, cx, ) } }) .on_click({ let editor = editor.clone(); move |_event, window, cx| { editor.update(cx, |editor, cx| { editor.stage_or_unstage_diff_hunks( true, vec![hunk_range.start..hunk_range.start], window, cx, ); }); } }) } else { Button::new(("unstage", row as u64), "Unstage") .alpha(if status.is_pending() { 0.66 } else { 1.0 }) .tooltip({ let focus_handle = editor.focus_handle(cx); move |_window, cx| { Tooltip::for_action_in( "Unstage Hunk", &::git::ToggleStaged, &focus_handle, cx, ) } }) .on_click({ let editor = editor.clone(); move |_event, window, cx| { editor.update(cx, |editor, cx| { editor.stage_or_unstage_diff_hunks( false, vec![hunk_range.start..hunk_range.start], window, cx, ); }); } }) }) }) .when(show_stage_restore, |el| { el.child( Button::new(("restore", row as u64), "Restore") .tooltip({ let focus_handle = editor.focus_handle(cx); move |_window, cx| { Tooltip::for_action_in( "Restore Hunk", &::git::Restore, &focus_handle, cx, ) } }) .on_click({ let editor = editor.clone(); move |_event, window, cx| { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); let point = hunk_range.start.to_point(&snapshot.buffer_snapshot()); editor.restore_hunks_in_ranges(vec![point..point], window, cx); }); } }) .disabled(is_created_file), ) }) .when( !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), |el| { el.child( IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) // .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); move |_window, cx| { Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx) } }) .on_click({ let editor = editor.clone(); move |_event, window, cx| { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); let position = hunk_range.end.to_point(&snapshot.buffer_snapshot()); editor.go_to_hunk_before_or_after_position( &snapshot, position, Direction::Next, true, window, cx, ); editor.expand_selected_diff_hunks(cx); }); } }), ) .child( IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) // .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); move |_window, cx| { Tooltip::for_action_in( "Previous Hunk", &GoToPreviousHunk, &focus_handle, cx, ) } }) .on_click({ let editor = editor.clone(); move |_event, window, cx| { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); let point = hunk_range.start.to_point(&snapshot.buffer_snapshot()); editor.go_to_hunk_before_or_after_position( &snapshot, point, Direction::Prev, true, window, cx, ); editor.expand_selected_diff_hunks(cx); }); } }), ) }, ) .into_any_element()}
impl Editor { pub(super) fn update_uncommitted_diff_for_buffer( &mut self, project: &Entity<Project>, buffers: impl IntoIterator<Item = Entity<Buffer>>, cx: &mut Context<Self>, ) -> Task<()> { let mut tasks = Vec::new(); project.update(cx, |project, cx| { let git_store = project.git_store().clone(); git_store.update(cx, |git_store, cx| { for buffer in buffers { if project::File::from_dyn(buffer.read(cx).file()).is_some() { tasks.push(git_store.open_display_diff(buffer, cx)); } } }); });
let editor = cx.entity(); let buffer = self.buffer.clone(); cx.spawn(async move |_, cx| { let diffs = future::join_all(tasks).await; if editor.read_with(cx, |editor, _cx| editor.diff_hunk_delegate.is_some()) { return; }
buffer.update(cx, |buffer, cx| { for diff in diffs.into_iter().flatten() { buffer.add_diff(diff, cx); } }); }) }}