We had a frequent panic when the agent was using our edit file tool. The root cause was that we were constructing a `BufferDiff` with `BufferDiff::new`, then calling `set_base_text`, but not waiting for that asynchronous operation to finish. This means there was a window of time where the diff's base text was set to the initial value of `""`--that's not a problem in itself, but it was possible for us to call `PendingDiff::update` during that window, which calls `BufferDiff::update_diff`, which calls `BufferDiffSnapshot::new_with_base_buffer`, which takes two arguments `base_text` and `base_text_snapshot` that are supposed to represent the same text. We were getting the first of those arguments from the `base_text` field of `PendingDiff`, which is set immediately to the target base text without waiting for `BufferDiff::set_base_text` to run to completion; and the second from the `BufferDiff` itself, which still has the empty base text during that window. As a result of that mismatch, we could end up adding `DeletedHunk` diff transforms to the multibuffer for the diff card even though the multibuffer's base text was empty, ultimately leading to a panic very far away in rendering code. I've fixed this by adding a new `BufferDiff` constructor for the case where the buffer contents and the base text are (initially) the same, like for the diff cards, and so we don't need an async diff calculation. I also added a debug assertion to catch the basic issue here earlier, when `BufferDiffSnapshot::new_with_base_buffer` is called with two base texts that don't match. Release Notes: - N/A ---------
Showing the changes to crates/buffer_diff/src/buffer_diff.rs (1 of 2 files). Show all
Unified · Split
| @@ -162,6 +162,22 @@ impl BufferDiffSnapshot { · 161 unmodified lines | ||
| 162 | 162 | } |
| 163 | 163 | } |
| 164 | 164 | |
| 165 | + fn unchanged( | |
| 166 | + buffer: &text::BufferSnapshot, | |
| 167 | + base_text: language::BufferSnapshot, | |
| 168 | + ) -> BufferDiffSnapshot { | |
| 169 | + debug_assert_eq!(buffer.text(), base_text.text()); | |
| 170 | + BufferDiffSnapshot { | |
| 171 | + inner: BufferDiffInner { | |
| 172 | + base_text, | |
| 173 | + hunks: SumTree::new(buffer), | |
| 174 | + pending_hunks: SumTree::new(buffer), | |
| 175 | + base_text_exists: false, | |
| 176 | + }, | |
| 177 | + secondary_diff: None, | |
| 178 | + } | |
| 179 | + } | |
| 180 | + | |
| 165 | 181 | fn new_with_base_text( |
| 166 | 182 | buffer: text::BufferSnapshot, |
| 167 | 183 | base_text: Option<Arc<String>>, |
| @@ -213,7 +229,10 @@ impl BufferDiffSnapshot { · 45 unmodified lines | ||
| 213 | 229 | cx: &App, |
| 214 | 230 | ) -> impl Future<Output = Self> + use<> { |
| 215 | 231 | let base_text_exists = base_text.is_some(); |
| 216 | − let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone())); | |
| 232 | + let base_text_pair = base_text.map(|text| { | |
| 233 | + debug_assert_eq!(&*text, &base_text_snapshot.text()); | |
| 234 | + (text, base_text_snapshot.as_rope().clone()) | |
| 235 | + }); | |
| 217 | 236 | cx.background_executor() |
| 218 | 237 | .spawn_labeled(*CALCULATE_DIFF_TASK, async move { |
| 219 | 238 | Self { |
| @@ -873,6 +892,18 @@ impl BufferDiff { · 653 unmodified lines | ||
| 873 | 892 | } |
| 874 | 893 | } |
| 875 | 894 | |
| 895 | + pub fn new_unchanged( | |
| 896 | + buffer: &text::BufferSnapshot, | |
| 897 | + base_text: language::BufferSnapshot, | |
| 898 | + ) -> Self { | |
| 899 | + debug_assert_eq!(buffer.text(), base_text.text()); | |
| 900 | + BufferDiff { | |
| 901 | + buffer_id: buffer.remote_id(), | |
| 902 | + inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner, | |
| 903 | + secondary_diff: None, | |
| 904 | + } | |
| 905 | + } | |
| 906 | + | |
| 876 | 907 | #[cfg(any(test, feature = "test-support"))] |
| 877 | 908 | pub fn new_with_base_text( |
| 878 | 909 | base_text: &str, |