1use std::ops::Range;
4
5use gpui::SharedString;
6
7use super::{
8 DiffFile, DiffFileStatus, DiffParseError,
9 document::{FileSide, LinePair},
10};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14#[non_exhaustive]
15pub enum DiffConflictResolution {
16 Current,
18 Incoming,
20 Both,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub(crate) enum ConflictPart {
27 Current,
28 Base,
29 Incoming,
30}
31
32#[derive(Clone, Debug)]
39pub struct DiffConflict {
40 lines: Range<usize>,
41 current_lines: Range<usize>,
42 base_lines: Option<Range<usize>>,
43 incoming_lines: Range<usize>,
44 current: Range<usize>,
45 base: Option<Range<usize>>,
46 incoming: Range<usize>,
47 current_label: SharedString,
48 base_label: SharedString,
49 incoming_label: SharedString,
50}
51
52impl DiffConflict {
53 pub fn lines(&self) -> Range<usize> {
55 self.lines.clone()
56 }
57 pub fn current_lines(&self) -> Range<usize> {
59 self.current_lines.clone()
60 }
61 pub fn base_lines(&self) -> Option<Range<usize>> {
63 self.base_lines.clone()
64 }
65 pub fn incoming_lines(&self) -> Range<usize> {
67 self.incoming_lines.clone()
68 }
69 pub fn current_label(&self) -> &SharedString {
71 &self.current_label
72 }
73 pub fn base_label(&self) -> Option<&SharedString> {
75 self.base.as_ref().map(|_| &self.base_label)
76 }
77 pub fn incoming_label(&self) -> &SharedString {
79 &self.incoming_label
80 }
81 pub(crate) fn source_lines(&self) -> Range<usize> {
83 self.current.start..self.incoming.end
84 }
85 pub(crate) fn part(&self, part: ConflictPart) -> Option<Range<usize>> {
86 match part {
87 ConflictPart::Current => Some(self.current.clone()),
88 ConflictPart::Base => self.base.clone(),
89 ConflictPart::Incoming => Some(self.incoming.clone()),
90 }
91 }
92 pub(crate) fn label(&self, part: ConflictPart) -> &SharedString {
93 match part {
94 ConflictPart::Current => &self.current_label,
95 ConflictPart::Base => &self.base_label,
96 ConflictPart::Incoming => &self.incoming_label,
97 }
98 }
99 pub(crate) fn part_of(&self, ix: usize) -> Option<ConflictPart> {
101 [
102 ConflictPart::Current,
103 ConflictPart::Base,
104 ConflictPart::Incoming,
105 ]
106 .into_iter()
107 .find(|part| self.part(*part).is_some_and(|range| range.contains(&ix)))
108 }
109 pub(crate) fn kept(&self, resolution: DiffConflictResolution) -> Vec<Range<usize>> {
111 match resolution {
112 DiffConflictResolution::Current => vec![self.current.clone()],
113 DiffConflictResolution::Incoming => vec![self.incoming.clone()],
114 DiffConflictResolution::Both => vec![self.current.clone(), self.incoming.clone()],
115 }
116 }
117}
118
119fn line_ranges(text: &str) -> Vec<Range<usize>> {
121 let mut offset = 0;
122 text.split_inclusive('\n')
123 .map(|line| {
124 let range = offset..offset + line.len();
125 offset = range.end;
126 range
127 })
128 .collect()
129}
130
131impl DiffFile {
132 pub fn unchanged(path: impl Into<SharedString>, text: &str) -> Self {
135 let path = path.into();
136 let ranges = line_ranges(text);
137 let pairs = (0..ranges.len())
138 .map(|ix| LinePair::new(None, Some(ix), false))
139 .collect();
140 let lines = ranges
141 .into_iter()
142 .enumerate()
143 .map(|(ix, range)| (ix + 1, range));
144 DiffFile::new(
145 DiffFileStatus::Unchanged,
146 FileSide::new(Some(path.clone()), String::new(), []),
147 FileSide::new(Some(path), text.to_owned(), lines),
148 pairs,
149 Vec::new(),
150 Vec::new(),
151 false,
152 )
153 }
154
155 pub fn parse_conflicts(
159 path: impl Into<SharedString>,
160 text: &str,
161 ) -> Result<Self, DiffParseError> {
162 let path = path.into();
163 let mut source = String::new();
164 let mut lines = Vec::new();
165 let mut pairs = Vec::new();
166 let mut conflicts = Vec::new();
167 let mut open: Option<(ConflictPart, DiffConflict)> = None;
169 for (ix, range) in line_ranges(text).into_iter().enumerate() {
170 let line = &text[range.clone()];
171 let content = line.trim_end_matches(['\n', '\r']);
172 let marker = |prefix: &str| {
173 content
174 .strip_prefix(prefix)
175 .filter(|rest| rest.is_empty() || rest.starts_with(' '))
176 .map(|rest| SharedString::from(rest.trim().to_owned()))
177 };
178 let next = lines.len();
179 let (opening, base, separator, closing) = (
180 marker("<<<<<<<"),
181 marker("|||||||"),
182 marker("======="),
183 marker(">>>>>>>"),
184 );
185 match open.as_mut() {
186 None => {
187 if let Some(label) = opening {
188 open = Some((
189 ConflictPart::Current,
190 DiffConflict {
191 lines: ix + 1..ix + 1,
192 current_lines: ix + 2..ix + 2,
193 base_lines: None,
194 incoming_lines: ix + 2..ix + 2,
195 current: next..next,
196 base: None,
197 incoming: next..next,
198 current_label: label,
199 base_label: SharedString::default(),
200 incoming_label: SharedString::default(),
201 },
202 ));
203 continue;
204 }
205 if base.is_some() || closing.is_some() {
206 return Err(DiffParseError::new(
207 ix + 1,
208 "conflict marker outside a conflict",
209 ));
210 }
211 }
212 Some((part, conflict)) => {
213 if opening.is_some() {
214 return Err(DiffParseError::new(ix + 1, "nested conflict marker"));
215 }
216 match (*part, base, separator, closing) {
217 (ConflictPart::Current, Some(label), _, _) => {
218 conflict.current.end = next;
219 conflict.current_lines.end = ix + 1;
220 conflict.base_lines = Some(ix + 2..ix + 2);
221 conflict.base = Some(next..next);
222 conflict.base_label = label;
223 *part = ConflictPart::Base;
224 continue;
225 }
226 (ConflictPart::Current | ConflictPart::Base, _, Some(_), _) => {
227 if *part == ConflictPart::Current {
228 conflict.current.end = next;
229 conflict.current_lines.end = ix + 1;
230 } else if let Some(base) = &mut conflict.base {
231 base.end = next;
232 conflict.base_lines.as_mut().unwrap().end = ix + 1;
233 }
234 conflict.incoming = next..next;
235 conflict.incoming_lines = ix + 2..ix + 2;
236 *part = ConflictPart::Incoming;
237 continue;
238 }
239 (ConflictPart::Incoming, _, _, Some(label)) => {
240 conflict.incoming.end = next;
241 conflict.incoming_lines.end = ix + 1;
242 conflict.lines.end = ix + 2;
243 conflict.incoming_label = label;
244 if let Some((_, conflict)) = open.take() {
245 conflicts.push(conflict);
246 }
247 continue;
248 }
249 _ => {}
250 }
251 }
252 }
253 let start = source.len();
254 source.push_str(line);
255 lines.push((ix + 1, start..source.len()));
256 pairs.push(LinePair::new(None, Some(next), open.is_some()));
257 }
258 if open.is_some() {
259 return Err(DiffParseError::new(
260 text.lines().count(),
261 "unterminated conflict",
262 ));
263 }
264 Ok(DiffFile::new(
265 DiffFileStatus::Conflicted,
266 FileSide::new(Some(path.clone()), String::new(), []),
267 FileSide::new(Some(path), source, lines),
268 pairs,
269 Vec::new(),
270 Vec::new(),
271 false,
272 )
273 .with_conflicts(conflicts))
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::diff::DiffSide;
281
282 const MERGE: &str = "fn retry() {\n<<<<<<< HEAD\n 3\n=======\n 5\n>>>>>>> feature\n}\n";
283
284 #[test]
285 fn unchanged_files_show_every_line_once() {
286 let file = DiffFile::unchanged("src/lib.rs", "one\r\ntwo\nlast");
287 assert_eq!(file.status(), DiffFileStatus::Unchanged);
288 assert!(!file.has_changes());
289 assert_eq!(file.lines_count(DiffSide::Modified), 3);
290 assert_eq!(file.lines_count(DiffSide::Original), 0);
291 assert_eq!(
292 file.text_for_lines(DiffSide::Modified, 1, 3),
293 "one\r\ntwo\nlast"
294 );
295 assert!(DiffFile::unchanged("empty", "").pairs().is_empty());
296 }
297
298 #[test]
299 fn conflicts_keep_working_file_line_numbers() {
300 let file = DiffFile::parse_conflicts("src/retry.rs", MERGE).unwrap();
301 assert_eq!(file.status(), DiffFileStatus::Conflicted);
302 let numbers = file
303 .lines(DiffSide::Modified)
304 .iter()
305 .map(|line| line.line_number())
306 .collect::<Vec<_>>();
307 assert_eq!(numbers, [1, 3, 5, 7]);
308 let conflict = &file.conflicts()[0];
309 assert_eq!(conflict.lines(), 2..7);
310 assert_eq!(conflict.current_lines(), 3..4);
311 assert_eq!(conflict.incoming_lines(), 5..6);
312 assert_eq!(conflict.base_lines(), None);
313 assert_eq!(conflict.base_label(), None);
314 assert_eq!(conflict.current_label().as_str(), "HEAD");
315 assert_eq!(conflict.incoming_label().as_str(), "feature");
316 assert_eq!(conflict.part(ConflictPart::Current), Some(1..2));
317 assert_eq!(conflict.part(ConflictPart::Incoming), Some(2..3));
318 assert_eq!(conflict.label(ConflictPart::Current).as_str(), "HEAD");
319 assert_eq!(conflict.label(ConflictPart::Incoming).as_str(), "feature");
320 assert_eq!(conflict.part_of(2), Some(ConflictPart::Incoming));
321 assert!(file.pairs()[1].is_changed() && !file.pairs()[0].is_changed());
322 }
323
324 #[test]
325 fn diff3_bases_and_malformed_markers() {
326 let file = DiffFile::parse_conflicts(
327 "a.txt",
328 "<<<<<<< ours\na\n||||||| base\nb\n=======\nc\n>>>>>>> theirs\n",
329 )
330 .unwrap();
331 let conflict = &file.conflicts()[0];
332 assert_eq!(conflict.lines(), 1..8);
333 assert_eq!(conflict.current_lines(), 2..3);
334 assert_eq!(conflict.base_lines(), Some(4..5));
335 assert_eq!(conflict.incoming_lines(), 6..7);
336 assert_eq!(conflict.base_label().unwrap().as_str(), "base");
337 assert_eq!(conflict.part(ConflictPart::Base), Some(1..2));
338 assert_eq!(conflict.label(ConflictPart::Base).as_str(), "base");
339 assert_eq!(conflict.kept(DiffConflictResolution::Both), [0..1, 2..3]);
340 for text in [
341 "<<<<<<< a\nx\n",
342 "<<<<<<< a\n<<<<<<< b\n",
343 ">>>>>>> stray\n",
344 ] {
345 let error = DiffFile::parse_conflicts("a.txt", text).err().unwrap();
346 assert!(error.line() > 0);
347 }
348 let empty = DiffFile::parse_conflicts(
349 "empty.txt",
350 "<<<<<<< ours\n||||||| base\n=======\n>>>>>>> theirs\n",
351 )
352 .unwrap();
353 let conflict = &empty.conflicts()[0];
354 assert_eq!(conflict.lines(), 1..5);
355 assert_eq!(conflict.current_lines(), 2..2);
356 assert_eq!(conflict.base_lines(), Some(3..3));
357 assert_eq!(conflict.incoming_lines(), 4..4);
358 assert!(DiffFile::parse_conflicts("a.md", "title\n=======\n").is_ok());
360 }
361}