%-----------------------------------------------------------------------------% % vim: ft=mercury ts=4 sw=4 et wm=0 tw=0 %-----------------------------------------------------------------------------% % Copyright (C) 1995-1998, 2006, 2011 The University of Melbourne. % Copyright (C) 2015, 2019, 2021 The Mercury team. % This file may only be copied under the terms of the GNU General % Public License - see the file COPYING in the Mercury distribution. %-----------------------------------------------------------------------------% % % Main author: bromage % Based on diffs.m, written by bromage and simplified by % Marnix Klooster % % This module contains the predicates to display a diff in various % output styles, based on the command-line options supplied. % %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% :- module diff_out. :- interface. :- import_module difftype. :- import_module file. :- import_module io. %-----------------------------------------------------------------------------% :- type output_style ---> normal ; help_only ; version_only ; context(int) ; unified(int) ; ed ; forward_ed ; rcs ; ifdef(string) ; brief ; side_by_side ; cvs_merge_conflict. % The default output style. % :- pred default_output_style(output_style::out) is det. % Succeeds if, for this output style, an absence of differences % means that no output should be generated. % :- pred no_diff_implies_no_output(output_style::in) is semidet. % Succeeds if the user only wants to know about the presence % of any differences, not what they actually are. % :- pred full_diff_not_required(output_style::in) is semidet. % Succeeds if the output style is "robust", that is, the absence of a % newline at the end of the file actually matters. % :- pred robust(output_style::in) is semidet. % display_diff takes a diff and displays it in the user's specified output % format. % :- pred display_diff(file::in, file::in, diff::in, io::di, io::uo) is det. %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% :- implementation. :- import_module globals. :- import_module options. :- import_module bool. :- import_module char. :- import_module int. :- import_module list. :- import_module pair. :- import_module require. :- import_module string. %-----------------------------------------------------------------------------% default_output_style(normal). %-----------------------------------------------------------------------------% no_diff_implies_no_output(normal). no_diff_implies_no_output(context(_)). no_diff_implies_no_output(unified(_)). no_diff_implies_no_output(ed). no_diff_implies_no_output(forward_ed). no_diff_implies_no_output(rcs). no_diff_implies_no_output(brief). %-----------------------------------------------------------------------------% full_diff_not_required(brief). %-----------------------------------------------------------------------------% robust(normal). robust(context(_)). robust(unified(_)). robust(rcs). robust(ifdef(_)). robust(side_by_side). robust(cvs_merge_conflict). %-----------------------------------------------------------------------------% % show_file shows the segment of the file from Low to High, with % each line preceded by the Prefix character and a space. The diff(1) % format specifies that the lines affected in the first file should be % flagged by '<' and the lines affected in the second file should be % flagged by '>'. % :- pred show_file(file::in, string::in, pos::in, pos::in, io::di, io::uo) is det. show_file(File, Prefix, Low, High, !IO) :- globals.io_lookup_bool_option(expand_tabs, ExpandTabs, !IO), show_file_2(ExpandTabs, File, Prefix, Low, High, !IO). :- pred show_file_2(bool::in, file::in, string::in, pos::in, pos::in, io::di, io::uo) is det. show_file_2(ExpandTabs, File, Prefix, Low, High, !IO) :- ( if Low < High then ( if file.get_line(File, Low, Line) then io.write_string(Prefix, !IO), ( ExpandTabs = yes, string.to_char_list(Line, LineList), expand_tabs(LineList, 0, !IO) ; ExpandTabs = no, io.write_string(Line, !IO) ), show_file_2(ExpandTabs, File, Prefix, Low + 1, High, !IO) else error("diff_out_show_file: file ended prematurely") ) else true ). :- pred expand_tabs(list(char)::in, int::in, io::di, io::uo) is det. expand_tabs([], _, !IO). expand_tabs([C | Cs], Pos, !IO) :- ( if C = '\t' then Spaces = tab_width - (Pos rem tab_width), put_spaces(Spaces, Pos, NewPos, !IO), expand_tabs(Cs, NewPos, !IO) else io.write_char(C, !IO), expand_tabs(Cs, Pos + 1, !IO) ). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % display_diff: Determine which output style to use, then call the % predicate to display that output. % % Some of these options (notably the ones which require no output) should % have been handled already by the time we reach here. In those cases, we % just call error/1. % display_diff(File1, File2, Diff, !IO) :- globals.io_get_output_style(OutputStyle, !IO), ( if Diff = [], no_diff_implies_no_output(OutputStyle) then true else display_diff_2(OutputStyle, File1, File2, Diff, !IO) ). :- pred display_diff_2(output_style::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_2(normal, File1, File2, Diff, !IO) :- display_diff_normal(File1, File2, Diff, !IO). display_diff_2(help_only, _File1, _File2, _Diff, !IO) :- error("display_diff: help_only"). display_diff_2(version_only, _File1, _File2, _Diff, !IO) :- error("display_diff: version_only"). display_diff_2(context(Context), File1, File2, Diff, !IO) :- display_context_diff(Context, File1, File2, Diff, !IO). display_diff_2(unified(Context), File1, File2, Diff, !IO) :- display_unified_diff(Context, File1, File2, Diff, !IO). display_diff_2(ed, File1, File2, Diff, !IO) :- display_diff_ed(File1, File2, Diff, !IO). display_diff_2(forward_ed, File1, File2, Diff, !IO) :- display_diff_forward_ed(File1, File2, Diff, !IO). display_diff_2(rcs, File1, File2, Diff, !IO) :- display_diff_rcs(File1, File2, Diff, !IO). display_diff_2(ifdef(Sym), File1, File2, Diff, !IO) :- display_diff_ifdef(Sym, File1, File2, Diff, !IO). display_diff_2(brief, File1, File2, _Diff, !IO) :- % XXX For this output style, we really don't need to perform % a complete diff. This should be handled higher up for efficiency. file.get_file_name(File1, FileName1), file.get_file_name(File2, FileName2), io.write_strings(["Files ", FileName1, " and ", FileName2, " differ\n"], !IO). display_diff_2(side_by_side, File1, File2, Diff, !IO) :- display_diff_side_by_side(File1, File2, Diff, !IO). display_diff_2(cvs_merge_conflict, File1, File2, Diff, !IO) :- display_diff_cvs_merge_conflict(File1, File2, Diff, !IO). %-----------------------------------------------------------------------------% % display_diff_normal takes a diff and displays it % in the standard diff(1) output format. % :- pred display_diff_normal(file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_normal(File1, File2, Diff, !IO) :- globals.io_lookup_bool_option(initial_tab, InitialTab, !IO), ( InitialTab = no, FromStr = "< ", ToStr = "> " ; InitialTab = yes, FromStr = "<\t", ToStr = ">\t" ), display_diff_normal_2(File1, File2, Diff, FromStr, ToStr, !IO). % display_diff_normal takes a diff and displays it % in the standard diff(1) output format. % :- pred display_diff_normal_2(file::in, file::in, diff::in, string::in, string::in, io::di, io::uo) is det. display_diff_normal_2(_, _, [], _, _, !IO). display_diff_normal_2(File1, File2, [SingDiff | Diff], FromStr, ToStr, !IO) :- ( SingDiff = add(X, Y1 - Y2), diff_out.write_command(X - X, 'a', Y1 - Y2, !IO), diff_out.show_file(File2, ToStr, Y1, Y2, !IO) ; SingDiff = delete(X1 - X2, Y), diff_out.write_command(X1 - X2, 'd', Y - Y, !IO), diff_out.show_file(File1, FromStr, X1, X2, !IO) ; SingDiff = change(X1 - X2, Y1 - Y2), diff_out.write_command(X1 - X2, 'c', Y1 - Y2, !IO), diff_out.show_file(File1, FromStr, X1, X2, !IO), io.write_string("---\n", !IO), diff_out.show_file(File2, ToStr, Y1, Y2, !IO) ), display_diff_normal_2(File1, File2, Diff, FromStr, ToStr, !IO). % write_command displays a diff(1) command. % Like ed(1), a pair of numbers which are identical are abbreviated by a % single number. % MK: Assumption X== X2 then % Either empty or singleton segment. io.write_int(X2, !IO) else io.write_int(X1, !IO), io.write_char(',', !IO), io.write_int(X2, !IO) ), io.write_char(C, !IO), Y1 = Y + 1, % Convert from pos to line number. ( if Y1 >= Y2 then % Either empty or singleton segment. io.write_int(Y2, !IO) else io.write_int(Y1, !IO), io.write_char(',', !IO), io.write_int(Y2, !IO) ), io.write_char('\n', !IO). %-----------------------------------------------------------------------------% % display_diff_rcs takes a diff and displays it in the RCS difference % format. % :- pred display_diff_rcs(file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_rcs(_File1, _File2, [], !IO). display_diff_rcs(File1, File2, [Cmd | Diff], !IO) :- ( Cmd = add(X, Y1 - Y2), write_command_rcs('a', X, Y2 - Y1, !IO), show_file(File2, "", Y1, Y2, !IO) ; Cmd = delete(X1 - X2, _Y), write_command_rcs('d', X1, X2 - X1, !IO) ; Cmd = change(X1 - X2, Y1 - Y2), write_command_rcs('d', X1, X2 - X1, !IO), write_command_rcs('a', X1, Y2 - Y1, !IO), show_file(File2, "", Y1, Y2, !IO) ), display_diff_rcs(File1, File2, Diff, !IO). % diff_out.write_command_rcs displays a diff command in the RCS ,v format. % :- pred write_command_rcs(char::in, int::in, int::in, io::di, io::uo) is det. write_command_rcs(C, X, Y, !IO) :- io.write_char(C, !IO), io.write_int(X + 1, !IO), % Convert from pos to line number. io.write_char(' ', !IO), io.write_int(Y, !IO), io.write_char('\n', !IO). %-----------------------------------------------------------------------------% % display_diff_ed takes a diff and displays it in ed(1) format, but with % all diffs backward. % :- pred display_diff_ed(file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_ed(_File1, _File2, [], !IO). display_diff_ed(File1, File2, [Cmd | Diff], !IO) :- display_diff_ed(File1, File2, Diff, !IO), ( Cmd = add(X, Y1 - Y2), write_command_ed(X - X, 'a', !IO), show_file(File2, "", Y1, Y2, !IO), io.write_string(".\n", !IO) ; Cmd = delete(X, _Y), write_command_ed(X, 'd', !IO) ; Cmd = change(X, Y1 - Y2), write_command_ed(X, 'c', !IO), show_file(File2, "", Y1, Y2, !IO), io.write_string(".\n", !IO) ). % write_command_ed displays an ed(1) command. % :- pred write_command_ed(segment::in, char::in, io::di, io::uo) is det. write_command_ed(X - X2, C, !IO) :- X1 = X + 1, % Convert from pos to line number ( if X1 >= X2 then % Either empty or singleton segment. io.write_int(X2, !IO) else io.write_int(X1, !IO), io.write_char(',', !IO), io.write_int(X2, !IO) ), io.write_char(C, !IO), io.write_char('\n', !IO). %-----------------------------------------------------------------------------% % display_diff_forward_ed takes a diff and displays it in ed(1) format, but % with all diff_out forward. This is actually useless for feeding to ed(1), % but nicer to read. % :- pred display_diff_forward_ed(file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_forward_ed(_File1, _File2, [], !IO). display_diff_forward_ed(File1, File2, [Cmd | Diff], !IO) :- ( Cmd = add(X, Y1 - Y2), write_command_forward_ed(X - X, 'a', !IO), show_file(File2, "", Y1, Y2, !IO), io.write_string(".\n", !IO) ; Cmd = delete(X, _Y), write_command_forward_ed(X, 'd', !IO) ; Cmd = change(X, Y1 - Y2), write_command_forward_ed(X, 'c', !IO), show_file(File2, "", Y1, Y2, !IO), io.write_string(".\n", !IO) ), display_diff_forward_ed(File1, File2, Diff, !IO). % write_command_forward_ed displays a forward ed(1) command. % The difference between this and write_command_ed is that the command char % comes first here. Who comes up with these dumb formats anyway? % :- pred write_command_forward_ed(segment::in, char::in, io::di, io::uo) is det. write_command_forward_ed(X - X2, C, !IO) :- io.write_char(C, !IO), X1 = X + 1, % Convert from pos to line number ( if X1 >= X2 then % Either empty or singleton segment. io.write_int(X2, !IO) else io.write_int(X1, !IO), io.write_char(' ', !IO), io.write_int(X2, !IO) ), io.write_char('\n', !IO). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % display_diff_ifdef writes out the files in a unified diff, % using #ifdefs around each edit. % % TO DO: GNU diff makes this output style much more configurable. % We should too. % :- pred display_diff_ifdef(string::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_ifdef(Sym, File1, File2, Diff, !IO) :- display_diff_ifdef_2(0, Sym, File1, File2, Diff, !IO). % Argument 1 (prev) is the last pos displayed before the current edit (or % end of edits, in the base case). % This is important for when we have to display the "non-diffed" text % between edits. % :- pred display_diff_ifdef_2(int::in, string::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_ifdef_2(Prev, _Sym, File1, _File2, [], !IO) :- file.get_numlines(File1, SegEnd), show_file(File1, "", Prev, SegEnd, !IO). display_diff_ifdef_2(Prev, Sym, File1, File2, [Edit | Diff], !IO) :- first_mentioned_positions(Edit, StartOfEdit, _), show_file(File1, "", Prev, StartOfEdit, !IO), ( Edit = add(X, Y1 - Y2), io.write_strings(["#ifdef ", Sym, "\n"], !IO), show_file(File2, "", Y1, Y2, !IO), io.write_strings(["#endif /* ", Sym, " */\n"], !IO), Next = X ; Edit = delete(X1 - X2, _), io.write_strings(["#ifndef ", Sym, "\n"], !IO), show_file(File1, "", X1, X2, !IO), io.write_strings(["#endif /* not ", Sym, " */\n"], !IO), Next = X2 ; Edit = change(X1 - X2, Y1 - Y2), io.write_strings(["#ifndef ", Sym, "\n"], !IO), show_file(File1, "", X1, X2, !IO), io.write_strings(["#else /* ", Sym, " */\n"], !IO), show_file(File2, "", Y1, Y2, !IO), io.write_strings(["#endif /* ", Sym, " */\n"], !IO), Next = X2 ), display_diff_ifdef_2(Next, Sym, File1, File2, Diff, !IO). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % display_diff_cvs_merge_conflict writes out the files in a % unified diff, using CVS merge conflict marks around each edit. % :- pred display_diff_cvs_merge_conflict(file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_cvs_merge_conflict(File1, File2, Diff, !IO) :- display_diff_cvs_merge_conflict_2(0, File1, File2, Diff, !IO). % Argument 1 (prev) is the last pos displayed before the current edit (or % end of edits, in the base case). % This is important for when we have to display the "non-diffed" text % between edits. % :- pred display_diff_cvs_merge_conflict_2(int::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_cvs_merge_conflict_2(Prev, File1, _File2, [], !IO) :- file.get_numlines(File1, SegEnd), show_file(File1, "", Prev, SegEnd, !IO). display_diff_cvs_merge_conflict_2(Prev, File1, File2, [Edit | Diff], !IO) :- first_mentioned_positions(Edit, StartOfEdit, _), show_file(File1, "", Prev, StartOfEdit, !IO), file.get_file_name(File1, FileName1), file.get_file_name(File2, FileName2), ( Edit = add(X, Y1 - Y2), io.write_strings(["<<<<<<< ", FileName1, "\n"], !IO), show_file(File2, "", Y1, Y2, !IO), io.write_string("=======\n", !IO), io.write_strings([">>>>>>> ", FileName2, "\n"], !IO), Next = X ; Edit = delete(X1 - X2, _), io.write_strings(["<<<<<<< ", FileName1, "\n"], !IO), io.write_string("=======\n", !IO), show_file(File1, "", X1, X2, !IO), io.write_strings([">>>>>>> ", FileName2, "\n"], !IO), Next = X2 ; Edit = change(X1 - X2, Y1 - Y2), io.write_strings(["<<<<<<< ", FileName1, "\n"], !IO), show_file(File1, "", X1, X2, !IO), io.write_string("=======\n", !IO), show_file(File2, "", Y1, Y2, !IO), io.write_strings([">>>>>>> ", FileName2, "\n"], !IO), Next = X2 ), display_diff_cvs_merge_conflict_2(Next, File1, File2, Diff, !IO). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % Types for context/unified diffs. % A context diff is a bit more complicated than a "standard" % diff, because it requires the display of some parts of the % files which are not actually part of the diff, but not all % of it. % % Because context and unified diffs both require the same % kind of information, we factor out the code to turn a % normal diff into a context diff. % :- type context_edit ---> context_edit(segment, segment, diff). :- type context_diff == list(context_edit). %-----------------------------------------------------------------------------% :- pred diff_to_context_diff(int::in, int::in, int::in, diff::in, context_diff::out) is det. diff_to_context_diff(_Xsize, _Ysize, _Context, [], []). diff_to_context_diff(Xsize, Ysize, Context, [Edit | Diff], CDiff) :- diff_to_context_diff(Xsize, Ysize, Context, Diff, CDiff0), % Work out how far the context of this edit reaches. first_mentioned_positions(Edit, Xfirst0, Yfirst0), int.max(Xfirst0 - Context, 0, Xfirst), int.max(Yfirst0 - Context, 0, Yfirst), last_mentioned_positions(Edit, Xlast0, Ylast0), int.min(Xlast0 + Context, Xsize, Xlast), int.min(Ylast0 + Context, Ysize, Ylast), ( CDiff0 = [], CDiff = [context_edit(Xfirst - Xlast, Yfirst - Ylast, [Edit])] ; CDiff0 = [context_edit(XsegLo - XsegHi, YsegLo - YsegHi, DDiff) | CDiff1], % Should we merge this edit into the next one? ( if ( XsegLo =< Xlast ; YsegLo =< Ylast ) then CDiff = [context_edit(Xfirst - XsegHi, Yfirst - YsegHi, [Edit | DDiff]) | CDiff1] else CDiff = [context_edit(Xfirst - Xlast, Yfirst - Ylast, [Edit]) | CDiff0] ) ). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % Display a diff in unified format. % :- pred display_unified_diff(int::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_unified_diff(Context, File1, File2, Diff, !IO) :- file.get_numlines(File1, Size1), file.get_numlines(File2, Size2), diff_to_context_diff(Size1, Size2, Context, Diff, CDiff), file.get_file_name(File1, Name1), file.get_file_name(File2, Name2), % XXX Should also print out file dates. But how? io.write_strings(["--- ", Name1, "\n"], !IO), io.write_strings(["+++ ", Name2, "\n"], !IO), globals.io_lookup_bool_option(initial_tab, InitialTab, !IO), ( InitialTab = no, NoneStr = " ", AddStr = "+", DelStr = "-" ; InitialTab = yes, NoneStr = "\t", AddStr = "+\t", DelStr = "-\t" ), display_unified_diff_2(File1, File2, CDiff, NoneStr, AddStr, DelStr, !IO). :- pred display_unified_diff_2(file::in, file::in, context_diff::in, string::in, string::in, string::in, io::di, io::uo) is det. display_unified_diff_2(_File1, _File2, [], _, _, _, !IO). display_unified_diff_2(File1, File2, [Edit | CDiff], NoneStr, AddStr, DelStr, !IO) :- Edit = context_edit(Xlow - Xhigh, Ylow - Yhigh, Diff), io.format("@@ -%d,%d +%d,%d @@\n", [i(Xlow + 1), i(Xhigh - Xlow), i(Ylow + 1), i(Yhigh - Ylow)], !IO), display_unified_diff_3(Xlow, Xhigh, File1, File2, Diff, NoneStr, AddStr, DelStr, !IO), display_unified_diff_2(File1, File2, CDiff, NoneStr, AddStr, DelStr, !IO). :- pred display_unified_diff_3(int::in, int::in, file::in, file::in, diff::in, string::in, string::in, string::in, io::di, io::uo) is det. display_unified_diff_3(Prev, Size1, File1, _File2, [], NoneStr, _, _, !IO) :- show_file(File1, NoneStr, Prev, Size1, !IO). display_unified_diff_3(Prev, Size1, File1, File2, [Edit | Diff], NoneStr, AddStr, DelStr, !IO) :- first_mentioned_positions(Edit, StartOfEdit, _), show_file(File1, NoneStr, Prev, StartOfEdit, !IO), ( Edit = add(X, Y1 - Y2), show_file(File2, AddStr, Y1, Y2, !IO), Next = X ; Edit = delete(X1 - X2, _), show_file(File1, DelStr, X1, X2, !IO), Next = X1 ; Edit = change(X1 - X2, Y1 - Y2), show_file(File1, DelStr, X1, X2, !IO), show_file(File2, AddStr, Y1, Y2, !IO), Next = X1 ), display_unified_diff_3(Next, Size1, File1, File2, Diff, NoneStr, AddStr, DelStr, !IO). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % Display a diff in context format. % :- pred display_context_diff(int::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_context_diff(Context, File1, File2, Diff, !IO) :- file.get_numlines(File1, Size1), file.get_numlines(File2, Size2), diff_to_context_diff(Size1, Size2, Context, Diff, CDiff), file.get_file_name(File1, Name1), file.get_file_name(File2, Name2), % XXX Should also print out file dates. But how?? io.write_strings(["*** ", Name1, "\n"], !IO), io.write_strings(["--- ", Name2, "\n"], !IO), globals.io_lookup_bool_option(initial_tab, InitialTab, !IO), ( InitialTab = no, NoneStr = " ", AddStr = "+ ", DelStr = "- ", ChgStr = "! " ; InitialTab = yes, NoneStr = "\t", AddStr = "+\t", DelStr = "-\t", ChgStr = "!\t" ), display_context_diff_2(File1, File2, CDiff, NoneStr, AddStr, DelStr, ChgStr, !IO). :- pred display_context_diff_2(file::in, file::in, context_diff::in, string::in, string::in, string::in, string::in, io::di, io::uo) is det. display_context_diff_2(_File1, _File2, [], _, _, _, _, !IO). display_context_diff_2(File1, File2, [Edit | CDiff], NoneStr, AddStr, DelStr, ChgStr, !IO) :- Edit = context_edit(Xlow - Xhigh, Ylow - Yhigh, Diff), io.write_string("***************\n", !IO), io.format("*** %d,%d ****\n", [i(Xlow + 1), i(Xhigh)], !IO), % Don't display the "context from" lines if there's nothing deleted or % changed. % ( if all [AEdit] list.member(AEdit, Diff) => AEdit = add(_, _) then true else display_context_diff_left(Xlow, Xhigh, File1, Diff, NoneStr, DelStr, ChgStr, !IO) ), io.format("--- %d,%d ----\n", [i(Ylow + 1), i(Yhigh)], !IO), % Don't display the "context to" lines if there's nothing added or changed. % ( if all [DEdit] list.member(DEdit, Diff) => DEdit = delete(_, _) then true else display_context_diff_right(Ylow, Yhigh, File2, Diff, NoneStr, AddStr, ChgStr, !IO) ), display_context_diff_2(File1, File2, CDiff, NoneStr, AddStr, DelStr, ChgStr, !IO). :- pred display_context_diff_left(int::in, int::in, file::in, diff::in, string::in, string::in, string::in, io::di, io::uo) is det. display_context_diff_left(Prev, Size1, File1, [], NoneStr, _, _, !IO) :- show_file(File1, NoneStr, Prev, Size1, !IO). display_context_diff_left(Prev, Size1, File1, [Edit | Diff], NoneStr, DelStr, ChgStr, !IO) :- first_mentioned_positions(Edit, StartOfEdit, _), show_file(File1, NoneStr, Prev, StartOfEdit, !IO), ( Edit = add(X, _), Next = X ; Edit = delete(X1 - X2, _), show_file(File1, DelStr, X1, X2, !IO), Next = X2 ; Edit = change(X1 - X2, _), show_file(File1, ChgStr, X1, X2, !IO), Next = X2 ), display_context_diff_left(Next, Size1, File1, Diff, NoneStr, DelStr, ChgStr, !IO). :- pred display_context_diff_right(int::in, int::in, file::in, diff::in, string::in, string::in, string::in, io::di, io::uo) is det. display_context_diff_right(Prev, Size2, File2, [], NoneStr, _, _, !IO) :- diff_out.show_file(File2, NoneStr, Prev, Size2, !IO). display_context_diff_right(Prev, Size2, File2, [Edit | Diff], NoneStr, AddStr, ChgStr, !IO) :- first_mentioned_positions(Edit, StartOfEdit, _), show_file(File2, NoneStr, Prev, StartOfEdit, !IO), ( Edit = add(_, Y1 - Y2), show_file(File2, AddStr, Y1, Y2, !IO), Next = Y2 ; Edit = delete(_, Y), Next = Y ; Edit = change(_, Y1 - Y2), show_file(File2, ChgStr, Y1, Y2, !IO), Next = Y2 ), display_context_diff_right(Next, Size2, File2, Diff, NoneStr, AddStr, ChgStr, !IO). %-----------------------------------------------------------------------------% %-----------------------------------------------------------------------------% % Side-by-side diffs are incredibly complex, as you'll see if % you inspect the code below. % % TO DO: GNU diff has --sdiff-merge-assist, but I can find no documentation % on what this actually does, and haven't had the time to investigate. % For the moment, we accept the option and note here whether or not % it is turned on, but do nothing with it. % Parameters to pass around. % :- type side_by_side_info ---> side_by_side_info( int, % Half width int, % Column 2 offset bool, % Left column only bool, % Suppress common lines bool % Help sdiff ). :- pred display_diff_side_by_side(file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_side_by_side(File1, File2, Diff, !IO) :- globals.io_lookup_int_option(width, Width0, !IO), % Calculate the half-width and offset stuff. % XXX If we are expanding tabs, we should factor this in. Off = (Width0 + 4) // 8 * 4, Max = Off - 3, HalfWidth0 = Width0 - Off + 1, ( if HalfWidth0 =< 0 then HalfWidth = 0 else if HalfWidth0 > Max then HalfWidth = Max else HalfWidth = HalfWidth0 ), ( if HalfWidth > 0 then Col2Off = Off else Col2Off = Width0 ), globals.io_lookup_bool_option(left_column, LeftCol, !IO), globals.io_lookup_bool_option(suppress_common_lines, Suppress, !IO), globals.io_lookup_bool_option(sdiff_merge_assist, Sdiff, !IO), SBS = side_by_side_info(HalfWidth, Col2Off, LeftCol, Suppress, Sdiff), display_diff_side_by_side_2(0, SBS, File1, File2, Diff, !IO). :- pred display_diff_side_by_side_2(int::in, side_by_side_info::in, file::in, file::in, diff::in, io::di, io::uo) is det. display_diff_side_by_side_2(Prev, SBS, File1, _File2, [], !IO) :- SBS = side_by_side_info(_, _, _, Suppress, _), ( Suppress = no, file.get_numlines(File1, SegEnd), show_sbs_same_lines(File1, SBS, Prev - SegEnd, !IO) ; Suppress = yes ). display_diff_side_by_side_2(Prev, SBS, File1, File2, [Edit | Diff], !IO) :- SBS = side_by_side_info(_, _, _, Suppress, _), first_mentioned_positions(Edit, StartOfEdit, _), ( Suppress = no, show_sbs_same_lines(File1, SBS, Prev - StartOfEdit, !IO) ; Suppress = yes ), ( Edit = add(X, Seg2), show_sbs_added_lines(File2, SBS, Seg2, !IO), Next = X ; Edit = delete(X1 - X2, _), show_sbs_deleted_lines(File1, SBS, X1 - X2, !IO), Next = X2 ; Edit = change(X1 - X2, Y1 - Y2), % The side-by-side change diff format is sort of weird. % We have to compute the minimum of the two change sizes, and display % "changed" lines for the minimum of these sizes. Then we display % "added" or "deleted" lines for whatever is left over. int.min(X2 - X1, Y2 - Y1, Size), show_sbs_changed_lines(File1, File2, SBS, X1, Y1, Size, !IO), show_sbs_deleted_lines(File1, SBS, (X1 + Size) - X2, !IO), show_sbs_added_lines(File2, SBS, (Y1 + Size) - Y2, !IO), Next = X2 ), display_diff_side_by_side_2(Next, SBS, File1, File2, Diff, !IO). :- pred show_sbs_changed_lines(file::in, file::in, side_by_side_info::in, int::in, int::in, int::in, io::di, io::uo) is det. show_sbs_changed_lines(File1, File2, SBS, X1, Y1, Size, !IO) :- ( if Size > 0 then ( if file.get_line(File1, X1, Line1), file.get_line(File2, Y1, Line2) then SBS = side_by_side_info(Width, _, _, _, _), string.to_char_list(Line1, Chars1), print_half_line(Chars1, SBS, 0, 0, Width, OutPos, !IO), tab_to_column(OutPos, Width, !IO), io.write_string("|", !IO), tab_to_column(Width + 1, Width + 2, !IO), string.to_char_list(Line2, Chars2), print_half_line(Chars2, SBS, 0, 0, Width, _, !IO), io.write_string("\n", !IO), show_sbs_changed_lines(File1, File2, SBS, X1 + 1, Y1 + 1, Size - 1, !IO) else error("show_sbs_changed_lines: file ended prematurely") ) else true ). :- pred show_sbs_same_lines(file::in, side_by_side_info::in, segment::in, io::di, io::uo) is det. show_sbs_same_lines(File, SBS, Low - High, !IO) :- ( if Low < High then ( if file.get_line(File, Low, Line) then SBS = side_by_side_info(Width, _, LeftCol, _, _), string.to_char_list(Line, Chars), print_half_line(Chars, SBS, 0, 0, Width, OutPos, !IO), % If the user specified --left, don't display the right column % here. ( LeftCol = yes, tab_to_column(OutPos, Width, !IO), io.write_string("(", !IO) ; LeftCol = no, tab_to_column(OutPos, Width + 2, !IO), print_half_line(Chars, SBS, 0, 0, Width, _, !IO) ), io.write_string("\n", !IO), show_sbs_same_lines(File, SBS, (Low + 1) - High, !IO) else error("show_sbs_same_lines: file ended prematurely") ) else true ). :- pred show_sbs_added_lines(file::in, side_by_side_info::in, segment::in, io::di, io::uo) is det. show_sbs_added_lines(File, SBS, Low - High, !IO) :- ( if Low < High then ( if file.get_line(File, Low, Line) then SBS = side_by_side_info(Width, _, _, _, _), string.to_char_list(Line, Chars), tab_to_column(0, Width, !IO), io.write_string("> ", !IO), print_half_line(Chars, SBS, 0, 0, Width, _, !IO), io.write_string("\n", !IO), show_sbs_added_lines(File, SBS, (Low + 1) - High, !IO) else error("show_sbs_added_lines: file ended prematurely") ) else true ). :- pred show_sbs_deleted_lines(file::in, side_by_side_info::in, segment::in, io::di, io::uo) is det. show_sbs_deleted_lines(File, SBS, Low - High, !IO) :- ( if Low < High then ( if file.get_line(File, Low, Line) then SBS = side_by_side_info(Width, _, _, _, _), string.to_char_list(Line, Chars), print_half_line(Chars, SBS, 0, 0, Width, OutPos, !IO), tab_to_column(OutPos, Width, !IO), io.write_string("<\n", !IO), show_sbs_deleted_lines(File, SBS, (Low + 1) - High, !IO) else error("show_sbs_deleted_lines: file ended prematurely") ) else true ). :- func tab_width = int. tab_width = 8. % Put a number of spaces on the output stream. % Update % the output column as we go. % :- pred put_spaces(int::in, int::in, int::out, io::di, io::uo) is det. put_spaces(Spaces, !OutPos, !IO) :- ( if Spaces =< 0 then true else io.write_char(' ', !IO), !:OutPos = !.OutPos + 1, put_spaces(Spaces - 1, !OutPos, !IO) ). % Given a "from" column and a "to" column, put sufficient spaces % on the output stream to reach that column. Use tabs if we can. % :- pred tab_to_column(int::in, int::in, io::di, io::uo) is det. tab_to_column(From, To, !IO) :- AfterTab = From + tab_width - (From rem tab_width), ( if AfterTab > To then ( if From < To then io.write_char(' ', !IO), tab_to_column(From + 1, To, !IO) else true ) else io.write_char('\t', !IO), tab_to_column(AfterTab, To, !IO) ). % Display half a line in a side-by-side diff, stopping when % we reach a certain column. % % This is actually a very simple thing to do, except for one % complication, which is the displaying of tab characters. % % The important variables are: % % InPos: The current column in the input line. % OutPos: The current column in the output line. % OutBound: The column that we must stop at. % :- pred print_half_line(list(char)::in, side_by_side_info::in, int::in, int::in, int::in, int::out, io::di, io::uo) is det. print_half_line([], _SBS, _InPos, OutPos, _OutBound, OutPos, !IO). print_half_line([C | Cs], SBS, InPos0, OutPos0, OutBound, OutPos, !IO) :- ( if C = '\t' then % Calculate how many spaces this tab is worth. Spaces = tab_width - InPos0 rem tab_width, ( if InPos0 = OutPos0 then globals.io_lookup_bool_option(expand_tabs, ExpandTabs, !IO), ( ExpandTabs = yes, % If we are expanding tabs, we just pretend that % we had Spaces spaces and write them. TabStop0 = OutPos0 + Spaces, ( if TabStop0 > OutBound then TabStop = OutBound else TabStop = TabStop0 ), put_spaces(TabStop - OutPos0, OutPos0, OutPos1, !IO) ; % If we are not expanding tabs, just print it and % hope everything lines up okay. ExpandTabs = no, io.write_char('\t', !IO), OutPos1 = OutPos0 + Spaces ) else OutPos1 = OutPos0 ), InPos = InPos0 + Spaces else if ( C = '\r' ; C = '\b' ; C = '\n' ) then % XXX What to do? For the moment, we'll just ignore it. InPos = InPos0, OutPos1 = OutPos0 /*********** % XXX Binary files aren't really supported. else if not char.is_print(C) then InPos = InPos0, OutPos1 = OutPos0 ( if InPos < OutBound then io.write_char(C, !IO) else true ) ***********/ else % The default case. Print and be done with it. InPos = InPos0 + 1, ( if InPos < OutBound then OutPos1 = InPos, io.write_char(C, !IO) else OutPos1 = OutPos0 ) ), print_half_line(Cs, SBS, InPos, OutPos1, OutBound, OutPos, !IO). %-----------------------------------------------------------------------------% :- end_module diff_out. %-----------------------------------------------------------------------------%