@@ -58,24 +58,33 @@ pub const MIN_CONFLICT_MARKER_LEN: usize = 7;
58
58
/// existing markers.
59
59
const CONFLICT_MARKER_LEN_INCREMENT : usize = 4 ;
60
60
61
+ /// Marker for missing terminating newline in a term of a conflict.
62
+ const NO_EOL_MARKER : & str = " [noeol]" ;
63
+
64
+ /// Marker for missing terminating newline in the "add" side of a diff.
65
+ const ADD_NO_EOL_MARKER : & str = " [+noeol]" ;
66
+
67
+ /// Marker for missing terminating newline in the "remove" side of a diff.
68
+ const REMOVE_NO_EOL_MARKER : & str = " [-noeol]" ;
69
+
61
70
fn write_diff_hunks ( hunks : & [ DiffHunk ] , file : & mut dyn Write ) -> io:: Result < ( ) > {
62
71
for hunk in hunks {
63
72
match hunk. kind {
64
73
DiffHunkKind :: Matching => {
65
74
debug_assert ! ( hunk. contents. iter( ) . all_equal( ) ) ;
66
75
for line in hunk. contents [ 0 ] . lines_with_terminator ( ) {
67
76
file. write_all ( b" " ) ?;
68
- file . write_all ( line) ?;
77
+ write_and_ensure_newline ( file , line) ?;
69
78
}
70
79
}
71
80
DiffHunkKind :: Different => {
72
81
for line in hunk. contents [ 0 ] . lines_with_terminator ( ) {
73
82
file. write_all ( b"-" ) ?;
74
- file . write_all ( line) ?;
83
+ write_and_ensure_newline ( file , line) ?;
75
84
}
76
85
for line in hunk. contents [ 1 ] . lines_with_terminator ( ) {
77
86
file. write_all ( b"+" ) ?;
78
- file . write_all ( line) ?;
87
+ write_and_ensure_newline ( file , line) ?;
79
88
}
80
89
}
81
90
}
@@ -460,29 +469,35 @@ fn materialize_git_style_conflict(
460
469
output,
461
470
ConflictMarkerLineChar :: ConflictStart ,
462
471
conflict_marker_len,
463
- & format ! ( "Side #1 ({conflict_info})" ) ,
472
+ & format ! ( "Side #1{} ({conflict_info})" , maybe_no_eol_marker ( left ) ) ,
464
473
) ?;
465
- output. write_all ( left) ?;
474
+ write_and_ensure_newline ( output, left) ?;
475
+
466
476
write_conflict_marker (
467
477
output,
468
478
ConflictMarkerLineChar :: GitAncestor ,
469
479
conflict_marker_len,
470
- "Base" ,
480
+ & format ! ( "Base{}" , maybe_no_eol_marker ( base ) ) ,
471
481
) ?;
472
- output. write_all ( base) ?;
482
+ write_and_ensure_newline ( output, base) ?;
483
+
473
484
// VS Code doesn't seem to support any trailing text on the separator line
474
485
write_conflict_marker (
475
486
output,
476
487
ConflictMarkerLineChar :: GitSeparator ,
477
488
conflict_marker_len,
478
489
"" ,
479
490
) ?;
480
- output. write_all ( right) ?;
491
+
492
+ write_and_ensure_newline ( output, right) ?;
481
493
write_conflict_marker (
482
494
output,
483
495
ConflictMarkerLineChar :: ConflictEnd ,
484
496
conflict_marker_len,
485
- & format ! ( "Side #2 ({conflict_info} ends)" ) ,
497
+ & format ! (
498
+ "Side #2{} ({conflict_info} ends)" ,
499
+ maybe_no_eol_marker( right)
500
+ ) ,
486
501
) ?;
487
502
488
503
Ok ( ( ) )
@@ -501,9 +516,13 @@ fn materialize_jj_style_conflict(
501
516
output,
502
517
ConflictMarkerLineChar :: Add ,
503
518
conflict_marker_len,
504
- & format ! ( "Contents of side #{}" , add_index + 1 ) ,
519
+ & format ! (
520
+ "Contents of side #{}{}" ,
521
+ add_index + 1 ,
522
+ maybe_no_eol_marker( data)
523
+ ) ,
505
524
) ?;
506
- output . write_all ( data)
525
+ write_and_ensure_newline ( output , data)
507
526
} ;
508
527
509
528
// Write a negative snapshot (base) of a conflict
@@ -512,19 +531,34 @@ fn materialize_jj_style_conflict(
512
531
output,
513
532
ConflictMarkerLineChar :: Remove ,
514
533
conflict_marker_len,
515
- & format ! ( "Contents of {base_str}" ) ,
534
+ & format ! ( "Contents of {base_str}{}" , maybe_no_eol_marker ( data ) ) ,
516
535
) ?;
517
- output . write_all ( data)
536
+ write_and_ensure_newline ( output , data)
518
537
} ;
519
538
520
539
// Write a diff from a negative term to a positive term
521
540
let write_diff =
522
541
|base_str : & str , add_index : usize , diff : & [ DiffHunk ] , output : & mut dyn Write | {
542
+ let no_eol_remove = diff
543
+ . last ( )
544
+ . is_some_and ( |diff_hunk| has_no_eol ( diff_hunk. contents [ 0 ] ) ) ;
545
+ let no_eol_add = diff
546
+ . last ( )
547
+ . is_some_and ( |diff_hunk| has_no_eol ( diff_hunk. contents [ 1 ] ) ) ;
548
+ let no_eol_marker = match ( no_eol_remove, no_eol_add) {
549
+ ( true , true ) => NO_EOL_MARKER ,
550
+ ( true , _) => REMOVE_NO_EOL_MARKER ,
551
+ ( _, true ) => ADD_NO_EOL_MARKER ,
552
+ _ => "" ,
553
+ } ;
523
554
write_conflict_marker (
524
555
output,
525
556
ConflictMarkerLineChar :: Diff ,
526
557
conflict_marker_len,
527
- & format ! ( "Changes from {base_str} to side #{}" , add_index + 1 ) ,
558
+ & format ! (
559
+ "Changes from {base_str} to side #{}{no_eol_marker}" ,
560
+ add_index + 1
561
+ ) ,
528
562
) ?;
529
563
write_diff_hunks ( diff, output)
530
564
} ;
@@ -593,6 +627,29 @@ fn materialize_jj_style_conflict(
593
627
Ok ( ( ) )
594
628
}
595
629
630
+ fn maybe_no_eol_marker ( slice : & [ u8 ] ) -> & ' static str {
631
+ if has_no_eol ( slice) {
632
+ NO_EOL_MARKER
633
+ } else {
634
+ ""
635
+ }
636
+ }
637
+
638
+ // Write a chunk of data, ensuring that it doesn't end with a line which is
639
+ // missing its terminating newline.
640
+ fn write_and_ensure_newline ( output : & mut dyn Write , data : & [ u8 ] ) -> io:: Result < ( ) > {
641
+ output. write_all ( data) ?;
642
+ if has_no_eol ( data) {
643
+ writeln ! ( output) ?;
644
+ }
645
+ Ok ( ( ) )
646
+ }
647
+
648
+ // Check whether a slice is missing its terminating newline character.
649
+ fn has_no_eol ( slice : & [ u8 ] ) -> bool {
650
+ slice. last ( ) . is_some_and ( |& last| last != b'\n' )
651
+ }
652
+
596
653
fn diff_size ( hunks : & [ DiffHunk ] ) -> usize {
597
654
hunks
598
655
. iter ( )
@@ -873,7 +930,7 @@ pub async fn update_from_content(
873
930
// conflicts initially. If unsuccessful, attempt to parse conflicts from with
874
931
// the arity of the unsimplified conflicts since such a conflict may be
875
932
// present in the working copy if written by an earlier version of jj.
876
- let ( used_file_ids, hunks) = ' hunks: {
933
+ let ( used_file_ids, mut hunks) = ' hunks: {
877
934
if let Some ( hunks) = parse_conflict (
878
935
content,
879
936
simplified_file_ids. num_sides ( ) ,
@@ -892,6 +949,20 @@ pub async fn update_from_content(
892
949
return Ok ( Merge :: normal ( file_id) ) ;
893
950
} ;
894
951
952
+ // If there is a conflict at the end of the file and a term ends with a newline,
953
+ // check whether the original term ended with a newline. If it didn't, then
954
+ // remove the newline since it was added automatically when materializing.
955
+ if let Some ( last_hunk) = hunks. last_mut ( ) . filter ( |hunk| !hunk. is_resolved ( ) ) {
956
+ for ( used_file_id, term) in zip ( used_file_ids. iter ( ) , last_hunk. iter_mut ( ) ) {
957
+ if term. last ( ) == Some ( & b'\n' ) {
958
+ let used_file_content = get_file_contents ( store, path, used_file_id) . block_on ( ) ?;
959
+ if has_no_eol ( & used_file_content) {
960
+ term. pop ( ) ;
961
+ }
962
+ }
963
+ }
964
+ }
965
+
895
966
let mut contents = used_file_ids. map ( |_| vec ! [ ] ) ;
896
967
for hunk in hunks {
897
968
if let Some ( slice) = hunk. as_resolved ( ) {
0 commit comments