diff --git a/helix-core/src/conflict.rs b/helix-core/src/conflict.rs index e9d6147b..bdc9d203 100644 --- a/helix-core/src/conflict.rs +++ b/helix-core/src/conflict.rs @@ -45,6 +45,28 @@ use crate::Rope; // ── Types ───────────────────────────────────────────────────────────────────── +/// Pair of removed/added word-diff ranges for a conflict refine pair. +type RefineDiffs = (Vec>, Vec>); + +/// Per-conflict word-level diff cache entry, keyed by conflict start position. +#[derive(Debug, Clone, Default)] +pub struct ConflictRefineEntry { + /// Current refine pair index (see [`ConflictRegion::refine_pair_indices`]). + /// For a 3-section diff3 conflict: 0 = current↔base, 1 = base↔incoming, 2 = current↔incoming. + /// For N-way jj conflicts the ordering prioritises each side vs base, + /// then side–side comparisons; remaining base-involving pairs are excluded. + pub pair: usize, + /// Cached word-diff results for the current `pair`: + /// `(removed_ranges, added_ranges)`. + pub diffs: Option, +} + +/// Per-conflict word-diff refine state, keyed by [`ConflictRegion::start`]. +/// +/// Cleared on every edit; the active pair setting is preserved when the cursor +/// was inside a conflict before the edit. +pub type ConflictCache = HashMap; + /// Whether a section holds one side of a conflict or the common base. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SectionKind { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8d9117d3..e072e223 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6365,10 +6365,10 @@ fn conflict_cycle_diffs(cx: &mut Context) { return; } - let key = region.start; - let current = doc.conflict_refine_state.get(&key).copied().unwrap_or(0); - let next = (current + 1) % region.num_refine_pairs(); - doc.conflict_refine_state.insert(key, next); + let mut cache = doc.conflict_refine.borrow_mut(); + let entry = cache.entry(region.start).or_default(); + entry.pair = (entry.pair + 1) % region.num_refine_pairs(); + entry.diffs = None; } #[derive(Clone, Copy)] diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 075706c3..b5172f3f 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -14,10 +14,7 @@ use crate::{ }; use helix_core::{ - conflict::{ - conflict_marker_lines, conflict_pair_sections, conflict_refine_pair, find_conflicts, - refine_diff, - }, + conflict::{conflict_marker_lines, conflict_pair_sections, find_conflicts, refine_diff}, diagnostic::NumberOrString, graphemes::{next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, @@ -713,6 +710,8 @@ impl EditorView { let conflicts = find_conflicts(text); + let mut cache = doc.conflict_refine.borrow_mut(); + // Accumulate word-diff spans for every conflict visible in the viewport. let mut spans: Vec<(syntax::Highlight, ops::Range)> = Vec::new(); for region in &conflicts { @@ -722,14 +721,17 @@ impl EditorView { continue; } - let pair = conflict_refine_pair(&doc.conflict_refine_state, region); + let entry = cache.entry(region.start).or_default(); + let pair = entry.pair.min(region.num_refine_pairs().saturating_sub(1)); let Some((left, right)) = conflict_pair_sections(region, pair) else { continue; }; - let (removed, added) = refine_diff(text, left, right); - spans.extend(removed.into_iter().map(|r| (removed_hl, r))); - spans.extend(added.into_iter().map(|r| (added_hl, r))); + let (removed, added) = entry + .diffs + .get_or_insert_with(|| refine_diff(text, left, right)); + spans.extend(removed.iter().map(|r| (removed_hl, r.clone()))); + spans.extend(added.iter().map(|r| (added_hl, r.clone()))); } if spans.is_empty() { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index aa4b0dbb..13fb4378 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -23,7 +23,7 @@ use ::parking_lot::Mutex; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::future::Future; @@ -34,6 +34,7 @@ use std::sync::{Arc, Weak}; use std::time::SystemTime; use helix_core::{ + conflict::{conflict_at, find_conflicts, ConflictRefineEntry}, editor_config::EditorConfig, encoding, history::{History, State, UndoKind}, @@ -235,14 +236,12 @@ pub struct Document { // `ArcSwap` directly. syn_loader: Arc>, - /// Per-conflict refine pair state. + /// Per-conflict refine cache: pair index + optional word-diff results. /// - /// Maps `ConflictRegion::start` (char position) to a pair index. - /// Defaults to 0 (first pair). N-way conflicts have C(n,2) pairs total. - /// - /// Entries become stale when the document is edited before the conflict, - /// causing a graceful reset to pair 0 (same behavior as smerge). - pub conflict_refine_state: HashMap, + /// Key: `ConflictRegion::start` (char position). + /// Cleared on every edit; the pair setting is preserved when the cursor + /// was inside a conflict before the edit. + pub conflict_refine: RefCell>, } #[derive(Debug, Clone, Default)] @@ -785,7 +784,7 @@ impl Document { previous_diagnostic_ids: HashMap::new(), pull_diagnostic_controller: TaskController::new(), document_link_controller: TaskController::new(), - conflict_refine_state: HashMap::new(), + conflict_refine: RefCell::new(HashMap::new()), } } @@ -1476,6 +1475,18 @@ impl Document { use helix_core::Assoc; let old_doc = self.text().clone(); + + // Save refine pair for the cursor's conflict before the edit. + let saved_pair = self + .selections + .get(&view_id) + .and_then(|sel| { + let cursor = sel.primary().cursor(old_doc.slice(..)); + let conflicts = find_conflicts(&old_doc); + conflict_at(&conflicts, cursor).map(|idx| conflicts[idx].start) + }) + .and_then(|key| self.conflict_refine.borrow().get(&key).map(|e| e.pair)); + let changes = transaction.changes(); if !changes.apply(&mut self.text) { return false; @@ -1507,6 +1518,22 @@ impl Document { .ensure_invariants(self.text.slice(..)); } + // Clear conflict refine cache; preserve cursor's pair if still in a conflict. + self.conflict_refine.borrow_mut().clear(); + if let Some(pair) = saved_pair { + let cursor = self + .selection(view_id) + .primary() + .cursor(self.text().slice(..)); + let conflicts = find_conflicts(self.text()); + if let Some(idx) = conflict_at(&conflicts, cursor) { + self.conflict_refine + .borrow_mut() + .entry(conflicts[idx].start) + .or_insert_with(|| ConflictRefineEntry { pair, diffs: None }); + } + } + for view_data in self.view_data.values_mut() { view_data.view_position.anchor = transaction .changes()