OSDN Git Service

Fix issue #951: Reloading a file that was changed by another application does not...
[winmerge-jp/winmerge-jp.git] / Src / MergeDoc.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /** 
8  * @file  MergeDoc.cpp
9  *
10  * @brief Implementation file for CMergeDoc
11  *
12  */
13
14 #include "StdAfx.h"
15 #include "MergeDoc.h"
16 #include <io.h>
17 #include <Poco/Timestamp.h>
18 #include "UnicodeString.h"
19 #include "Merge.h"
20 #include "MainFrm.h"
21 #include "DiffTextBuffer.h"
22 #include "Environment.h"
23 #include "MovedLines.h"
24 #include "MergeEditView.h"
25 #include "MergeEditFrm.h"
26 #include "DirDoc.h"
27 #include "files.h"
28 #include "FileTransform.h"
29 #include "Plugins.h"
30 #include "unicoder.h"
31 #include "UniFile.h"
32 #include "OptionsDef.h"
33 #include "DiffFileInfo.h"
34 #include "SaveClosingDlg.h"
35 #include "OpenTableDlg.h"
36 #include "DiffList.h"
37 #include "paths.h"
38 #include "OptionsMgr.h"
39 #include "OptionsDiffOptions.h"
40 #include "MergeLineFlags.h"
41 #include "FileOrFolderSelect.h"
42 #include "LineFiltersList.h"
43 #include "SubstitutionFiltersList.h"
44 #include "TempFile.h"
45 #include "codepage_detect.h"
46 #include "SelectPluginDlg.h"
47 #include "EncodingErrorBar.h"
48 #include "MergeCmdLineInfo.h"
49 #include "TFile.h"
50 #include "Constants.h"
51 #include "Merge7zFormatMergePluginImpl.h"
52 #include "7zCommon.h"
53 #include "PatchTool.h"
54 #include "charsets.h"
55 #include "markdown.h"
56 #include "stringdiffs.h"
57
58 #ifdef _DEBUG
59 #define new DEBUG_NEW
60 #endif
61
62 using std::swap;
63
64 int CMergeDoc::m_nBuffersTemp = 2;
65
66 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
67
68 /////////////////////////////////////////////////////////////////////////////
69 // CMergeDoc
70
71 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
72
73 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
74         //{{AFX_MSG_MAP(CMergeDoc)
75         // [File] menu
76         ON_COMMAND(ID_FILE_SAVE, OnFileSave)
77         ON_UPDATE_COMMAND_UI(ID_FILE_SAVE, OnUpdateFileSave)
78         ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
79         ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_LEFT, OnUpdateFileSaveLeft)
80         ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
81         ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_MIDDLE, OnUpdateFileSaveMiddle)
82         ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
83         ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_RIGHT, OnUpdateFileSaveRight)
84         ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
85         ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
86         ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
87         ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
88         ON_COMMAND(ID_FILE_LEFT_READONLY, OnFileReadOnlyLeft)
89         ON_UPDATE_COMMAND_UI(ID_FILE_LEFT_READONLY, OnUpdateFileReadOnlyLeft)
90         ON_COMMAND(ID_FILE_MIDDLE_READONLY, OnFileReadOnlyMiddle)
91         ON_UPDATE_COMMAND_UI(ID_FILE_MIDDLE_READONLY, OnUpdateFileReadOnlyMiddle)
92         ON_COMMAND(ID_FILE_RIGHT_READONLY, OnFileReadOnlyRight)
93         ON_UPDATE_COMMAND_UI(ID_FILE_RIGHT_READONLY, OnUpdateFileReadOnlyRight)
94         ON_COMMAND(ID_RESCAN, OnFileReload)
95         ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
96         ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
97         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
98         ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
99         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
100         ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
101         ON_COMMAND_RANGE(ID_UNPACKERS_FIRST, ID_UNPACKERS_LAST, OnFileRecompareAs)
102         // [View] menu
103         ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
104         ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
105         ON_COMMAND(ID_SWAPPANES_SWAP12, (OnViewSwapPanes<0, 1>))
106         ON_COMMAND(ID_SWAPPANES_SWAP23, (OnViewSwapPanes<1, 2>))
107         ON_COMMAND(ID_SWAPPANES_SWAP13, (OnViewSwapPanes<0, 2>))
108         ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
109         ON_COMMAND(ID_REFRESH, OnRefresh)
110         // [Tools] menu
111         ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
112         ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
113         // [Plugins] menu
114         ON_COMMAND(ID_OPEN_WITH_UNPACKER, OnOpenWithUnpacker)
115         ON_COMMAND(ID_APPLY_PREDIFFER, OnApplyPrediffer)
116         ON_COMMAND_RANGE(ID_NO_PREDIFFER, ID_NO_PREDIFFER, OnPrediffer)
117         ON_COMMAND_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnPrediffer)
118         ON_UPDATE_COMMAND_UI(ID_NO_PREDIFFER, OnUpdatePrediffer)
119         ON_UPDATE_COMMAND_UI_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnUpdatePrediffer)
120         // Encoding Error dialog
121         ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
122         ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
123         ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
124         ON_COMMAND(IDOK, OnOK)  
125         // Status bar
126         ON_UPDATE_COMMAND_UI(ID_STATUS_PANE0FILE_RO, OnUpdateStatusRO)
127         ON_UPDATE_COMMAND_UI(ID_STATUS_PANE1FILE_RO, OnUpdateStatusRO)
128         ON_UPDATE_COMMAND_UI(ID_STATUS_PANE2FILE_RO, OnUpdateStatusRO)
129         ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
130         //}}AFX_MSG_MAP
131 END_MESSAGE_MAP()
132
133 /////////////////////////////////////////////////////////////////////////////
134 // CMergeDoc construction/destruction
135
136 /**
137  * @brief Constructor.
138  */
139 CMergeDoc::CMergeDoc()
140 : m_bEnableRescan(true)
141 , m_nCurDiff(-1)
142 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
143 , m_pDirDoc(nullptr)
144 , m_bMixedEol(false)
145 , m_pEncodingErrorBar(nullptr)
146 , m_bHasSyncPoints(false)
147 , m_bAutoMerged(false)
148 , m_nGroups(0)
149 , m_pView{nullptr}
150 , m_bAutomaticRescan(false)
151 , m_CurrentPredifferID(0)
152 , m_bChangedSchemeManually(false)
153 {
154         DIFFOPTIONS options = {0};
155
156         m_nBuffers = m_nBuffersTemp;
157         m_filePaths.SetSize(m_nBuffers);
158
159         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
160         {
161                 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
162                 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
163                 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
164                 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
165                 m_bEditAfterRescan[nBuffer] = false;
166         }
167
168         m_bEnableRescan = true;
169         m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
170
171         // COleDateTime m_LastRescan
172         curUndo = undoTgt.begin();
173         m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
174         m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
175
176         m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
177         Options::DiffOptions::Load(GetOptionsMgr(), options);
178
179         m_diffWrapper.SetOptions(&options);
180         m_diffWrapper.SetPrediffer(nullptr);
181 }
182
183 /**
184  * @brief Destructor.
185  *
186  * Informs associated dirdoc that mergedoc is closing.
187  */
188 CMergeDoc::~CMergeDoc()
189 {       
190         if (m_pDirDoc != nullptr)
191         {
192                 m_pDirDoc->MergeDocClosing(this);
193                 m_pDirDoc = nullptr;
194         }
195 }
196
197 /**
198  * @brief Deleted data associated with doc before closing.
199  */
200 void CMergeDoc::DeleteContents ()
201 {
202         CDocument::DeleteContents ();
203         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
204         {
205                 m_ptBuf[nBuffer]->FreeAll ();
206                 m_tempFiles[nBuffer].Delete();
207         }
208 }
209
210 /**
211  * @brief Called when new document is created.
212  *
213  * Initialises buffers.
214  */
215 BOOL CMergeDoc::OnNewDocument()
216 {
217         if (!CDocument::OnNewDocument())
218                 return false;
219
220         SetTitle(_("File Comparison").c_str());
221         
222         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
223                 m_ptBuf[nBuffer]->InitNew ();
224         return true;
225 }
226
227 /**
228  * @brief Return active merge edit view (or left one if neither active)
229  */
230 CMergeEditView * CMergeDoc::GetActiveMergeView()
231 {
232         CView * pActiveView = GetParentFrame()->GetActiveView();
233         CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
234         if (pMergeEditView == nullptr)
235                 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
236         return pMergeEditView;
237 }
238
239 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
240 {
241         return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
242 }
243
244 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
245 {
246         if (infoNewHandler != nullptr)
247         {
248                 m_infoUnpacker = *infoNewHandler;
249         }
250 }
251
252 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
253 {
254         m_diffWrapper.SetPrediffer(infoPrediffer);
255 }
256
257 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
258 {
259         m_diffWrapper.GetPrediffer(infoPrediffer);
260 }
261
262 const PrediffingInfo* CMergeDoc::GetPrediffer() const
263 {
264         static PrediffingInfo infoPrediffer;
265         m_diffWrapper.GetPrediffer(&infoPrediffer);
266         return &infoPrediffer;
267 }
268
269 /////////////////////////////////////////////////////////////////////////////
270 // CMergeDoc serialization
271
272 void CMergeDoc::Serialize(CArchive& ar)
273 {
274         ASSERT(false); // we do not use CDocument serialization
275 }
276
277 /////////////////////////////////////////////////////////////////////////////
278 // CMergeDoc commands
279
280 /**
281  * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
282  *
283  * @note 
284  * original file is Ansi : 
285  *   buffer  -> save as Ansi -> Ansi plugins -> diffutils
286  * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
287  *   buffer  -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
288  * (the plugins are optional, not the conversion)
289  * @todo Show SaveToFile() errors?
290  */
291 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
292 {
293         // and we don't repack the file
294         PackingInfo tempPacker(false);
295
296         // write buffer out to temporary file
297         String sError;
298         int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
299                 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
300 }
301
302 /**
303  * @brief Save files to temp files & compare again.
304  *
305  * @param bBinary [in,out] [in] If true, compare two binary files
306  * [out] If true binary file was detected.
307  * @param bIdentical [out] If true files were identical
308  * @param bForced [in] If true, suppressing is ignored and rescan
309  * is done always
310  * @return Tells if rescan was successfully, was suppressed, or
311  * error happened
312  * If this code is OK, Rescan has detached the views temporarily
313  * (positions of cursors have been lost)
314  * @note Rescan() ALWAYS compares temp files. Actual user files are not
315  * touched by Rescan().
316  * @sa CDiffWrapper::RunFileDiff()
317  */
318 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
319                 bool bForced /* =false */)
320 {
321         DIFFOPTIONS diffOptions = {0};
322         DiffFileInfo fileInfo;
323         bool diffSuccess = false;
324         int nResult = RESCAN_OK;
325         FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
326         int nBuffer;
327
328         if (!bForced)
329         {
330                 if (!m_bEnableRescan)
331                         return RESCAN_SUPPRESSED;
332         }
333
334         ClearWordDiffCache();
335
336         if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
337         {
338                 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
339         }
340         else
341         {
342                 m_diffWrapper.SetFilterList(_T(""));
343         }
344
345         if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
346         {
347                 m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
348         }
349         else
350         {
351                 m_diffWrapper.SetSubstitutionList(nullptr);
352         }
353
354         if (GetView(0, 0)->m_CurSourceDef->type != 0)
355                 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
356         else
357                 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
358
359         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
360         {
361                 // Check if files have been modified since last rescan
362                 // Ignore checking in case of scratchpads (empty filenames)
363                 if (!m_filePaths[nBuffer].empty())
364                 {
365                         Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
366                                         fileInfo, false, nBuffer);
367                 }
368         }
369         m_LastRescan = COleDateTime::GetCurrentTime();
370
371         LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
372         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
373         {
374                 if (Changed[nBuffer] == FileChange::Removed)
375                 {
376                         String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
377                         ShowMessageBox(msg, MB_ICONWARNING);
378                         bool bSaveResult = false;
379                         bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
380                         if (!ok || !bSaveResult)
381                         {
382                                 return RESCAN_FILE_ERR;
383                         }
384                 }
385
386                 String temp = m_tempFiles[nBuffer].GetPath();
387                 if (temp.empty())
388                         temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
389                 if (temp.empty())
390                         return RESCAN_TEMP_ERR;
391         }
392
393         CheckFileChanged();
394
395         String tempPath = env::GetTemporaryPath();
396
397         // Set up DiffWrapper
398         m_diffWrapper.GetOptions(&diffOptions);
399
400         // Clear diff list
401         m_diffList.Clear();
402         m_nCurDiff = -1;
403         m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
404         // Clear moved lines lists
405         if (m_diffWrapper.GetDetectMovedBlocks())
406         {
407                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
408                         m_diffWrapper.GetMovedLines(nBuffer)->Clear();
409         }
410
411         // Set paths for diffing and run diff
412         m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
413         if (m_nBuffers < 3)
414                 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
415         else
416                 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
417         m_diffWrapper.SetCompareFiles(m_filePaths);
418
419         DIFFSTATUS status;
420
421         if (!HasSyncPoints())
422         {
423                 // Save text buffer to file
424                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
425                 {
426                         m_ptBuf[nBuffer]->SetTempPath(tempPath);
427                         SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
428                 }
429
430                 m_diffWrapper.SetCreateDiffList(&m_diffList);
431                 diffSuccess = m_diffWrapper.RunFileDiff();
432
433                 // Read diff-status
434                 m_diffWrapper.GetDiffStatus(&status);
435                 if (bBinary) // believe caller if we were told these are binaries
436                         status.bBinaries = true;
437         }
438         else
439         {
440                 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();   
441                 int nStartLine[3] = {0};
442                 int nLines[3], nRealLine[3];
443                 for (size_t i = 0; i <= syncpoints.size(); ++i)
444                 {
445                         // Save text buffer to file
446                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
447                         {
448                                 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
449                                 m_ptBuf[nBuffer]->SetTempPath(tempPath);
450                                 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(), 
451                                         nStartLine[nBuffer], nLines[nBuffer]);
452                         }
453                         DiffList templist;
454                         templist.Clear();
455                         m_diffWrapper.SetCreateDiffList(&templist);
456                         diffSuccess = m_diffWrapper.RunFileDiff();
457                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
458                                 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
459
460                         // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
461                         if (i == 0 && templist.GetSize() > 0)
462                                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
463                                         if (nStartLine[nBuffer] == 0)
464                                         {
465                                                 bool isEmptyFile = true;
466                                                 for (int j = 0; j < nLines[nBuffer]; j++)
467                                                 {
468                                                         if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
469                                                         {
470                                                                 isEmptyFile = false;
471                                                                 break;
472                                                         }
473                                                 }
474                                                 if (isEmptyFile)
475                                                 {
476                                                         DIFFRANGE di;
477                                                         templist.GetDiff(0, di);
478                                                         if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
479                                                         {
480                                                                 di.end[nBuffer] = -1;
481                                                                 templist.SetDiff(0, di);
482                                                         }
483                                                 }
484                                         }
485
486                         m_diffList.AppendDiffList(templist, nRealLine);
487                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
488                                 nStartLine[nBuffer] += nLines[nBuffer];
489
490                         // Read diff-status
491                         DIFFSTATUS status_part;
492                         m_diffWrapper.GetDiffStatus(&status_part);
493                         if (bBinary) // believe caller if we were told these are binaries
494                                 status.bBinaries = true;
495                         status.MergeStatus(status_part);
496                 }
497                 m_diffWrapper.SetCreateDiffList(&m_diffList);
498         }
499
500         // If one file has EOL before EOF and other not...
501         if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
502         {
503                 // ..last DIFFRANGE of file which has EOL must be
504                 // fixed to contain last line too
505                 int lineCount[3] = { 0,0,0 };
506                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
507                         lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
508                 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
509         }
510
511         // set identical/diff result as recorded by diffutils
512         identical = status.Identical;
513
514         // Determine errors and binary file compares
515         if (!diffSuccess)
516                 nResult = RESCAN_FILE_ERR;
517         else if (status.bBinaries)
518         {
519                 bBinary = true;
520         }
521         else
522         {
523                 // Now update views and buffers for ghost lines
524
525                 // Prevent displaying views during this update 
526                 // BTW, this solves the problem of double asserts
527                 // (during the display of an assert message box, a second assert in one of the 
528                 //  display functions happens, and hides the first assert)
529                 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
530
531                 // Remove blank lines and clear winmerge flags
532                 // this operation does not change the modified flag
533                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
534                         m_ptBuf[nBuffer]->prepareForRescan();
535
536                 // Divide diff blocks to match lines.
537                 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
538                         AdjustDiffBlocks();
539
540                 // Analyse diff-list (updating real line-numbers)
541                 // this operation does not change the modified flag
542                 PrimeTextBuffers();
543
544                 // Hide identical lines if diff-context is not 'All'
545                 HideLines();
546
547                 // Apply flags to lines that are trivial
548                 PrediffingInfo infoPrediffer;
549                 GetPrediffer(&infoPrediffer);
550                 if (!infoPrediffer.GetPluginPipeline().empty())
551                         FlagTrivialLines();
552                 
553                 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
554                 if (m_diffWrapper.GetDetectMovedBlocks())
555                         FlagMovedLines();
556                 
557                 // After PrimeTextBuffers() we know amount of real diffs
558                 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
559
560                 // Identical files are also updated
561                 if (!m_diffList.HasSignificantDiffs())
562                         identical = IDENTLEVEL::ALL;
563
564                 ForEachView([](auto& pView) {
565                         // just apply some options to the views
566                         pView->PrimeListWithFile();
567                         // Now buffers data are valid
568                         pView->ReAttachToBuffer();
569                 });
570                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
571                 {
572                         m_bEditAfterRescan[nBuffer] = false;
573                 }
574         }
575
576         if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
577                 identical == IDENTLEVEL::ALL &&
578                 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
579                         [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
580                 identical = IDENTLEVEL::NONE;
581
582         GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
583
584         return nResult;
585 }
586
587 void CMergeDoc::CheckFileChanged(void)
588 {
589         int nBuffer;
590         DiffFileInfo fileInfo;
591         FileChange FileChange[3];
592
593         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
594         {
595                 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
596                         false, nBuffer);
597
598                 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
599         }
600
601         bool bDoReload = false;
602         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
603         {
604                 if (FileChange[nBuffer] == FileChange::Changed)
605                 {
606                         String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge scanned it last time.\n\nDo you want to reload the file?"), m_filePaths[nBuffer]);
607                         if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
608                                 bDoReload = true;
609                         break;
610                 }
611         }
612         if (bDoReload)
613         {
614                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
615                 {
616                         if (FileChange[nBuffer] == FileChange::Changed)
617                         {
618                                 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
619                                 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
620                         }
621                 }
622         }
623 }
624
625 /** @brief Apply flags to lines that are trivial */
626 void CMergeDoc::FlagTrivialLines(void)
627 {
628         for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
629         {
630                 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
631                 {
632                         String str[3];
633                         for (int file = 0; file < m_nBuffers; ++file)
634                         {
635                                 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
636                                 str[file] = p ? p : _T("");
637                         }
638
639                         if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
640                         {
641                                 DIFFOPTIONS diffOptions = {0};
642                                 m_diffWrapper.GetOptions(&diffOptions);
643
644                                 // Make the call to stringdiffs, which does all the hard & tedious computations
645                                 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
646                                         !diffOptions.bIgnoreCase,
647                                         !diffOptions.bIgnoreEol,
648                                         diffOptions.nIgnoreWhitespace,
649                                         GetBreakType(), // whitespace only or include punctuation
650                                         GetByteColoringOption());
651                                 if (!worddiffs.empty())
652                                 {
653                                         for (int file = 0; file < m_nBuffers; ++file)
654                                                 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
655                                 }
656                         }
657                 }
658         }
659 }
660
661 /** @brief Adjust all different lines that were detected as actually matching moved lines */
662 void CMergeDoc::FlagMovedLines(void)
663 {
664         int i;
665         MovedLines *pMovedLines;
666
667         pMovedLines = m_diffWrapper.GetMovedLines(0);
668         for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
669         {
670                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
671                 if (j != -1)
672                 {
673                         TRACE(_T("%d->%d\n"), i, j);
674                         ASSERT(j>=0);
675                         // We only flag lines that are already marked as being different
676                         int apparent = m_ptBuf[0]->ComputeApparentLine(i);
677                         if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
678                         {
679                                 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
680                                 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
681                                 {
682                                         int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
683                                         if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
684                                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
685                                 }
686                         }
687                 }
688         }
689
690         pMovedLines = m_diffWrapper.GetMovedLines(1);
691         for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
692         {
693                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
694                 if (j != -1)
695                 {
696                         TRACE(_T("%d->%d\n"), i, j);
697                         ASSERT(j>=0);
698                         // We only flag lines that are already marked as being different
699                         int apparent = m_ptBuf[1]->ComputeApparentLine(i);
700                         if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
701                         {
702                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
703                                 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
704                                 {
705                                         int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
706                                         if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
707                                                 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
708                                 }
709                         }
710                 }
711         }
712
713         if (m_nBuffers < 3)
714                 return;
715
716         pMovedLines = m_diffWrapper.GetMovedLines(1);
717         for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
718         {
719                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
720                 if (j != -1)
721                 {
722                         TRACE(_T("%d->%d\n"), i, j);
723                         ASSERT(j>=0);
724                         // We only flag lines that are already marked as being different
725                         int apparent = m_ptBuf[1]->ComputeApparentLine(i);
726                         if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
727                         {
728                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
729                                 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
730                                 {
731                                         int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
732                                         if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
733                                                 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
734                                 }
735                         }
736                 }
737         }
738
739         pMovedLines = m_diffWrapper.GetMovedLines(2);
740         for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
741         {
742                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
743                 if (j != -1)
744                 {
745                         TRACE(_T("%d->%d\n"), i, j);
746                         ASSERT(j>=0);
747                         // We only flag lines that are already marked as being different
748                         int apparent = m_ptBuf[2]->ComputeApparentLine(i);
749                         if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
750                         {
751                                 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
752                                 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
753                                 {
754                                         int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
755                                         if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
756                                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
757                                 }
758                         }
759                 }
760         }
761
762         // todo: Need to record actual moved information
763 }
764
765 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
766 {
767         if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
768         {
769                 GetParentFrame()->InitialUpdateFrame(this, true);
770                 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
771         }
772         return AfxMessageBox(sText.c_str(), nType, nIDHelp);
773 }
774
775 /**
776  * @brief Prints (error) message by rescan status.
777  *
778  * @param nRescanResult [in] Resultcocode from rescan().
779  * @param bIdentical [in] Were files identical?.
780  * @sa CMergeDoc::Rescan()
781  */
782 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
783 {
784         // Rescan was suppressed, there is no sensible status
785         if (nRescanResult == RESCAN_SUPPRESSED)
786                 return;
787
788         String s;
789
790         if (nRescanResult == RESCAN_FILE_ERR)
791         {
792                 s = _("An error occurred while comparing the files.");
793                 LogErrorString(s);
794                 ShowMessageBox(s, MB_ICONSTOP);
795                 return;
796         }
797
798         if (nRescanResult == RESCAN_TEMP_ERR)
799         {
800                 s = _("Temporary files could not be created. Check your temporary path settings.");
801                 LogErrorString(s);
802                 ShowMessageBox(s, MB_ICONSTOP);
803                 return;
804         }
805
806         // Files are not binaries, but they are identical
807         if (identical != IDENTLEVEL::NONE)
808         {
809                 CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
810                         [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
811         }
812 }
813
814 bool CMergeDoc::Undo()
815 {
816         return false;
817 }
818
819 /**
820  * @brief An instance of RescanSuppress prevents rescan during its lifetime
821  * (or until its Clear method is called, which ends its effect).
822  */
823 class RescanSuppress
824 {
825 public:
826         explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
827         {
828                 m_bSuppress = true;
829                 m_bPrev = doc.m_bEnableRescan;
830                 m_doc.m_bEnableRescan = false;
831         }
832         void Clear() 
833         {
834                 if (m_bSuppress)
835                 {
836                         m_bSuppress = false;
837                         m_doc.m_bEnableRescan = m_bPrev;
838                 }
839         }
840         ~RescanSuppress()
841         {
842                 Clear();
843         }
844 private:
845         CMergeDoc & m_doc;
846         bool m_bPrev;
847         bool m_bSuppress;
848 };
849
850 /**
851  * @brief Copy all diffs from one side to side.
852  * @param [in] srcPane Source side from which diff is copied
853  * @param [in] dstPane Destination side
854  */
855 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
856 {
857         CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
858 }
859
860 /**
861  * @brief Copy range of diffs from one side to side.
862  * This function copies given range of differences from side to another.
863  * Ignored differences are skipped, and not copied.
864  * @param [in] srcPane Source side from which diff is copied
865  * @param [in] dstPane Destination side
866  * @param [in] firstDiff First diff copied (0-based index)
867  * @param [in] lastDiff Last diff copied (0-based index)
868  */
869 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
870 {
871 #ifdef _DEBUG
872         if (firstDiff > lastDiff)
873                 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
874         if (firstDiff < 0)
875                 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
876         if (lastDiff > m_diffList.GetSize() - 1)
877                 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
878 #endif
879
880         lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
881         firstDiff = max(0, firstDiff);
882         if (firstDiff > lastDiff)
883                 return;
884         
885         RescanSuppress suppressRescan(*this);
886
887         // Note we don't care about m_nDiffs count to become zero,
888         // because we don't rescan() so it does not change
889
890         SetCurrentDiff(lastDiff);
891
892         bool bGroupWithPrevious = false;
893         if (firstWordDiff <= 0 && lastWordDiff == -1)
894         {
895                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
896                         return; // sync failure
897         }
898         else
899         {
900                 if (!WordListCopy(srcPane, dstPane, lastDiff, 
901                         (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
902                         return; // sync failure
903         }
904
905         SetEditedAfterRescan(dstPane);
906
907         int nGroup = GetActiveMergeView()->m_nThisGroup;
908         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
909         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
910         CPoint currentPosSrc = pViewSrc->GetCursorPos();
911         currentPosSrc.x = 0;
912         CPoint currentPosDst = pViewDst->GetCursorPos();
913         currentPosDst.x = 0;
914
915         CPoint pt(0, 0);
916         pViewDst->SetCursorPos(pt);
917         pViewDst->SetNewSelection(pt, pt, false);
918         pViewDst->SetNewAnchor(pt);
919
920         // copy from bottom up is more efficient
921         for (int i = lastDiff - 1; i >= firstDiff; --i)
922         {
923                 if (m_diffList.IsDiffSignificant(i))
924                 {
925                         SetCurrentDiff(i);
926                         const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
927                         if (currentPosDst.y > pdi->dend)
928                         {
929                                 if (pdi->blank[dstPane] >= 0)
930                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
931                                 else if (pdi->blank[srcPane] >= 0)
932                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
933                         }                       
934                         // Group merge with previous (merge undo data to one action)
935                         bGroupWithPrevious = true;
936                         if (i > firstDiff || firstWordDiff <= 0)
937                         {
938                                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
939                                         break; // sync failure
940                         }
941                         else
942                         {
943                                 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
944                                         break; // sync failure
945                         }
946                 }
947         }
948
949         ForEachView(dstPane, [currentPosDst](auto& pView) {
950                 pView->SetCursorPos(currentPosDst);
951                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
952                 pView->SetNewAnchor(currentPosDst);
953         });
954
955         suppressRescan.Clear(); // done suppress Rescan
956         FlushAndRescan();
957 }
958
959 void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
960         int firstLineDiff, int lastLineDiff)
961 {
962         lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
963         firstDiff = max(0, firstDiff);
964         if (firstDiff > lastDiff)
965                 return;
966         
967         RescanSuppress suppressRescan(*this);
968
969         bool bGroupWithPrevious = false;
970         if (firstLineDiff <= 0 && lastLineDiff == -1)
971         {
972                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
973                         return; // sync failure
974         }
975         else
976         {
977                 if (!PartialListCopy(srcPane, dstPane, lastDiff, 
978                         (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
979                         return; // sync failure
980         }
981
982
983         SetEditedAfterRescan(dstPane);
984
985         int nGroup = GetActiveMergeView()->m_nThisGroup;
986         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
987         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
988         CPoint currentPosSrc = pViewSrc->GetCursorPos();
989         currentPosSrc.x = 0;
990         CPoint currentPosDst = pViewDst->GetCursorPos();
991         currentPosDst.x = 0;
992
993         CPoint pt(0, 0);
994         pViewDst->SetCursorPos(pt);
995         pViewDst->SetNewSelection(pt, pt, false);
996         pViewDst->SetNewAnchor(pt);
997
998         // copy from bottom up is more efficient
999         for (int i = lastDiff - 1; i >= firstDiff; --i)
1000         {
1001                 if (m_diffList.IsDiffSignificant(i))
1002                 {
1003                         SetCurrentDiff(i);
1004                         const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1005                         if (currentPosDst.y > pdi->dend)
1006                         {
1007                                 if (pdi->blank[dstPane] >= 0)
1008                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1009                                 else if (pdi->blank[srcPane] >= 0)
1010                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1011                         }                       
1012                         // Group merge with previous (merge undo data to one action)
1013                         bGroupWithPrevious = true;
1014                         if (i > firstDiff || firstLineDiff <= 0)
1015                         {
1016                                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1017                                         break; // sync failure
1018                         }
1019                         else
1020                         {
1021                                 if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
1022                                         break; // sync failure
1023                         }
1024                 }
1025         }
1026
1027         ForEachView(dstPane, [currentPosDst](auto& pView) {
1028                 pView->SetCursorPos(currentPosDst);
1029                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1030                 pView->SetNewAnchor(currentPosDst);
1031         });
1032
1033         suppressRescan.Clear(); // done suppress Rescan
1034         FlushAndRescan();
1035 }
1036
1037 enum MergeResult { NoMergeNeeded, Merged, Conflict };
1038
1039 template<class Type>
1040 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
1041 {
1042         bool equal_all = middle == left && middle == right && left == right;
1043         if (equal_all)
1044                 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1045         bool conflict = middle != left && middle != right && left != right;
1046         if (conflict)
1047                 return std::pair<MergeResult, Type>(Conflict, left);
1048         switch (dstPane)
1049         {
1050         case 0:
1051                 if (left == right)
1052                         return std::pair<MergeResult, Type>(Merged, middle);
1053                 break;
1054         case 1:
1055                 if (left == middle)
1056                         return std::pair<MergeResult, Type>(Merged, right);
1057                 else
1058                         return std::pair<MergeResult, Type>(Merged, left);
1059                 break;
1060         case 2:
1061                 if (left == right)
1062                         return std::pair<MergeResult, Type>(Merged, middle);
1063                 break;
1064         }
1065         return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1066 }
1067
1068 /**
1069  * @brief Do auto-merge.
1070  * @param [in] dstPane Destination side
1071  */
1072 void CMergeDoc::DoAutoMerge(int dstPane)
1073 {
1074         const int lastDiff = m_diffList.GetSize() - 1;
1075         const int firstDiff = 0;
1076         bool bGroupWithPrevious = false;
1077         int autoMergedCount = 0;
1078         int unresolvedConflictCount = 0;
1079
1080         std::pair<MergeResult, FileTextEncoding> mergedEncoding = 
1081                 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
1082         if (mergedEncoding.first == Merged)
1083         {
1084                 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
1085                 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
1086         }
1087         else if (mergedEncoding.first == Conflict)
1088                 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
1089
1090         std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle = 
1091                 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
1092         if (mergedEOLStyle.first == Merged)
1093         {
1094                 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1095                 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1096         }
1097         else if (mergedEOLStyle.first == Conflict)
1098                 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1099
1100         RescanSuppress suppressRescan(*this);
1101
1102         // Note we don't care about m_nDiffs count to become zero,
1103         // because we don't rescan() so it does not change
1104
1105         SetCurrentDiff(lastDiff);
1106
1107         SetEditedAfterRescan(dstPane);
1108
1109         int nGroup = GetActiveMergeView()->m_nThisGroup;
1110         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1111         CPoint currentPosDst = pViewDst->GetCursorPos();
1112         currentPosDst.x = 0;
1113
1114         CPoint pt(0, 0);
1115         pViewDst->SetCursorPos(pt);
1116         pViewDst->SetNewSelection(pt, pt, false);
1117         pViewDst->SetNewAnchor(pt);
1118
1119         // copy from bottom up is more efficient
1120         for (int i = lastDiff; i >= firstDiff; --i)
1121         {
1122                 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1123                 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1124                 if (srcPane != -1)
1125                 {
1126                         SetCurrentDiff(i);
1127                         if (currentPosDst.y > pdi->dend)
1128                         {
1129                                 if (pdi->blank[dstPane] >= 0)
1130                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1131                                 else if (pdi->blank[srcPane] >= 0)
1132                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1133                         }                       
1134                         // Group merge with previous (merge undo data to one action)
1135                         if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1136                                 break; // sync failure
1137                         if (!bGroupWithPrevious)
1138                                 bGroupWithPrevious = true;
1139                         ++autoMergedCount;
1140                 }
1141                 if (pdi->op == OP_DIFF)
1142                         ++unresolvedConflictCount;
1143         }
1144
1145         ForEachView(dstPane, [currentPosDst](auto& pView) {
1146                 pView->SetCursorPos(currentPosDst);
1147                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1148                 pView->SetNewAnchor(currentPosDst);
1149         });
1150
1151         suppressRescan.Clear(); // done suppress Rescan
1152         FlushAndRescan();
1153         UpdateHeaderPath(dstPane);
1154
1155         if (autoMergedCount > 0)
1156                 m_bAutoMerged = true;
1157
1158         // move to first conflict 
1159         const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1160         if (nDiff != -1)
1161                 pViewDst->SelectDiff(nDiff, true, false);
1162
1163         ShowMessageBox(
1164                 strutils::format_string2(
1165                         _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"), 
1166                         strutils::format(_T("%d"), autoMergedCount),
1167                         strutils::format(_T("%d"), unresolvedConflictCount)),
1168                 MB_ICONINFORMATION);
1169 }
1170
1171 /**
1172  * @brief Sanity check difference.
1173  *
1174  * Checks that lines in difference are inside difference in both files.
1175  * If file is edited, lines added or removed diff lines get out of sync and
1176  * merging fails miserably.
1177  *
1178  * @param [in] dr Difference to check.
1179  * @return true if difference lines match, false otherwise.
1180  */
1181 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1182 {
1183         const int cd_dbegin = dr.dbegin;
1184         const int cd_dend = dr.dend;
1185
1186         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1187         {
1188                 // Must ensure line number is in range before getting line flags
1189                 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1190                         return false;
1191
1192                 // Optimization - check last line first so we don't need to
1193                 // check whole diff for obvious cases
1194                 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1195                 if (!(dwFlags & LF_WINMERGE_FLAGS))
1196                         return false;
1197         }
1198
1199         for (int line = cd_dbegin; line < cd_dend; line++)
1200         {
1201                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1202                 {
1203                         DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1204                         if (!(dwFlags & LF_WINMERGE_FLAGS))
1205                                 return false;
1206                 }
1207         }
1208         return true;
1209 }
1210
1211 /**
1212  * @brief Copy selected (=current) difference from from side to side.
1213  * @param [in] srcPane Source side from which diff is copied
1214  * @param [in] dstPane Destination side
1215  * @param [in] nDiff Diff to copy, if -1 function determines it.
1216  * @param [in] bGroupWithPrevious Adds diff to same undo group with
1217  * @return true if ok, false if sync failure & need to abort copy
1218  * previous action (allows one undo for copy all)
1219  */
1220 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1221                 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1222 {
1223         int nGroup = GetActiveMergeView()->m_nThisGroup;
1224         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1225         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1226         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1227
1228         // suppress Rescan during this method
1229         // (Not only do we not want to rescan a lot of times, but
1230         // it will wreck the line status array to rescan as we merge)
1231         RescanSuppress suppressRescan(*this);
1232
1233         // If diff-number not given, determine it from active view
1234         if (nDiff == -1)
1235         {
1236                 nDiff = GetCurrentDiff();
1237
1238                 // No current diff, but maybe cursor is in diff?
1239                 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1240                         pViewDst->IsCursorInDiff()))
1241                 {
1242                         // Find out diff under cursor
1243                         CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1244                         nDiff = m_diffList.LineToDiff(ptCursor.y);
1245                 }
1246         }
1247
1248         if (nDiff != -1)
1249         {
1250                 DIFFRANGE cd;
1251                 VERIFY(m_diffList.GetDiff(nDiff, cd));
1252                 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1253                 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1254                 bool bSrcWasMod = sbuf.IsModified();
1255                 const int cd_dbegin = cd.dbegin;
1256                 const int cd_dend = cd.dend;
1257                 const int cd_blank = cd.blank[srcPane];
1258                 bool bInSync = SanityCheckDiff(cd);
1259
1260                 if (!bInSync)
1261                 {
1262                         LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1263                         return false; // abort copying
1264                 }
1265
1266                 // If we remove whole diff from current view, we must fix cursor
1267                 // position first. Normally we would move to end of previous line,
1268                 // but we want to move to begin of that line for usability.
1269                 if (bUpdateView)
1270                 {
1271                         CPoint currentPos = pViewDst->GetCursorPos();
1272                         currentPos.x = 0;
1273                         if (currentPos.y > cd_dend)
1274                         {
1275                                 if (cd.blank[dstPane] >= 0)
1276                                         currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1277                                 else if (cd.blank[srcPane] >= 0)
1278                                         currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1279                         }
1280                         ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1281                 }
1282
1283                 // if the current diff contains missing lines, remove them from both sides
1284                 int limit = cd_dend;
1285
1286                 // curView is the view which is changed, so the opposite of the source view
1287                 dbuf.BeginUndoGroup(bGroupWithPrevious);
1288                 if (cd_blank>=0)
1289                 {
1290                         // text was missing, so delete rest of lines on both sides
1291                         // delete only on destination side since rescan will clear the other side
1292                         if (cd_dend + 1 < dbuf.GetLineCount())
1293                         {
1294                                 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1295                         }
1296                         else
1297                         {
1298                                 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1299                                 ASSERT(cd_blank > 0);
1300                                 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1301                         }
1302
1303                         limit=cd_blank-1;
1304                         dbuf.FlushUndoGroup(pSource);
1305                         dbuf.BeginUndoGroup(true);
1306                 }
1307
1308
1309                 // copy the selected text over
1310                 if (cd_dbegin <= limit)
1311                 {
1312                         // text exists on left side, so just replace
1313                         dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1314                         dbuf.FlushUndoGroup(pSource);
1315                         dbuf.BeginUndoGroup(true);
1316                 }
1317                 dbuf.FlushUndoGroup(pSource);
1318
1319                 // remove the diff
1320                 SetCurrentDiff(-1);
1321
1322                 // reset the mod status of the source view because we do make some
1323                 // changes, but none that concern the source text
1324                 sbuf.SetModified(bSrcWasMod);
1325         }
1326
1327         suppressRescan.Clear(); // done suppress Rescan
1328         FlushAndRescan();
1329         return true;
1330 }
1331
1332 bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
1333         bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1334 {
1335         int nGroup = GetActiveMergeView()->m_nThisGroup;
1336         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1337         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1338         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1339
1340         // suppress Rescan during this method
1341         // (Not only do we not want to rescan a lot of times, but
1342         // it will wreck the line status array to rescan as we merge)
1343         RescanSuppress suppressRescan(*this);
1344
1345         DIFFRANGE cd;
1346         VERIFY(m_diffList.GetDiff(nDiff, cd));
1347         CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1348         CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1349         bool bSrcWasMod = sbuf.IsModified();
1350         const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
1351         const int cd_dend = cd.dend;
1352         const int cd_blank = cd.blank[srcPane];
1353         bool bInSync = SanityCheckDiff(cd);
1354
1355         if (!bInSync)
1356         {
1357                 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1358                 return false; // abort copying
1359         }
1360
1361         // If we remove whole diff from current view, we must fix cursor
1362         // position first. Normally we would move to end of previous line,
1363         // but we want to move to begin of that line for usability.
1364         if (bUpdateView)
1365         {
1366                 CPoint currentPos = pViewDst->GetCursorPos();
1367                 currentPos.x = 0;
1368                 if (currentPos.y > cd_dend)
1369                 {
1370                         if (cd.blank[dstPane] >= 0)
1371                                 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1372                         else if (cd.blank[srcPane] >= 0)
1373                                 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1374                 }
1375                 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1376         }
1377
1378         // if the current diff contains missing lines, remove them from both sides
1379         int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
1380
1381         // curView is the view which is changed, so the opposite of the source view
1382         dbuf.BeginUndoGroup(bGroupWithPrevious);
1383         if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
1384         {
1385                 // text was missing, so delete rest of lines on both sides
1386                 // delete only on destination side since rescan will clear the other side
1387                 if (limit+1 < dbuf.GetLineCount())
1388                 {
1389                         dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
1390                 }
1391                 else
1392                 {
1393                         // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1394                         ASSERT(cd_dbegin > 0);
1395                         dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
1396                 }
1397
1398                 limit = cd_dbegin-1;
1399                 dbuf.FlushUndoGroup(pSource);
1400                 dbuf.BeginUndoGroup(true);
1401         }
1402
1403         // copy the selected text over
1404         if (cd_dbegin <= limit)
1405         {
1406                 // text exists on left side, so just replace
1407                 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1408                 dbuf.FlushUndoGroup(pSource);
1409                 dbuf.BeginUndoGroup(true);
1410         }
1411         dbuf.FlushUndoGroup(pSource);
1412
1413         // remove the diff
1414         SetCurrentDiff(-1);
1415
1416         // reset the mod status of the source view because we do make some
1417         // changes, but none that concern the source text
1418         sbuf.SetModified(bSrcWasMod);
1419
1420         suppressRescan.Clear(); // done suppress Rescan
1421         FlushAndRescan();
1422         return true;
1423 }
1424
1425 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1426                 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1427 {
1428         int nGroup = GetActiveMergeView()->m_nThisGroup;
1429         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1430         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1431         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1432
1433         // suppress Rescan during this method
1434         // (Not only do we not want to rescan a lot of times, but
1435         // it will wreck the line status array to rescan as we merge)
1436         RescanSuppress suppressRescan(*this);
1437
1438         DIFFRANGE cd;
1439         VERIFY(m_diffList.GetDiff(nDiff, cd));
1440         CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1441         CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1442         bool bSrcWasMod = sbuf.IsModified();
1443         const int cd_dbegin = cd.dbegin;
1444         const int cd_dend = cd.dend;
1445         const int cd_blank = cd.blank[srcPane];
1446         bool bInSync = SanityCheckDiff(cd);
1447
1448         if (!bInSync)
1449         {
1450                 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1451                 return false; // abort copying
1452         }
1453
1454         std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1455
1456         if (worddiffs.empty())
1457                 return false;
1458
1459         if (cd.end[srcPane] < cd.begin[srcPane])
1460                 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1461
1462         if (firstWordDiff == -1)
1463                 firstWordDiff = 0;
1464         if (lastWordDiff == -1)
1465                 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1466
1467         // If we remove whole diff from current view, we must fix cursor
1468         // position first. Normally we would move to end of previous line,
1469         // but we want to move to begin of that line for usability.
1470         if (bUpdateView)
1471         {
1472                 CPoint currentPos = pViewDst->GetCursorPos();
1473                 currentPos.x = 0;
1474                 if (currentPos.y > cd_dend)
1475                 {
1476                         if (cd.blank[dstPane] >= 0)
1477                                 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1478                         else if (cd.blank[srcPane] >= 0)
1479                                 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1480                 }
1481                 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1482         }
1483
1484         // if the current diff contains missing lines, remove them from both sides
1485         int limit = cd_dend;
1486
1487         // curView is the view which is changed, so the opposite of the source view
1488         dbuf.BeginUndoGroup(bGroupWithPrevious);
1489
1490         CString srcText, dstText;
1491         CPoint ptDstStart, ptDstEnd;
1492         CPoint ptSrcStart, ptSrcEnd;
1493
1494         ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1495         ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1496         ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1497         ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1498         ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1499         ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1500         ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1501         ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1502
1503         std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1504         std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1505
1506         dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1507         sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1508
1509         nDstOffsets[0] = 0;
1510         for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1511                 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1512         nSrcOffsets[0] = 0;
1513         for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1514                 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1515
1516         for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1517         {
1518                 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1519                         continue;
1520                 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1521                 int srcEnd   = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1522                 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1523                 int dstEnd   = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1524                 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1525                         + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1526                         + dstText.Mid(dstEnd - ptDstStart.x);
1527         }
1528
1529         dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1530
1531         int endl,endc;
1532         dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1533
1534         dbuf.FlushUndoGroup(pSource);
1535
1536         // reset the mod status of the source view because we do make some
1537         // changes, but none that concern the source text
1538         sbuf.SetModified(bSrcWasMod);
1539
1540         suppressRescan.Clear(); // done suppress Rescan
1541         FlushAndRescan();
1542
1543         return true;
1544 }
1545
1546 /**
1547  * @brief Save file with new filename.
1548  *
1549  * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1550  * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1551  * normal filename fails, to let user choose another filename/location.
1552  * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1553  * using this function.
1554  * @param [in, out] strPath 
1555  * - [in] : Initial path shown to user
1556  * - [out] : Path to new filename if saving succeeds
1557  * @param [in, out] nSaveResult 
1558  * - [in] : Statuscode telling why we ended up here. Maybe the result of
1559  * previous save.
1560  * - [out] : Statuscode of this saving try
1561  * @param [in, out] sError Error string from lower level saving code
1562  * @param [in] nBuffer Buffer we are saving
1563  * @return false as long as the user is not satisfied. Calling function
1564  * should not continue until true is returned.
1565  * @sa CMergeDoc::DoSave()
1566  * @sa CMergeDoc::DoSaveAs()
1567  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1568  */
1569 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1570         int nBuffer, PackingInfo& infoTempUnpacker)
1571 {
1572         String s;
1573         String str;
1574         String strSavePath; // New path for next saving try
1575         String title;
1576         bool result = true;
1577         int answer = IDOK; // Set default we use for scratchpads
1578
1579         // We shouldn't get here if saving is succeed before
1580         ASSERT(nSaveResult != SAVE_DONE);
1581
1582         // Select message based on reason function called
1583         if (nSaveResult == SAVE_PACK_FAILED)
1584         {
1585                 str = CMergeApp::GetPackingErrorMessage(nBuffer, m_nBuffers, strPath, infoTempUnpacker);
1586                 // replace the unpacker with a "do nothing" unpacker
1587                 infoTempUnpacker.Initialize(false);
1588         }
1589         else
1590         {
1591                 str = strutils::format_string2(_("Saving file failed.\n%1\n%2\nDo you want to:\n\t- use a different filename (Press OK)\n\t- abort the current operation (Press Cancel)?"), strPath, sError);
1592         }
1593
1594         // SAVE_NO_FILENAME is temporarily used for scratchpad.
1595         // So don't ask about saving in that case.
1596         if (nSaveResult != SAVE_NO_FILENAME)
1597                 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1598
1599         switch (answer)
1600         {
1601         case IDOK:
1602                 if (nBuffer == 0)
1603                         title = _("Save Left File As");
1604                 else if (nBuffer == m_nBuffers - 1)
1605                         title = _("Save Right File As");
1606                 else
1607                         title = _("Save Middle File As");
1608
1609                 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1610                 {
1611                         CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1612                         strSavePath = s;
1613                         nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1614                                 infoTempUnpacker);
1615
1616                         if (nSaveResult == SAVE_DONE)
1617                         {
1618                                 // We are saving scratchpad (unnamed file)
1619                                 if (strPath.empty())
1620                                 {
1621                                         m_nBufferType[nBuffer] = BUFFERTYPE::UNNAMED_SAVED;
1622                                         m_strDesc[nBuffer].erase();
1623                                 }
1624                                         
1625                                 strPath = strSavePath;
1626                                 UpdateHeaderPath(nBuffer);
1627                         }
1628                         else
1629                                 result = false;
1630                 }
1631                 else
1632                         nSaveResult = SAVE_CANCELLED;
1633                 break;
1634
1635         case IDCANCEL:
1636                 nSaveResult = SAVE_CANCELLED;
1637                 break;
1638         }
1639         return result;
1640 }
1641
1642 /**
1643  * @brief Save file creating backups etc.
1644  *
1645  * Safe top-level file saving function. Checks validity of given path.
1646  * Creates backup file if wanted to. And if saving to given path fails,
1647  * allows user to select new location/name for file.
1648  * @param [in] szPath Path where to save including filename. Can be
1649  * empty/`nullptr` if new file is created (scratchpad) without filename.
1650  * @param [out] bSaveSuccess Will contain information about save success with
1651  * the original name (to determine if file statuses should be changed)
1652  * @param [in] nBuffer Index (0-based) of buffer to save
1653  * @return Tells if caller can continue (no errors happened)
1654  * @note Return value does not tell if SAVING succeeded. Caller must
1655  * Check value of bSaveSuccess parameter after calling this function!
1656  * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1657  * to directory it points to. If m_strSaveAsPath contains filename,
1658  * that filename is used.
1659  * @sa CMergeDoc::TrySaveAs()
1660  * @sa CMainFrame::CheckSavePath()
1661  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1662  */
1663 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1664 {
1665         DiffFileInfo fileInfo;
1666         String strSavePath(szPath);
1667         FileChange fileChanged;
1668         bool bApplyToAll = false;       
1669         int nRetVal = -1;
1670
1671         fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1672         if (fileChanged == FileChange::Changed)
1673         {
1674                 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1675                 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1676                 {
1677                         bSaveSuccess = true;
1678                         return true;
1679                 }               
1680         }
1681
1682         // use a temp packer
1683         // first copy the m_infoUnpacker
1684         // if an error arises during packing, change and take a "do nothing" packer
1685         PackingInfo infoTempUnpacker = m_infoUnpacker;
1686
1687         bSaveSuccess = false;
1688         
1689         // Check third arg possibly given from command-line
1690         if (!theApp.m_strSaveAsPath.empty())
1691         {
1692                 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1693                 {
1694                         // third arg was a directory, so get append the filename
1695                         String sname;
1696                         paths::SplitFilename(szPath, 0, &sname, 0);
1697                         strSavePath = theApp.m_strSaveAsPath;
1698                         strSavePath = paths::ConcatPath(strSavePath, sname);
1699                 }
1700                 else
1701                         strSavePath = theApp.m_strSaveAsPath;   
1702         }
1703
1704         nRetVal = CMergeApp::HandleReadonlySave(strSavePath, false, bApplyToAll);
1705         if (nRetVal == IDCANCEL)
1706                 return false;
1707
1708         if (!CMergeApp::CreateBackup(false, strSavePath))
1709                 return false;
1710
1711         // false as long as the user is not satisfied
1712         // true if saving succeeds, even with another filename, or if the user cancels
1713         bool result = false;
1714         // the error code from the latest save operation, 
1715         // or SAVE_DONE when the save succeeds
1716         // TODO: Shall we return this code in addition to bSaveSuccess ?
1717         int nSaveErrorCode = SAVE_DONE;
1718         CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1719
1720         // Assume empty filename means Scratchpad (unnamed file)
1721         // Todo: This is not needed? - buffer type check should be enough
1722         if (strSavePath.empty())
1723                 nSaveErrorCode = SAVE_NO_FILENAME;
1724
1725         // Handle unnamed buffers
1726         if (m_nBufferType[nBuffer] == BUFFERTYPE::UNNAMED)
1727                 nSaveErrorCode = SAVE_NO_FILENAME;
1728
1729         String sError;
1730         if (nSaveErrorCode == SAVE_DONE)
1731                 // We have a filename, just try to save
1732                 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, infoTempUnpacker);
1733
1734         if (nSaveErrorCode != SAVE_DONE)
1735         {
1736                 // Saving failed, user may save to another location if wants to
1737                 do
1738                         result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1739                 while (!result);
1740         }
1741
1742         // Saving succeeded with given/selected filename
1743         if (nSaveErrorCode == SAVE_DONE)
1744         {
1745                 // Preserve file times if user wants to
1746                 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1747                 {
1748                         fileInfo.SetFile(strSavePath);
1749                         try
1750                         {
1751                                 TFile file(strSavePath);
1752                                 file.setLastModified(fileInfo.mtime);
1753                         }
1754                         catch (...)
1755                         {
1756                         }
1757                 }
1758
1759                 m_ptBuf[nBuffer]->SetModified(false);
1760                 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1761                 m_filePaths[nBuffer] = strSavePath;
1762                 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1763                 UpdateHeaderPath(nBuffer);
1764                 bSaveSuccess = true;
1765                 result = true;
1766         }
1767         else if (nSaveErrorCode == SAVE_CANCELLED)
1768         {
1769                 // User cancelled current operation, lets do what user wanted to do
1770                 result = false;
1771         }
1772         return result;
1773 }
1774
1775 /**
1776  * @brief Save file with different filename.
1777  *
1778  * Safe top-level file saving function. Asks user to select filename
1779  * and path. Does not create backups.
1780  * @param [in] szPath Path where to save including filename. Can be
1781  * empty/`nullptr` if new file is created (scratchpad) without filename.
1782  * @param [out] bSaveSuccess Will contain information about save success with
1783  * the original name (to determine if file statuses should be changed)
1784  * @param [in] nBuffer Index (0-based) of buffer to save
1785  * @return Tells if caller can continue (no errors happened)
1786  * @note Return value does not tell if SAVING succeeded. Caller must
1787  * Check value of bSaveSuccess parameter after calling this function!
1788  * @sa CMergeDoc::TrySaveAs()
1789  * @sa CMainFrame::CheckSavePath()
1790  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1791  */
1792 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1793 {
1794         String strSavePath(szPath);
1795
1796         // use a temp packer
1797         // first copy the m_infoUnpacker
1798         // if an error arises during packing, change and take a "do nothing" packer
1799         PackingInfo infoTempUnpacker = m_infoUnpacker;
1800
1801         bSaveSuccess = false;
1802         // false as long as the user is not satisfied
1803         // true if saving succeeds, even with another filename, or if the user cancels
1804         bool result = false;
1805         // the error code from the latest save operation, 
1806         // or SAVE_DONE when the save succeeds
1807         // TODO: Shall we return this code in addition to bSaveSuccess ?
1808         int nSaveErrorCode = SAVE_DONE;
1809
1810         // Use SAVE_NO_FILENAME to prevent asking about error
1811         nSaveErrorCode = SAVE_NO_FILENAME;
1812
1813         // Loop until user succeeds saving or cancels
1814         String sError;
1815         do
1816                 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1817         while (!result);
1818
1819         // Saving succeeded with given/selected filename
1820         if (nSaveErrorCode == SAVE_DONE)
1821         {
1822                 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1823                 m_filePaths[nBuffer] = strSavePath;
1824                 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1825                 UpdateHeaderPath(nBuffer);
1826                 bSaveSuccess = true;
1827                 result = true;
1828         }
1829         return result;
1830 }
1831
1832 /**
1833  * @brief Get left->right info for a moved line (apparent line number)
1834  */
1835 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1836 {
1837         if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1838                 return -1;
1839
1840         int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1841         int realRightLine = -1;
1842         if (m_diffWrapper.GetDetectMovedBlocks())
1843         {
1844                 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1845                                 MovedLines::SIDE::RIGHT);
1846         }
1847         if (realRightLine != -1)
1848                 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1849         else
1850                 return -1;
1851 }
1852
1853 /**
1854  * @brief Get right->left info for a moved line (apparent line number)
1855  */
1856 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1857 {
1858         if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1859                 return -1;
1860
1861         int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1862         int realLeftLine = -1;
1863         if (m_diffWrapper.GetDetectMovedBlocks())
1864         {
1865                 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1866                                 MovedLines::SIDE::LEFT);
1867         }
1868         if (realLeftLine != -1)
1869                 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1870         else
1871                 return -1;
1872 }
1873
1874 /**
1875  * @brief Save modified documents.
1876  * This function asks if user wants to save modified documents. We also
1877  * allow user to cancel the closing.
1878  *
1879  * There is a special trick avoiding showing two save-dialogs, as MFC framework
1880  * sometimes calls this function twice. We use static counter for these calls
1881  * and if we already have saving in progress (counter == 1) we skip the new
1882  * saving dialog.
1883  *
1884  * @return true if docs are closed, false if closing is cancelled.
1885  */
1886 BOOL CMergeDoc::SaveModified()
1887 {
1888         static int counter;
1889         ++counter;
1890         if (counter > 1)
1891                 return false;
1892
1893         if (PromptAndSaveIfNeeded(true))
1894         {
1895                 counter = 0;
1896                 return true;
1897         }
1898         else
1899         {
1900                 counter = 0;
1901                 return false;
1902         }
1903 }
1904
1905 /**
1906  * @brief Sets the current difference.
1907  * @param [in] nDiff Difference to set as current difference.
1908  */
1909 void CMergeDoc::SetCurrentDiff(int nDiff)
1910 {
1911         if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1912                 m_nCurDiff = nDiff;
1913         else
1914                 m_nCurDiff = -1;
1915 }
1916
1917 /**
1918  * @brief Take care of rescanning document.
1919  * 
1920  * Update view and restore cursor and scroll position after
1921  * rescanning document.
1922  * @param [in] bForced If true rescan cannot be suppressed
1923  */
1924 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1925 {
1926         // Ignore suppressing when forced rescan
1927         if (!bForced)
1928                 if (!m_bEnableRescan) return;
1929
1930         CWaitCursor waitstatus;
1931
1932         CMergeEditView *pActiveView = GetActiveMergeView();
1933
1934         // store cursors and hide caret
1935         ForEachView([](auto& pView) { pView->PushCursors(); });
1936         pActiveView->HideCursor();
1937
1938         bool bBinary = false;
1939         IDENTLEVEL identical = IDENTLEVEL::NONE;
1940         int nRescanResult = Rescan(bBinary, identical, bForced);
1941
1942         // restore cursors and caret
1943         ForEachView([](auto& pView) { pView->PopCursors(); });
1944         pActiveView->ShowCursor();
1945
1946         ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1947                 // because of ghostlines, m_nTopLine may differ just after Rescan
1948                 // scroll both views to the same top line
1949                 pView->UpdateSiblingScrollPos(false);
1950         });
1951         // make sure we see the cursor from the curent view
1952         pActiveView->EnsureVisible(pActiveView->GetCursorPos());
1953
1954         // Refresh display
1955         UpdateAllViews(nullptr);
1956
1957         // Show possible error after updating screen
1958         if (nRescanResult != RESCAN_SUPPRESSED)
1959                 ShowRescanError(nRescanResult, identical);
1960         m_LastRescan = COleDateTime::GetCurrentTime();
1961 }
1962
1963 /**
1964  * @brief Saves both files
1965  */
1966 void CMergeDoc::OnFileSave() 
1967 {
1968         // We will need to know if either of the originals actually changed
1969         // so we know whether to update the diff status
1970         bool bChangedOriginal = false;
1971
1972         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1973         {
1974                 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1975                 {
1976                         // (why we don't use return value of DoSave)
1977                         // DoSave will return true if it wrote to something successfully
1978                         // but we have to know if it overwrote the original file
1979                         bool bSaveOriginal = false;
1980                         DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1981                         if (bSaveOriginal)
1982                                 bChangedOriginal = true;
1983                 }
1984         }
1985
1986         // If either of the actual source files being compared was changed
1987         // we need to update status in the dir view
1988         if (bChangedOriginal)
1989         {
1990                 // If DirDoc contains diffs
1991                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1992                 {
1993                         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1994                         {
1995                                 if (m_bEditAfterRescan[nBuffer])
1996                                 {
1997                                         FlushAndRescan(false);
1998                                         break;
1999                                 }
2000                         }
2001
2002                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2003                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2004                                         m_nTrivialDiffs, bIdentical);
2005                 }
2006         }
2007 }
2008
2009 void CMergeDoc::DoFileSave(int nBuffer)
2010 {
2011         bool bSaveSuccess = false;
2012         bool bModified = false;
2013
2014         if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
2015         {
2016                 bModified = true;
2017                 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
2018         }
2019
2020         // If file were modified and saving succeeded,
2021         // update status on dir view
2022         if (bModified && bSaveSuccess)
2023         {
2024                 // If DirDoc contains compare results
2025                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2026                 {
2027                         for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
2028                         {
2029                                 if (m_bEditAfterRescan[nBuffer1])
2030                                 {
2031                                         FlushAndRescan(false);
2032                                         break;
2033                                 }
2034                         }
2035
2036                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2037                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2038                                         m_nTrivialDiffs, bIdentical);
2039                 }
2040         }
2041 }
2042
2043 /**
2044  * @brief Saves left-side file
2045  */
2046 void CMergeDoc::OnFileSaveLeft()
2047 {
2048         DoFileSave(0);
2049 }
2050
2051 /**
2052  * @brief Saves middle-side file
2053  */
2054 void CMergeDoc::OnFileSaveMiddle()
2055 {
2056         DoFileSave(1);
2057 }
2058
2059 /**
2060  * @brief Saves right-side file
2061  */
2062 void CMergeDoc::OnFileSaveRight()
2063 {
2064         DoFileSave(m_nBuffers - 1);
2065 }
2066
2067 /**
2068  * @brief Called when "Save" item is updated
2069  */
2070 void CMergeDoc::OnUpdateFileSave(CCmdUI* pCmdUI)
2071 {
2072         bool bModified = false;
2073         for (int nPane = 0; nPane < m_nBuffers; nPane++)
2074         {
2075                 if (m_ptBuf[nPane]->IsModified())
2076                         bModified = true;
2077         }
2078         pCmdUI->Enable(bModified);
2079 }
2080
2081 /**
2082  * @brief Called when "Save left (as...)" item is updated
2083  */
2084 void CMergeDoc::OnUpdateFileSaveLeft(CCmdUI* pCmdUI)
2085 {
2086         pCmdUI->Enable(m_ptBuf[0]->IsModified());
2087 }
2088
2089 /**
2090  * @brief Called when "Save middle (as...)" item is updated
2091  */
2092 void CMergeDoc::OnUpdateFileSaveMiddle(CCmdUI* pCmdUI)
2093 {
2094         pCmdUI->Enable(m_nBuffers == 3 && m_ptBuf[1]->IsModified());
2095 }
2096
2097 /**
2098  * @brief Called when "Save right (as...)" item is updated
2099  */
2100 void CMergeDoc::OnUpdateFileSaveRight(CCmdUI* pCmdUI)
2101 {
2102         pCmdUI->Enable(m_ptBuf[m_nBuffers - 1]->IsModified());
2103 }
2104
2105 /**
2106  * @brief Saves left-side file with name asked
2107  */
2108 void CMergeDoc::OnFileSaveAsLeft()
2109 {
2110         bool bSaveResult = false;
2111         DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
2112 }
2113
2114 /**
2115  * @brief Called when "Save middle (as...)" item is updated
2116  */
2117 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
2118 {
2119         pCmdUI->Enable(m_nBuffers == 3);
2120 }
2121
2122 /**
2123  * @brief Saves right-side file with name asked
2124  */
2125 void CMergeDoc::OnFileSaveAsMiddle()
2126 {
2127         bool bSaveResult = false;
2128         DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
2129 }
2130
2131 /**
2132  * @brief Saves right-side file with name asked
2133  */
2134 void CMergeDoc::OnFileSaveAsRight()
2135 {
2136         bool bSaveResult = false;
2137         DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
2138 }
2139
2140 /**
2141  * @brief Enable/disable left buffer read-only
2142  */
2143 void CMergeDoc::OnFileReadOnlyLeft()
2144 {
2145         bool bReadOnly = m_ptBuf[0]->GetReadOnly();
2146         m_ptBuf[0]->SetReadOnly(!bReadOnly);
2147 }
2148
2149 /**
2150  * @brief Called when "Left read-only" item is updated
2151  */
2152 void CMergeDoc::OnUpdateFileReadOnlyLeft(CCmdUI* pCmdUI)
2153 {
2154         bool bReadOnly = m_ptBuf[0]->GetReadOnly();
2155         pCmdUI->Enable(true);
2156         pCmdUI->SetCheck(bReadOnly);
2157 }
2158
2159 /**
2160  * @brief Enable/disable middle buffer read-only
2161  */
2162 void CMergeDoc::OnFileReadOnlyMiddle()
2163 {
2164         if (m_nBuffers == 3)
2165         {
2166                 bool bReadOnly = m_ptBuf[1]->GetReadOnly();
2167                 m_ptBuf[1]->SetReadOnly(!bReadOnly);
2168         }
2169 }
2170
2171 /**
2172  * @brief Called when "Middle read-only" item is updated
2173  */
2174 void CMergeDoc::OnUpdateFileReadOnlyMiddle(CCmdUI* pCmdUI)
2175 {
2176         if (m_nBuffers < 3)
2177         {
2178                 pCmdUI->Enable(false);
2179         }
2180         else
2181         {
2182                 bool bReadOnly = m_ptBuf[1]->GetReadOnly();
2183                 pCmdUI->Enable(true);
2184                 pCmdUI->SetCheck(bReadOnly);
2185         }
2186 }
2187
2188 /**
2189  * @brief Enable/disable right buffer read-only
2190  */
2191 void CMergeDoc::OnFileReadOnlyRight()
2192 {
2193         bool bReadOnly = m_ptBuf[m_nBuffers - 1]->GetReadOnly();
2194         m_ptBuf[m_nBuffers - 1]->SetReadOnly(!bReadOnly);
2195 }
2196
2197 /**
2198  * @brief Called when "Left read-only" item is updated
2199  */
2200 void CMergeDoc::OnUpdateFileReadOnlyRight(CCmdUI* pCmdUI)
2201 {
2202         bool bReadOnly = m_ptBuf[m_nBuffers - 1]->GetReadOnly();
2203         pCmdUI->Enable(true);
2204         pCmdUI->SetCheck(bReadOnly);
2205 }
2206
2207 /**
2208  * @brief Update readonly statusbaritem
2209  */
2210 void CMergeDoc::OnUpdateStatusRO(CCmdUI* pCmdUI)
2211 {
2212         bool bRO = m_ptBuf[pCmdUI->m_nID - ID_STATUS_PANE0FILE_RO]->GetReadOnly();
2213         pCmdUI->Enable(bRO);
2214 }
2215
2216 /**
2217  * @brief Update diff-number pane text in file compare.
2218  * The diff number pane shows selected difference/amount of differences when
2219  * there is difference selected. If there is no difference selected, then
2220  * the panel shows amount of differences. If there are ignored differences,
2221  * those are not count into numbers.
2222  * @param [in] pCmdUI UI component to update.
2223  */
2224 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI) 
2225 {
2226         TCHAR sIdx[32] = { 0 };
2227         TCHAR sCnt[32] = { 0 };
2228         String s;
2229         const int nDiffs = m_diffList.GetSignificantDiffs();
2230         
2231         // Files are identical - show text "Identical"
2232         if (nDiffs <= 0)
2233                 s = _("Identical");
2234         
2235         // There are differences, but no selected diff
2236         // - show amount of diffs
2237         else if (GetCurrentDiff() < 0)
2238         {
2239                 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
2240                 _itot_s(nDiffs, sCnt, 10);
2241                 strutils::replace(s, _T("%1"), sCnt);
2242         }
2243         
2244         // There are differences and diff selected
2245         // - show diff number and amount of diffs
2246         else
2247         {
2248                 s = _("Difference %1 of %2");
2249                 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
2250                 _itot_s(signInd + 1, sIdx, 10);
2251                 strutils::replace(s, _T("%1"), sIdx);
2252                 _itot_s(nDiffs, sCnt, 10);
2253                 strutils::replace(s, _T("%2"), sCnt);
2254         }
2255         pCmdUI->SetText(s.c_str());
2256 }
2257
2258 /**
2259  * @brief Change number of diff context lines
2260  */
2261 void CMergeDoc::OnDiffContext(UINT nID)
2262 {
2263         switch (nID)
2264         {
2265         case ID_VIEW_DIFFCONTEXT_0:
2266                 m_nDiffContext = 0; break;
2267         case ID_VIEW_DIFFCONTEXT_1:
2268                 m_nDiffContext = 1; break;
2269         case ID_VIEW_DIFFCONTEXT_3:
2270                 m_nDiffContext = 3; break;
2271         case ID_VIEW_DIFFCONTEXT_5:
2272                 m_nDiffContext = 5; break;
2273         case ID_VIEW_DIFFCONTEXT_7:
2274                 m_nDiffContext = 7; break;
2275         case ID_VIEW_DIFFCONTEXT_9:
2276                 m_nDiffContext = 9; break;
2277         case ID_VIEW_DIFFCONTEXT_TOGGLE:
2278                 m_nDiffContext = -m_nDiffContext - 1; break;
2279         case ID_VIEW_DIFFCONTEXT_ALL:
2280                 if (m_nDiffContext >= 0)
2281                         m_nDiffContext = -m_nDiffContext - 1;
2282                 break;
2283         case ID_VIEW_DIFFCONTEXT_INVERT:
2284                 m_bInvertDiffContext = !m_bInvertDiffContext;
2285                 break;
2286         }
2287         GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2288         GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2289         FlushAndRescan(true);
2290         ForEachView([](auto& pView) { if (pView->m_bDetailView) pView->EnsureVisible(pView->GetCursorPos()); });
2291 }
2292
2293 /**
2294  * @brief Swap the positions of the two panes
2295  */
2296 template<int srcPane, int dstPane>
2297 void CMergeDoc::OnViewSwapPanes()
2298 {
2299         SwapFiles(srcPane, dstPane);
2300 }
2301
2302 /**
2303  * @brief Swap context enable for 3 file compares 
2304  */
2305 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
2306 {
2307         pCmdUI->Enable(m_nBuffers > 2);
2308 }
2309
2310 /**
2311  * @brief Update number of diff context lines
2312  */
2313 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2314 {
2315         bool bCheck;
2316         switch (pCmdUI->m_nID)
2317         {
2318         case ID_VIEW_DIFFCONTEXT_0:
2319                 bCheck = (m_nDiffContext == 0); break;
2320         case ID_VIEW_DIFFCONTEXT_1:
2321                 bCheck = (m_nDiffContext == 1); break;
2322         case ID_VIEW_DIFFCONTEXT_3:
2323                 bCheck = (m_nDiffContext == 3); break;
2324         case ID_VIEW_DIFFCONTEXT_5:
2325                 bCheck = (m_nDiffContext == 5); break;
2326         case ID_VIEW_DIFFCONTEXT_7:
2327                 bCheck = (m_nDiffContext == 7); break;
2328         case ID_VIEW_DIFFCONTEXT_9:
2329                 bCheck = (m_nDiffContext == 9); break;
2330         case ID_VIEW_DIFFCONTEXT_TOGGLE:
2331                 bCheck = false; break;
2332         case ID_VIEW_DIFFCONTEXT_INVERT:
2333                 bCheck = m_bInvertDiffContext; break;
2334         default:
2335                 bCheck = (m_nDiffContext < 0); break;
2336         }
2337         pCmdUI->SetCheck(bCheck);
2338         pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2339 }
2340
2341 /**
2342  * @brief Refresh display using text-buffers
2343  * @note This DOES NOT reload files!
2344  */
2345 void CMergeDoc::OnRefresh()
2346 {
2347         FlushAndRescan(true);
2348 }
2349
2350 /**
2351  * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2352  *
2353  * @note After PrimeTextBuffers(), all buffers should have the same length.
2354  */
2355 void CMergeDoc::PrimeTextBuffers()
2356 {
2357         SetCurrentDiff(-1);
2358         m_nTrivialDiffs = 0;
2359         int nDiff;
2360         int nDiffCount = m_diffList.GetSize();
2361         int file;
2362
2363         // walk the diff list and calculate numbers of extra lines to add
2364         int extras[3] = {0, 0, 0};   // extra lines added to each view
2365         m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2366
2367         // resize m_aLines once for each view
2368         UINT lcount[3] = {0, 0, 0};
2369         UINT lcountnew[3] = {0, 0, 0};
2370         UINT lcountmax = 0;
2371         
2372         for (file = 0; file < m_nBuffers; file++)
2373         {
2374                 lcount[file] = m_ptBuf[file]->GetLineCount();
2375                 lcountnew[file] = lcount[file] + extras[file];
2376                 lcountmax = max(lcountmax, lcountnew[file]);
2377         }
2378         for (file = 0; file < m_nBuffers; file++)
2379         {
2380                 m_ptBuf[file]->m_aLines.resize(lcountmax);
2381         }
2382
2383         // walk the diff list backward, move existing lines to proper place,
2384         // add ghost lines, and set flags
2385         for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2386         {
2387                 DIFFRANGE curDiff;
2388                 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2389
2390                 // move matched lines after curDiff
2391                 int nline[3] = { 0, 0, 0 };
2392                 for (file = 0; file < m_nBuffers; file++)
2393                         nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2394                 // Matched lines should really match...
2395                 // But matched lines after last diff may differ because of empty last line (see function's note)
2396                 if (nDiff < nDiffCount - 1)
2397                         ASSERT(nline[0] == nline[1]);
2398
2399                 int nmaxline = 0;
2400                 for (file = 0; file < m_nBuffers; file++)
2401                 {
2402                         // Move all lines after current diff down as far as needed
2403                         // for any ghost lines we're about to insert
2404                         m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2405                         lcountnew[file] -= nline[file];
2406                         lcount[file] -= nline[file];
2407                         // move unmatched lines and add ghost lines
2408                         nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2409                         nmaxline = max(nmaxline, nline[file]);
2410                 }
2411
2412                 for (file = 0; file < m_nBuffers; file++)
2413                 {
2414                         DWORD dflag = LF_GHOST;
2415                         if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2416                                 dflag |= LF_SNP;
2417                         m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2418                         int nextra = nmaxline - nline[file];
2419                         if (nextra > 0)
2420                         {
2421                                 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2422                                 for (int i = 1; i <= nextra; i++)
2423                                         m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2424                         }
2425                         lcountnew[file] -= nmaxline;
2426
2427                         lcount[file] -= nline[file];
2428
2429                 }
2430                 // set dbegin, dend, blank, and line flags
2431                 curDiff.dbegin = lcountnew[0];
2432
2433                 switch (curDiff.op)
2434                 {
2435                 case OP_TRIVIAL:
2436                         ++m_nTrivialDiffs;
2437                         [[fallthrough]];
2438                 case OP_DIFF:
2439                 case OP_1STONLY:
2440                 case OP_2NDONLY:
2441                 case OP_3RDONLY:
2442                         // set curdiff
2443                         {
2444                                 curDiff.dend = lcountnew[0]+nmaxline-1;
2445                                 for (file = 0; file < m_nBuffers; file++)
2446                                 {
2447                                         curDiff.blank[file] = -1;
2448                                         int nextra = nmaxline - nline[file];
2449                                         if (nmaxline > nline[file])
2450                                         {
2451                                                 // more lines on left, ghost lines on right side
2452                                                 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2453                                         }
2454                                 }
2455                         }
2456                         // flag lines
2457                         {
2458                                 for (file = 0; file < m_nBuffers; file++)
2459                                 {
2460                                         // left side
2461                                         int i;
2462                                         for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2463                                         {
2464                                                 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2465                                                 {
2466                                                         // set diff or trivial flag
2467                                                         DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2468                                                         if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2469                                                                 dflag |= LF_SNP;
2470                                                         m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2471                                                         m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2472                                                 }
2473                                                 else
2474                                                 {
2475                                                         // ghost lines are already inserted (and flagged)
2476                                                         // ghost lines opposite to trivial lines are ghost and trivial
2477                                                         if (curDiff.op == OP_TRIVIAL)
2478                                                                 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2479                                                 }
2480                                         }
2481                                 }
2482                         }
2483                         break;
2484                 }           // switch (curDiff.op)
2485                 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2486         }             // for (nDiff = nDiffCount; nDiff-- > 0; )
2487
2488         m_diffList.ConstructSignificantChain();
2489
2490 #ifdef _DEBUG
2491         // Note: By this point all `m_ptBuf[]` buffers must have the same  
2492         //              number of line entries; eventual buffer processing typically
2493         //              uses the line count from `m_ptBuf[0]` for all buffer processing.
2494
2495         for (file = 0; file < m_nBuffers; file++)
2496         {
2497                 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2498         }
2499 #endif
2500
2501         for (file = 0; file < m_nBuffers; file++)
2502                 m_ptBuf[file]->FinishLoading();
2503 }
2504
2505 /**
2506  * @brief Checks if file has changed since last update (save or rescan).
2507  * @param [in] szPath File to check
2508  * @param [in] dfi Previous fileinfo of file
2509  * @param [in] bSave If true Compare to last save-info, else to rescan-info
2510  * @param [in] nBuffer Index (0-based) of buffer
2511  * @return true if file is changed.
2512  */
2513 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2514         bool bSave, int nBuffer)
2515 {
2516         DiffFileInfo *fileInfo = nullptr;
2517         bool bFileChanged = false;
2518         bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2519         int tolerance = 0;
2520         if (bIgnoreSmallDiff)
2521                 tolerance = SmallTimeDiff; // From MainFrm.h
2522
2523         if (bSave)
2524                 fileInfo = m_pSaveFileInfo[nBuffer].get();
2525         else
2526                 fileInfo = m_pRescanFileInfo[nBuffer].get();
2527
2528         // We assume file existed, so disappearing means removal
2529         if (!dfi.Update(szPath))
2530                 return FileChange::Removed;
2531
2532         int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2533         if (timeDiff < 0) timeDiff = -timeDiff;
2534         if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2535         {
2536                 bFileChanged = true;
2537         }
2538
2539         if (bFileChanged)
2540                 return FileChange::Changed;
2541         else
2542                 return FileChange::NoChange;
2543 }
2544
2545 void CMergeDoc::HideLines()
2546 {
2547         int nLine;
2548         int file;
2549
2550         if (m_nDiffContext < 0)
2551         {
2552                 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2553                 return;
2554         }
2555
2556         int nLineCount = 0x7fffffff;
2557         for (file = 0; file < m_nBuffers; file++)
2558         {
2559                 if (nLineCount > m_ptBuf[file]->GetLineCount())
2560                         nLineCount = m_ptBuf[file]->GetLineCount();
2561         }
2562
2563         for (nLine =  0; nLine < nLineCount;)
2564         {
2565                 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2566                 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2567                 {
2568                         for (file = 0; file < m_nBuffers; file++)
2569                                 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2570                         nLine++;
2571                 }
2572                 else
2573                 {
2574                         int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2575                         for (; nLine2 < nLine; nLine2++)
2576                         {
2577                                 for (file = 0; file < m_nBuffers; file++)
2578                                         m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2579                         }
2580                 
2581                         for (; nLine < nLineCount; nLine++)
2582                         {
2583                                 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2584                                 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2585                                         break;
2586                                 for (file = 0; file < m_nBuffers; file++)
2587                                         m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2588                         }
2589
2590                         int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2591                         for (; nLine < nLineEnd2; nLine++)
2592                         {
2593                                 for (file = 0; file < m_nBuffers; file++)
2594                                         m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2595                                 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2596                                 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2597                                         nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2598                         }
2599                 }
2600         }
2601
2602         ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2603 }
2604
2605 /**
2606  * @brief Asks and then saves modified files.
2607  *
2608  * This function saves modified files. Dialog is shown for user to select
2609  * modified file(s) one wants to save or discard changed. Cancelling of
2610  * save operation is allowed unless denied by parameter. After successfully
2611  * save operation file statuses are updated to directory compare.
2612  * @param [in] bAllowCancel If false "Cancel" button is disabled.
2613  * @return true if user selected "OK" so next operation can be
2614  * executed. If false user choosed "Cancel".
2615  * @note If filename is empty, we assume scratchpads are saved,
2616  * so instead of filename, description is shown.
2617  * @todo If we have filename and description for file, what should
2618  * we do after saving to different filename? Empty description?
2619  * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2620  */
2621 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2622 {
2623         bool bLModified = false, bMModified = false, bRModified = false;
2624         bool result = true;
2625         bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2626
2627         if (m_nBuffers == 3)
2628         {
2629                 bLModified = m_ptBuf[0]->IsModified();
2630                 bMModified = m_ptBuf[1]->IsModified();
2631                 bRModified = m_ptBuf[2]->IsModified();
2632         }
2633         else
2634         {
2635                 bLModified = m_ptBuf[0]->IsModified();
2636                 bRModified = m_ptBuf[1]->IsModified();
2637         }
2638         if (!bLModified && !bMModified && !bRModified)
2639                  return true;
2640
2641         SaveClosingDlg dlg;
2642         dlg.DoAskFor(bLModified, bMModified, bRModified);
2643         if (!bAllowCancel)
2644                 dlg.m_bDisableCancel = true;
2645         if (!m_filePaths.GetLeft().empty())
2646         {
2647                 if (theApp.m_strSaveAsPath.empty())
2648                         dlg.m_sLeftFile = m_filePaths.GetLeft();
2649                 else
2650                         dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2651         }
2652         else
2653                 dlg.m_sLeftFile = m_strDesc[0];
2654         if (m_nBuffers == 3)
2655         {
2656                 if (!m_filePaths.GetMiddle().empty())
2657                 {
2658                         if (theApp.m_strSaveAsPath.empty())
2659                                 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2660                         else
2661                                 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2662                 }
2663                 else
2664                         dlg.m_sMiddleFile = m_strDesc[1];
2665         }
2666         if (!m_filePaths.GetRight().empty())
2667         {
2668                 if (theApp.m_strSaveAsPath.empty())
2669                         dlg.m_sRightFile = m_filePaths.GetRight();
2670                 else
2671                         dlg.m_sRightFile = theApp.m_strSaveAsPath;
2672         }
2673         else
2674                 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2675
2676         if (dlg.DoModal() == IDOK)
2677         {
2678                 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2679                 {
2680                         if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2681                                 result = false;
2682                 }
2683
2684                 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2685                 {
2686                         if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2687                                 result = false;
2688                 }
2689
2690                 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2691                 {
2692                         if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2693                                 result = false;
2694                 }
2695         }
2696         else
2697         {       
2698                 result = false;
2699         }
2700
2701         // If file were modified and saving was successfull,
2702         // update status on dir view
2703         if ((bLModified && bLSaveSuccess) || 
2704              (bMModified && bMSaveSuccess) ||
2705                  (bRModified && bRSaveSuccess))
2706         {
2707                 // If directory compare has results
2708                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2709                 {
2710                         if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2711                                 FlushAndRescan(false);
2712
2713                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2714                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2715                                         m_nTrivialDiffs, bIdentical);
2716                 }
2717         }
2718
2719         return result;
2720 }
2721
2722 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2723 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2724 {
2725         // if we did not rescan during the request timeOut, Rescan
2726         // else we did Rescan after the request, so do nothing
2727         COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2728         if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2729                 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2730                 FlushAndRescan();
2731 }
2732
2733 /**
2734  * @brief We have two child views (left & right), so we keep pointers directly
2735  * at them (the MFC view list doesn't have them both)
2736  */
2737 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2738 {
2739
2740         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2741                 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2742         ++m_nGroups;
2743 }
2744
2745 void CMergeDoc::RemoveMergeViews(int nGroup)
2746 {
2747
2748         for (; nGroup < m_nGroups - 1; nGroup++)
2749         {
2750                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2751                 {
2752                         m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2753                         m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2754                 }
2755         }
2756         --m_nGroups;
2757 }
2758
2759 /**
2760  * @brief DirDoc gives us its identity just after it creates us
2761  */
2762 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2763 {
2764         ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2765         m_pDirDoc = pDirDoc;
2766 }
2767
2768 /**
2769  * @brief Return pointer to parent frame
2770  */
2771 CMergeEditFrame * CMergeDoc::GetParentFrame() 
2772 {
2773         return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame()); 
2774 }
2775
2776 /**
2777  * @brief DirDoc is closing
2778  */
2779 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2780 {
2781         ASSERT(m_pDirDoc == pDirDoc);
2782         m_pDirDoc = nullptr;
2783         // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2784 }
2785
2786 /**
2787  * @brief DirDoc commanding us to close
2788  */
2789 bool CMergeDoc::CloseNow()
2790 {
2791         // Allow user to cancel closing
2792         if (!PromptAndSaveIfNeeded(true))
2793                 return false;
2794
2795         GetParentFrame()->DestroyWindow();
2796         return true;
2797 }
2798
2799 /**
2800  * @brief Loads file to buffer and shows load-errors
2801  * @param [in] sFileName File to open
2802  * @param [in] nBuffer Index (0-based) of buffer to load
2803  * @param [out] readOnly whether file is read-only
2804  * @param [in] encoding encoding used
2805  * @return Tells if files were loaded successfully
2806  * @sa CMergeDoc::OpenDocs()
2807  **/
2808 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2809 {
2810         String sError;
2811         DWORD retVal = FileLoadResult::FRESULT_ERROR;
2812
2813         CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2814         m_filePaths[nBuffer] = sFileName;
2815
2816         CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2817         CString sOpenError;
2818         retVal = pBuf->LoadFromFile(sFileName, m_infoUnpacker,
2819                 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2820
2821         // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2822         // it left the pBuf in a valid (but empty) state via a call to InitNew
2823
2824         if (FileLoadResult::IsOkImpure(retVal))
2825         {
2826                 // File loaded, and multiple EOL types in this file
2827                 FileLoadResult::SetMainOk(retVal);
2828
2829                 // If mixed EOLs are not enabled, enable them for this doc.
2830                 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2831                 {
2832                         pBuf->SetMixedEOL(true);
2833                 }
2834         }
2835
2836         if (FileLoadResult::IsError(retVal))
2837         {
2838                 // Error from Unifile/system
2839                 if (!sOpenError.IsEmpty())
2840                         sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2841                 else
2842                         sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2843                 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2844         }
2845         else if (FileLoadResult::IsErrorUnpack(retVal))
2846         {
2847                 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2848                 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2849         }
2850         return retVal;
2851 }
2852
2853 /**
2854  * @brief Check if specified codepage number is valid for WinMerge Editor.
2855  * @param [in] cp Codepage number to check.
2856  * @return true if codepage is valid, false otherwise.
2857  */
2858 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2859 {
2860         if (cp == 0) // 0 is our signal value for invalid
2861                 return false;
2862         return GetEncodingNameFromCodePage(cp) != nullptr;
2863 }
2864
2865 /**
2866  * @brief Sanity check file's specified codepage.
2867  * This function checks if file's specified codepage is valid for WinMerge
2868  * editor and if not resets the codepage to default.
2869  * @param [in,out] fileinfo Class containing file's codepage.
2870  */
2871 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2872 {
2873         if (fileinfo.encoding.m_unicoding == ucr::NONE
2874                 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2875         {
2876                 int cp = ucr::getDefaultCodepage();
2877                 if (!IsValidCodepageForMergeEditor(cp))
2878                         cp = CP_ACP;
2879                 fileinfo.encoding.SetCodepage(cp);
2880         }
2881 }
2882
2883 /**
2884  * @brief Loads one file from disk and updates file infos.
2885  * @param [in] index Index of file in internal buffers.
2886  * @param [in] filename File's name.
2887  * @param [in] readOnly Is file read-only?
2888  * @param [in] encoding File's encoding.
2889  * @return One of FileLoadResult values.
2890  */
2891 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc, 
2892                 const FileTextEncoding & encoding)
2893 {
2894         DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2895         
2896         m_strDesc[index] = strDesc;
2897         if (!filename.empty())
2898         {
2899                 if (strDesc.empty())
2900                         m_nBufferType[index] = BUFFERTYPE::NORMAL;
2901                 else
2902                         m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2903                 m_pSaveFileInfo[index]->Update(filename);
2904                 m_pRescanFileInfo[index]->Update(filename);
2905
2906                 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2907                 if (FileLoadResult::IsLossy(loadSuccess))
2908                 {
2909                         // Determine the file encoding by looking at all the contents of the file, not just part of it
2910                         FileTextEncoding encodingNew = codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1);
2911                         if (encoding != encodingNew)
2912                         {
2913                                 m_ptBuf[index]->FreeAll();
2914                                 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encodingNew);
2915                         }
2916                 }
2917         }
2918         else
2919         {
2920                 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2921                 m_ptBuf[index]->InitNew();
2922                 m_ptBuf[index]->m_encoding = encoding;
2923                 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer 
2924                 loadSuccess = FileLoadResult::FRESULT_OK;
2925         }
2926         return loadSuccess;
2927 }
2928
2929 CMergeDoc::TableProps CMergeDoc::MakeTablePropertiesByFileName(const String& path, const std::optional<bool>& enableTableEditing, bool showDialog)
2930 {
2931         const TCHAR quote = strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR));
2932         FileFilterHelper filterCSV, filterTSV, filterDSV;
2933         bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2934         const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2935         if (!csvFilePattern.empty())
2936         {
2937                 filterCSV.UseMask(true);
2938                 filterCSV.SetMask(csvFilePattern);
2939                 if (filterCSV.includeFile(path))
2940                         return { true, ',', quote, allowNewlineIQuotes };
2941         }
2942         const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2943         if (!tsvFilePattern.empty())
2944         {
2945                 filterTSV.UseMask(true);
2946                 filterTSV.SetMask(tsvFilePattern);
2947                 if (filterTSV.includeFile(path))
2948                         return { true, '\t', quote, allowNewlineIQuotes };
2949         }
2950         const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2951         if (!dsvFilePattern.empty())
2952         {
2953                 filterDSV.UseMask(true);
2954                 filterDSV.SetMask(dsvFilePattern);
2955                 if (filterDSV.includeFile(path))
2956                         return { true, strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR)), quote };
2957         }
2958         if (enableTableEditing.value_or(false))
2959         {
2960                 if (showDialog)
2961                 {
2962                         COpenTableDlg dlg;
2963                         if (dlg.DoModal() == IDOK)
2964                                 return { true, strutils::from_charstr(dlg.m_sDelimiterChar), strutils::from_charstr(dlg.m_sQuoteChar), dlg.m_bAllowNewlinesInQuotes };
2965                 }
2966                 else
2967                 {
2968                         return { true, strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR)), quote };
2969                 }
2970         }
2971         return { false, 0, 0, false };
2972 };
2973
2974 void CMergeDoc::SetTableProperties()
2975 {
2976         TableProps tableProps[3] = { };
2977         int nTableFileIndex = -1;
2978         if (m_pTablePropsPrepared)
2979         {
2980                 nTableFileIndex = 0;
2981                 tableProps[0] = *m_pTablePropsPrepared;
2982         }
2983         else
2984         {
2985                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2986                 {
2987                         if (nBuffer == 0 ||
2988                                 paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2989                                 tableProps[nBuffer] = MakeTablePropertiesByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2990                         else
2991                                 tableProps[nBuffer] = tableProps[nBuffer - 1];
2992                         if (tableProps[nBuffer].istable)
2993                                 nTableFileIndex = nBuffer;
2994                 }
2995         }
2996         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2997         {
2998                 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
2999                 {
3000                         int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
3001                         m_ptBuf[nBuffer]->SetTableEditing(true);
3002                         m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
3003                         m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
3004                         m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
3005                         m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
3006                         m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
3007                 }
3008                 else
3009                 {
3010                         m_ptBuf[nBuffer]->SetTableEditing(false);
3011                 }
3012         }
3013 }
3014
3015 void CMergeDoc::SetTextType(int textType)
3016 {
3017         ForEachView([textType, this](auto& pView) {
3018                 pView->SetTextType(CrystalLineParser::TextType(textType));
3019                 pView->SetDisableBSAtSOL(false);
3020                 m_bChangedSchemeManually = true;
3021         });
3022 }
3023
3024 void CMergeDoc::SetTextType(const String& ext)
3025 {
3026         String ext2 = ext;
3027         strutils::replace(ext2, _T("."), _T(""));
3028         ForEachView([&ext2, this](auto& pView) {
3029                 pView->SetTextType(ext2.c_str());
3030                 pView->SetDisableBSAtSOL(false);
3031                 m_bChangedSchemeManually = true;
3032         });
3033 }
3034
3035 /**
3036  * @brief Loads files and does initial rescan.
3037  * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
3038  * @param bRO [in] Is left/middle/right file read-only
3039  * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
3040  * @todo Options are still read from CMainFrame, this will change
3041  * @sa CMainFrame::ShowTextMergeDoc()
3042  */
3043 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
3044                 const bool bRO[], const String strDesc[])
3045 {
3046         CWaitCursor waitstatus;
3047         IDENTLEVEL identical = IDENTLEVEL::NONE;
3048         int nRescanResult = RESCAN_OK;
3049         int nBuffer;
3050         FileLocation fileloc[3];
3051
3052         std::copy_n(ifileloc, 3, fileloc);
3053
3054         // Filter out invalid codepages, or editor will display all blank
3055         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3056                 SanityCheckCodepage(fileloc[nBuffer]);
3057
3058         // clear undo stack
3059         undoTgt.clear();
3060         curUndo = undoTgt.begin();
3061
3062         // Prevent displaying views during LoadFile
3063         // Note : attach buffer again only if both loads succeed
3064         m_strBothFilenames.erase();
3065
3066         ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
3067
3068         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3069         {
3070                 // clear undo buffers
3071                 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
3072
3073                 // free the buffers
3074                 m_ptBuf[nBuffer]->FreeAll ();
3075
3076                 // build the text being filtered, "|" separates files as it is forbidden in filenames
3077                 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
3078         }
3079         m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
3080
3081         // Load files
3082         DWORD nSuccess[3] = { FileLoadResult::FRESULT_ERROR,  FileLoadResult::FRESULT_ERROR,  FileLoadResult::FRESULT_ERROR };
3083         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3084         {
3085                 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
3086                         fileloc[nBuffer].encoding);
3087                 if (!FileLoadResult::IsOk(nSuccess[nBuffer]))
3088                 {
3089                         CMergeEditFrame* pFrame = GetParentFrame();
3090                         if (pFrame != nullptr)
3091                         {
3092                                 // Use verify macro to trap possible error in debug.
3093                                 VERIFY(pFrame->DestroyWindow());
3094                         }
3095                         return false;
3096                 }
3097         }
3098
3099         SetTableProperties();
3100
3101         const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
3102
3103         // scratchpad : we don't call LoadFile, so
3104         // we need to initialize the unpacker as a "do nothing" one
3105         if (bFiltersEnabled)
3106         { 
3107                 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
3108                 {
3109                         m_infoUnpacker.Initialize(false);
3110                 }
3111         }
3112
3113         // Warn user if file load was lossy (bad encoding)
3114         int idres=0;
3115         int nLossyBuffers = 0;
3116         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3117         {
3118                 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
3119                 {
3120                         // TODO: It would be nice to report how many lines were lossy
3121                         // we did calculate those numbers when we loaded the files, in the text stats
3122         
3123                         idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
3124                         nLossyBuffers++;
3125                 }
3126         }
3127         if (nLossyBuffers > 1)
3128                 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
3129         
3130         if (nLossyBuffers > 0)
3131         {
3132                 if (m_pEncodingErrorBar == nullptr)
3133                 {
3134                         m_pEncodingErrorBar.reset(new CEncodingErrorBar());
3135                         m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
3136                 }
3137                 m_pEncodingErrorBar->SetText(LoadResString(idres));
3138                 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
3139         }
3140
3141         ForEachView([](auto& pView) {
3142                 // Now buffers data are valid
3143                 pView->AttachToBuffer();
3144                 // Currently there is only one set of syntax colors, which all documents & views share
3145                 pView->SetColorContext(theApp.GetMainSyntaxColors());
3146                 // Currently there is only one set of markers, which all documents & views share
3147                 pView->SetMarkersContext(theApp.GetMainMarkers());
3148         });
3149         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3150         {
3151                 // Set read-only statuses
3152                 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
3153         }
3154
3155         // Check the EOL sensitivity option (do it before Rescan)
3156         DIFFOPTIONS diffOptions = {0};
3157         m_diffWrapper.GetOptions(&diffOptions);
3158         if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
3159         {
3160                 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3161                         if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
3162                                 break;
3163
3164                 if (nBuffer < m_nBuffers)
3165                 {
3166                         // Options and files not are not compatible :
3167                         // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
3168                         // All lines will differ, that is not very interesting and probably not wanted.
3169                         // Propose to turn off the option 'sensitive to EOL'
3170                         String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
3171                         if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
3172                         {
3173                                 diffOptions.bIgnoreEol = true;
3174                                 m_diffWrapper.SetOptions(&diffOptions);
3175
3176                                 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
3177                                 const int nFormerResult = dlg.GetFormerResult();
3178                                 if (nFormerResult != -1)
3179                                 {
3180                                         // "Don't ask this question again" checkbox is checked
3181                                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
3182                                 }
3183                         }
3184                 }
3185         }
3186
3187         // Define the prediffer
3188         PackingInfo * infoUnpacker = nullptr;
3189         PrediffingInfo * infoPrediffer = nullptr;
3190         if (bFiltersEnabled && m_pDirDoc != nullptr)
3191         {
3192                 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
3193                 m_diffWrapper.SetPrediffer(infoPrediffer);
3194                 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
3195         }
3196
3197         bool bBinary = false;
3198         nRescanResult = Rescan(bBinary, identical);
3199
3200         // Open filed if rescan succeed and files are not binaries
3201         if (nRescanResult == RESCAN_OK)
3202         {
3203                 // set the document types
3204                 // Warning : it is the first thing to do (must be done before UpdateView,
3205                 // or any function that calls UpdateView, like SelectDiff)
3206                 // Note: If option enabled, and another side type is not recognized,
3207                 // we use recognized type for unrecognized side too.
3208                 String sext[3];
3209                 bool bTyped[3];
3210                 int paneTyped = 0;
3211
3212                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3213                 {
3214                         sext[nBuffer] = GetFileExt(m_ptBuf[nBuffer]->GetTempFileName().c_str(), m_strDesc[nBuffer].c_str());
3215                         ForEachView(nBuffer, [&](auto& pView) {
3216                                 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
3217                                 if (bTyped[nBuffer])
3218                                         paneTyped = nBuffer;
3219                         });
3220                 }
3221
3222                 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3223                 {
3224                         if (bTyped[0] != bTyped[nBuffer])
3225                                 break;
3226                 }
3227
3228                 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
3229                 if (syntaxHLEnabled && nBuffer < m_nBuffers)
3230                 {
3231                         if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
3232                         {
3233                                 CString sFirstLine;
3234                                 m_ptBuf[0]->GetLine(0, sFirstLine);
3235                                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3236                                 {
3237                                         bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
3238                                 }
3239                         }
3240                 }
3241
3242                 if (syntaxHLEnabled)
3243                 {
3244                         CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
3245                         ForEachView([&bTyped, enuType](auto& pView) {
3246                                 if (!bTyped[pView->m_nThisPane])
3247                                         pView->SetTextType(enuType);
3248                         });
3249                 }
3250
3251                 int nNormalBuffer = 0;
3252                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3253                 {
3254                         // set the frame window header
3255                         UpdateHeaderPath(nBuffer);
3256
3257                         ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
3258                         
3259                         if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
3260                             (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
3261                         {
3262                                 nNormalBuffer++;
3263                         }
3264                         
3265                 }
3266
3267                 // Inform user that files are identical
3268                 // Don't show message if new buffers created
3269                 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
3270                 {
3271                         ShowRescanError(nRescanResult, identical);
3272                 }
3273
3274                 // Exit if files are identical should only work for the first
3275                 // comparison and must be disabled afterward.
3276                 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
3277         }
3278         else
3279         {
3280                 // CMergeDoc::Rescan fails if files do not exist on both sides 
3281                 // or the really arcane case that the temp files couldn't be created, 
3282                 // which is too obscure to bother reporting if you can't write to 
3283                 // your temp directory, doing nothing is graceful enough for that).
3284                 ShowRescanError(nRescanResult, identical);
3285                 GetParentFrame()->DestroyWindow();
3286                 return false;
3287         }
3288
3289         // Force repaint of location pane to update it in case we had some warning
3290         // dialog visible and it got painted before files were loaded
3291         if (m_pView[0][0] != nullptr)
3292                 m_pView[0][0]->RepaintLocationPane();
3293
3294         return true;
3295 }
3296
3297 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex, bool bRealLine, int nCharIndex)
3298 {
3299         if (nPane < 0)
3300         {
3301                 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
3302                 if (nPane < 0 || nPane >= m_nBuffers)
3303                         nPane = 0;
3304         }
3305         if (nLineIndex == -1)
3306         {
3307                 // scroll to first diff
3308                 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
3309                         m_diffList.HasSignificantDiffs())
3310                 {
3311                         int nDiff = m_diffList.FirstSignificantDiff();
3312                         if (nDiff != -1)
3313                                 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
3314                         m_pView[0][nPane]->SetActivePane();
3315                         return;
3316                 }
3317         }
3318         m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, bRealLine, nPane, true, nCharIndex);
3319 }
3320
3321 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
3322 {
3323         if (!PromptAndSaveIfNeeded(true))
3324                 return;
3325
3326         FileLocation fileloc[3];
3327         String strDesc[3];
3328         bool bRO[3];
3329         for (int pane = 0; pane < m_nBuffers; pane++)
3330         {
3331                 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3332                 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3333                 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3334                 fileloc[pane].setPath(m_filePaths[pane]);
3335         }
3336         std::copy_n(m_strDesc, m_nBuffers, strDesc);
3337
3338         strDesc[nBuffer] = _T("");
3339         fileloc[nBuffer].setPath(path);
3340         fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3341
3342         bool filenameChanged = path != m_filePaths[nBuffer];
3343         auto columnWidths = m_ptBuf[nBuffer]->GetColumnWidths();
3344         
3345         if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3346         {
3347                 if (!filenameChanged)
3348                         m_ptBuf[nBuffer]->SetColumnWidths(columnWidths);
3349                 MoveOnLoad(nBuffer, nLineIndex);
3350         }
3351 }
3352
3353 /**
3354  * @brief Re-load a document.
3355  * This methods re-loads the file compare document. The re-loaded document is
3356  * one side of the file compare.
3357  * @param [in] index The document to re-load.
3358  * @return Open result code.
3359  */
3360 void CMergeDoc::RefreshOptions()
3361 {
3362         DIFFOPTIONS options = {0};
3363         
3364         m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
3365
3366         m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3367         Options::DiffOptions::Load(GetOptionsMgr(), options);
3368
3369         m_diffWrapper.SetOptions(&options);
3370
3371         // Refresh view options
3372         ForEachView([](auto& pView) { pView->RefreshOptions(); });
3373 }
3374
3375 /**
3376  * @brief Write path and filename to headerbar
3377  * @note SetText() does not repaint unchanged text
3378  */
3379 void CMergeDoc::UpdateHeaderPath(int pane)
3380 {
3381         CMergeEditFrame *pf = GetParentFrame();
3382         ASSERT(pf != nullptr);
3383         String sText;
3384         bool bChanges = false;
3385
3386         if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3387                 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3388         {
3389                 sText = m_strDesc[pane];
3390         }
3391         else
3392         {
3393                 sText = m_filePaths[pane];
3394                 if (m_pDirDoc != nullptr)
3395                 {
3396                         m_pDirDoc->ApplyDisplayRoot(pane, sText);
3397                 }
3398         }
3399         bChanges = m_ptBuf[pane]->IsModified();
3400
3401         if (bChanges)
3402                 sText.insert(0, _T("* "));
3403
3404         if (m_sCurrentHeaderTitle[pane] == sText)
3405                 return;
3406
3407         m_sCurrentHeaderTitle[pane] = sText;
3408
3409         pf->GetHeaderInterface()->SetText(pane, sText);
3410
3411         SetTitle(nullptr);
3412 }
3413
3414 /**
3415  * @brief Paint differently the headerbar of the active view
3416  */
3417 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3418 {
3419         CMergeEditFrame *pf = GetParentFrame();
3420         ASSERT(pf != nullptr);
3421         pf->GetHeaderInterface()->SetActive(pane, bActivate);
3422 }
3423
3424 /**
3425  * @brief Set detect/not detect Moved Blocks
3426  */
3427 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3428 {
3429         if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3430                 return;
3431
3432         GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3433         m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3434         FlushAndRescan();
3435 }
3436
3437 /**
3438  * @brief Check if given buffer has mixed EOL style.
3439  * @param [in] nBuffer Buffer to check.
3440  * @return true if buffer's EOL style is mixed, false otherwise.
3441  */
3442 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3443 {
3444         CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3445         return pBuf->IsMixedEOL();
3446 }
3447
3448 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3449 {
3450         m_bEditAfterRescan[nBuffer] = true;
3451 }
3452
3453 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3454 {
3455         if (nBuffer >= 0 && nBuffer < m_nBuffers)
3456                 return m_bEditAfterRescan[nBuffer];
3457
3458         for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3459         {
3460                 if (m_bEditAfterRescan[nBuffer])
3461                         return true;
3462         }
3463
3464         return false;
3465 }
3466
3467 /**
3468  * @brief Update document filenames to title
3469  */
3470 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3471 {
3472         PrediffingInfo infoPrediffer;
3473         GetPrediffer(&infoPrediffer);
3474         String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer);
3475         CDocument::SetTitle(sTitle.c_str());
3476 }
3477
3478 /**
3479  * @brief Update any resources necessary after a GUI language change
3480  */
3481 void CMergeDoc::UpdateResources()
3482 {
3483         if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3484                 m_strDesc[0] = _("Untitled left");
3485         if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3486                 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3487         if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3488                 m_strDesc[1] = _("Untitled middle");
3489         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3490                 UpdateHeaderPath(nBuffer);
3491
3492         GetParentFrame()->UpdateResources();
3493         ForEachView([](auto& pView) { pView->UpdateResources(); });
3494 }
3495
3496 // Return current word breaking break type setting (whitespace only or include punctuation)
3497 bool CMergeDoc::GetBreakType() const
3498 {
3499         bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3500         return breakType;
3501 }
3502
3503 // Return true to do line diff colors at the byte level (false to do them at word level)
3504 bool CMergeDoc::GetByteColoringOption() const
3505 {
3506         // color at byte level if 'break_on_words' option not set
3507         bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3508         return !breakWords;
3509 }
3510
3511 /// Swap files and update views
3512 void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
3513 {
3514         if ((nFromIndex >= 0 && nFromIndex < m_nBuffers) && (nToIndex >= 0 && nToIndex < m_nBuffers))
3515         {
3516                 // Swap views
3517                 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3518                 {
3519                         int nLeftViewId = m_pView[nGroup][nFromIndex]->GetDlgCtrlID();
3520                         int nRightViewId = m_pView[nGroup][nToIndex]->GetDlgCtrlID();
3521                         m_pView[nGroup][nFromIndex]->SetDlgCtrlID(nRightViewId);
3522                         m_pView[nGroup][nToIndex]->SetDlgCtrlID(nLeftViewId);
3523                 }
3524
3525
3526                 // Swap buffers and so on
3527                 std::swap(m_ptBuf[nFromIndex], m_ptBuf[nToIndex]);
3528                 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3529                         std::swap(m_pView[nGroup][nFromIndex], m_pView[nGroup][nToIndex]);
3530                 std::swap(m_pSaveFileInfo[nFromIndex], m_pSaveFileInfo[nToIndex]);
3531                 std::swap(m_pRescanFileInfo[nFromIndex], m_pRescanFileInfo[nToIndex]);
3532                 std::swap(m_nBufferType[nFromIndex], m_nBufferType[nToIndex]);
3533                 std::swap(m_bEditAfterRescan[nFromIndex], m_bEditAfterRescan[nToIndex]);
3534                 std::swap(m_strDesc[nFromIndex], m_strDesc[nToIndex]);
3535
3536                 m_filePaths.Swap(nFromIndex, nToIndex);
3537                 m_diffList.Swap(nFromIndex, nToIndex);
3538                 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3539                         swap(m_pView[nGroup][nFromIndex]->m_piMergeEditStatus, m_pView[nGroup][nToIndex]->m_piMergeEditStatus);
3540
3541                 ClearWordDiffCache();
3542
3543                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3544                 {
3545                         m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3546                         for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3547                                 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3548
3549                         // Update views
3550                         UpdateHeaderPath(nBuffer);
3551                 }
3552                 GetParentFrame()->UpdateSplitter();
3553                 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3554
3555                 UpdateAllViews(nullptr);
3556         }
3557 }
3558
3559 /**
3560  * @brief Reloads the opened files
3561  */
3562 void CMergeDoc::OnFileReload()
3563 {
3564         if (!PromptAndSaveIfNeeded(true))
3565                 return;
3566         
3567         FileLocation fileloc[3];
3568         bool bRO[3];
3569         for (int pane = 0; pane < m_nBuffers; pane++)
3570         {
3571                 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3572                 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3573                 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3574                 fileloc[pane].setPath(m_filePaths[pane]);
3575         }
3576         CPoint pt = GetActiveMergeView()->GetCursorPos();
3577         auto columnWidths = m_ptBuf[0]->GetColumnWidths();
3578         if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3579         {
3580                 m_ptBuf[0]->SetColumnWidths(columnWidths);
3581                 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3582         }
3583 }
3584
3585 /**
3586  * @brief Display encodings to user
3587  */
3588 void CMergeDoc::OnFileEncoding()
3589 {
3590         DoFileEncodingDialog();
3591 }
3592
3593 void CMergeDoc::OnOpenWithUnpacker()
3594 {
3595         CSelectPluginDlg dlg(m_infoUnpacker.GetPluginPipeline(),
3596                 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3597                 CSelectPluginDlg::PluginType::Unpacker, false);
3598         if (dlg.DoModal() != IDOK)
3599                 return;
3600
3601         if (!PromptAndSaveIfNeeded(true))
3602                 return;
3603
3604         PackingInfo infoUnpacker(dlg.GetPluginPipeline());
3605         PathContext paths = m_filePaths;
3606         DWORD dwFlags[3] = { FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU };
3607         String strDesc[3] = { m_strDesc[0], m_strDesc[1], m_strDesc[2] };
3608         int nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3609         nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3610
3611         if (GetMainFrame()->DoFileOpen(nID, &paths, dwFlags, strDesc, _T(""), &infoUnpacker))
3612                 GetParentFrame()->DestroyWindow();
3613 }
3614
3615 void CMergeDoc::OnApplyPrediffer() 
3616 {
3617         PrediffingInfo prediffer;
3618         GetPrediffer(&prediffer);
3619         // let the user choose a handler
3620         CSelectPluginDlg dlg(prediffer.GetPluginPipeline(),
3621                 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3622                 CSelectPluginDlg::PluginType::Prediffer, false);
3623         if (dlg.DoModal() != IDOK)
3624                 return;
3625         prediffer.SetPluginPipeline(dlg.GetPluginPipeline());
3626         SetPrediffer(&prediffer);
3627         m_CurrentPredifferID = -1;
3628         FlushAndRescan(true);
3629         SetTitle(nullptr);
3630 }
3631
3632 /**
3633  * @brief Create the dynamic submenu for prediffers
3634  *
3635  * @note The plugins are grouped in (suggested) and (not suggested)
3636  *       The IDs follow the order of GetAvailableScripts
3637  *       For example :
3638  *                              suggested 0         ID_1ST + 0 
3639  *                              suggested 1         ID_1ST + 2 
3640  *                              suggested 2         ID_1ST + 5 
3641  *                              not suggested 0     ID_1ST + 1 
3642  *                              not suggested 1     ID_1ST + 3 
3643  *                              not suggested 2     ID_1ST + 4 
3644  */
3645 HMENU CMergeDoc::createPrediffersSubmenu(HMENU hMenu)
3646 {
3647         // empty the menu
3648         int j = GetMenuItemCount(hMenu);
3649         while (j --)
3650                 DeleteMenu(hMenu, 0, MF_BYPOSITION);
3651
3652         // title
3653         AppendMenu(hMenu, MF_STRING, ID_NO_PREDIFFER, _("No prediffer (normal)").c_str());
3654         
3655         if (!GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED))
3656                 return hMenu;
3657
3658         m_CurrentPredifferID = -1;
3659
3660         // compute the m_CurrentPredifferID (to set the radio button)
3661         PrediffingInfo prediffer;
3662         GetPrediffer(&prediffer);
3663         if (prediffer.GetPluginPipeline().empty())
3664                 m_CurrentPredifferID = ID_NO_PREDIFFER;
3665
3666         // get the scriptlet files
3667         const auto& [ suggestedPlugins, allPlugins ]= FileTransform::CreatePluginMenuInfos(
3668                 m_strBothFilenames, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3669
3670         // build the menu : first part, suggested plugins
3671         // title
3672         AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3673         AppendMenu(hMenu, MF_STRING, ID_SUGGESTED_PLUGINS, _("Suggested plugins").c_str());
3674
3675         for (const auto& [caption, name, id, plugin ] : suggestedPlugins)
3676                 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3677
3678         // build the menu : second part, others plugins
3679         // title
3680         AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3681         AppendMenu(hMenu, MF_STRING, ID_NOT_SUGGESTED_PLUGINS, _("Other plugins").c_str());
3682
3683         String lastPluginName;
3684         String errorMessage;
3685         auto result = prediffer.ParsePluginPipeline(errorMessage);
3686         if (result.size() > 0)
3687                 lastPluginName = result.back().name;
3688         
3689         for (const auto& [processType, pluginAry] : allPlugins)
3690         {
3691                 for (const auto& [caption, name, id, plugin] : pluginAry)
3692                 {
3693                         if (!name.empty())
3694                         {
3695                                 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3696                                 if (lastPluginName == plugin->m_name)
3697                                         m_CurrentPredifferID = id;
3698                         }
3699                 }
3700         }
3701
3702         return hMenu;
3703 }
3704
3705 /**
3706  * @brief Called when an editor script item is updated
3707  */
3708 void CMergeDoc::OnUpdatePrediffer(CCmdUI* pCmdUI)
3709 {
3710         pCmdUI->Enable(true);
3711
3712         PrediffingInfo prediffer;
3713         GetPrediffer(&prediffer);
3714
3715         if (prediffer.GetPluginPipeline().find(_T("<Automatic>")) != String::npos)
3716         {
3717                 pCmdUI->SetRadio(false);
3718                 return;
3719         }
3720
3721         // Detect when CDiffWrapper::RunFileDiff has canceled a buggy prediffer
3722         if (prediffer.GetPluginPipeline().empty())
3723                 m_CurrentPredifferID = ID_NO_PREDIFFER;
3724
3725         pCmdUI->SetRadio(pCmdUI->m_nID == static_cast<UINT>(m_CurrentPredifferID));
3726 }
3727
3728 /**
3729  * @brief Handler for all prediffer choices, including ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO, ID_NO_PREDIFFER, & specific prediffers
3730  */
3731 void CMergeDoc::OnPrediffer(UINT nID )
3732 {
3733         SetPredifferByMenu(nID);
3734         FlushAndRescan(true);
3735         SetTitle(nullptr);
3736 }
3737
3738 /**
3739  * @brief Handler for all prediffer choices.
3740  * Prediffer choises include ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO,
3741  * ID_NO_PREDIFFER, & specific prediffers.
3742  */
3743 void CMergeDoc::SetPredifferByMenu(UINT nID)
3744 {
3745         // update data for the radio button
3746         m_CurrentPredifferID = nID;
3747
3748         if (nID == ID_NO_PREDIFFER)
3749         {
3750                 // All flags are set correctly during the construction
3751                 PrediffingInfo infoPrediffer(false);
3752                 SetPrediffer(&infoPrediffer);
3753                 return;
3754         }
3755
3756         String pluginName = CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3757
3758         // build a PrediffingInfo structure fom the ID
3759         PrediffingInfo prediffer(pluginName);
3760         
3761         // update the prediffer and rescan
3762         SetPrediffer(&prediffer);
3763 }
3764
3765 void CMergeDoc::OnBnClickedFileEncoding()
3766 {
3767         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3768                 return;
3769         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3770         DoFileEncodingDialog();
3771 }
3772
3773 void CMergeDoc::OnBnClickedPlugin()
3774 {
3775         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3776                 return;
3777         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3778         OnOpenWithUnpacker();
3779 }
3780
3781 void CMergeDoc::OnBnClickedHexView()
3782 {
3783         OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3784 }
3785
3786 void CMergeDoc::OnOK()
3787 {
3788         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3789                 return;
3790         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3791 }
3792
3793 void CMergeDoc::OnFileRecompareAsText()
3794 {
3795         m_bEnableTableEditing = false;
3796         OnFileReload();
3797 }
3798
3799 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3800 {
3801         pCmdUI->Enable(m_ptBuf[0]->GetTableEditing());
3802 }
3803
3804 void CMergeDoc::OnFileRecompareAsTable()
3805 {
3806         m_bEnableTableEditing = true;
3807         OnFileReload();
3808 }
3809
3810 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3811 {
3812         pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3813 }
3814
3815 void CMergeDoc::OnFileRecompareAs(UINT nID)
3816 {
3817         if (!PromptAndSaveIfNeeded(true))
3818                 return;
3819         
3820         DWORD dwFlags[3] = { 0 };
3821         FileLocation fileloc[3];
3822         String strDesc[3];
3823         int nBuffers = m_nBuffers;
3824         CDirDoc *pDirDoc = m_pDirDoc->GetMainView() ? m_pDirDoc : 
3825                 static_cast<CDirDoc*>(theApp.m_pDirTemplate->CreateNewDocument());
3826         PackingInfo infoUnpacker(m_infoUnpacker.GetPluginPipeline());
3827
3828         for (int pane = 0; pane < m_nBuffers; pane++)
3829         {
3830                 fileloc[pane].setPath(m_filePaths[pane]);
3831                 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3832                 strDesc[pane] = m_strDesc[pane];
3833         }
3834         if (ID_UNPACKERS_FIRST <= nID && nID <= ID_UNPACKERS_LAST)
3835         {
3836                 infoUnpacker.SetPluginPipeline(CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST));
3837                 nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3838                 nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3839         }
3840
3841         if (GetMainFrame()->ShowMergeDoc(nID, pDirDoc, nBuffers, fileloc, dwFlags, strDesc, _T(""), &infoUnpacker))
3842                 GetParentFrame()->DestroyWindow();
3843 }
3844
3845 // Return file extension either from file name 
3846 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3847 {
3848         String sExt;
3849         paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3850         return sExt;
3851 }
3852
3853 /**
3854  * @brief Generate report from file compare results.
3855  */
3856 bool CMergeDoc::GenerateReport(const String& sFileName) const
3857 {
3858         // calculate HTML font size
3859         LOGFONT lf;
3860         CDC dc;
3861         dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3862         m_pView[0][0]->GetFont(lf);
3863         int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3864
3865         // create HTML report
3866         UniStdioFile file;
3867         if (!file.Open(sFileName, _T("wt")))
3868         {
3869                 String errMsg = GetSysError(GetLastError());
3870                 String msg = strutils::format_string1(
3871                         _("Error creating the report:\n%1"), errMsg);
3872                 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3873                 return false;
3874         }
3875
3876         file.SetCodepage(ucr::CP_UTF_8);
3877
3878         CString headerText =
3879                 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3880                 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3881                 _T("<html>\n")
3882                 _T("<head>\n")
3883                 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3884                 _T("<title>WinMerge File Compare Report</title>\n")
3885                 _T("<style type=\"text/css\">\n")
3886                 _T("<!--\n")
3887                 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3888                 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3889                 _T("tr { vertical-align: top; }\n")
3890                 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3891                 _T("%s")
3892                 _T("-->\n")
3893                 _T("</style>\n")
3894                 _T("</head>\n")
3895                 _T("<body>\n")
3896                 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3897                 _T("<thead>\n")
3898                 _T("<tr>\n");
3899         String header = 
3900                 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3901         file.WriteString(header);
3902
3903         // Get paths
3904         // If archive, use archive path + folder + filename inside archive
3905         // If desc text given, use it
3906         PathContext paths = m_filePaths;
3907         if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3908         {
3909                 for (int i = 0; i < paths.GetSize(); i++)
3910                         m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3911         }
3912         else
3913         {
3914                 for (int i = 0; i < paths.GetSize(); i++)
3915                 {
3916                         if (!m_strDesc[i].empty())
3917                                 paths[i] = m_strDesc[i];
3918                 }
3919         }
3920
3921         // titles
3922         int nBuffer;
3923         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3924         {
3925                 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3926                         (double)100 / m_nBuffers);
3927                 file.WriteString(data);
3928                 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3929                 file.WriteString(_T("</th>\n"));
3930         }
3931         file.WriteString(
3932                 _T("</tr>\n")
3933                 _T("</thead>\n")
3934                 _T("<tbody>\n"));
3935
3936         // write the body of the report
3937         int idx[3] = {0};
3938         int nLineCount[3] = {0};
3939         int nDiff = 0;
3940         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3941                 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3942
3943         for (;;)
3944         {
3945                 file.WriteString(_T("<tr>\n"));
3946                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3947                 {
3948                         for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3949                         {
3950                                 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3951                                         break;
3952                         }
3953                                 
3954                         if (idx[nBuffer] < nLineCount[nBuffer])
3955                         {
3956                                 // line number
3957                                 int iVisibleLineNumber = 0;
3958                                 String tdtag = _T("<td class=\"ln\">");
3959                                 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3960                                 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3961                                 {
3962                                         iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3963                                 }
3964                                 if (nBuffer == 0 &&
3965                                         (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3966                                         (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3967                                 {
3968                                         ++nDiff;
3969                                         if (iVisibleLineNumber > 0)
3970                                         {
3971                                                 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3972                                                 iVisibleLineNumber = 0;
3973                                         }
3974                                         else
3975                                                 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3976                                 }
3977                                 if (iVisibleLineNumber > 0)
3978                                         tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3979                                 else
3980                                         tdtag += _T("</td>");
3981                                 file.WriteString(tdtag);
3982                                 // line content
3983                                 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3984                                 idx[nBuffer]++;
3985                         }
3986                         else
3987                                 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3988                         file.WriteString(_T("\n"));
3989                 }
3990                 file.WriteString(_T("</tr>\n"));
3991
3992                 bool bBorderLine = false;
3993                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3994                 {
3995                         if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3996                                 bBorderLine = true;
3997                 }
3998
3999                 if (bBorderLine)
4000                 {
4001                         file.WriteString(_T("<tr height=1>"));
4002                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
4003                         {
4004                                 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
4005                                         file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
4006                                 else
4007                                         file.WriteString(_T("<td></td><td></td>"));
4008                         }
4009                         file.WriteString(_T("</tr>\n"));
4010                 }
4011
4012                 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
4013                         break;
4014         }
4015         file.WriteString(
4016                 _T("</tbody>\n")
4017                 _T("</table>\n")
4018                 _T("</body>\n")
4019                 _T("</html>\n"));
4020
4021         file.Close();
4022
4023         return true;
4024 }
4025
4026 /**
4027  * @brief Generate report from file compare results.
4028  */
4029 void CMergeDoc::OnToolsGenerateReport()
4030 {
4031         String s;
4032         CString folder;
4033
4034         if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
4035                 return;
4036
4037         if (GenerateReport(s.c_str()))
4038                 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
4039 }
4040
4041 /**
4042  * @brief Generate patch from files selected.
4043  *
4044  * Creates a patch from selected files in active directory compare, or
4045  * active file compare. Files in file compare must be saved before
4046  * creating a patch.
4047  */
4048 void CMergeDoc::OnToolsGeneratePatch()
4049 {
4050         // If there are changes in files, tell user to save them first
4051         if (IsModified())
4052         {
4053                 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
4054                 return;
4055         }
4056
4057         CPatchTool patcher;
4058         patcher.AddFiles(m_filePaths.GetLeft(),
4059                         m_filePaths.GetRight());
4060         patcher.CreatePatch();
4061 }
4062
4063 /**
4064  * @brief Add synchronization point
4065  */
4066 void CMergeDoc::AddSyncPoint()
4067 {
4068         int nLine[3];
4069         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4070         {
4071                 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
4072                 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
4073         }
4074
4075         // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
4076         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4077                 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
4078                 {
4079                         LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
4080                         return;
4081                 }
4082
4083         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4084                 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
4085                         DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
4086
4087         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4088                 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
4089
4090         m_bHasSyncPoints = true;
4091
4092         ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
4093
4094         FlushAndRescan(true);
4095 }
4096
4097 /**
4098  * @brief Delete a synchronization point
4099  */
4100 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
4101 {
4102         const auto syncpoints = GetSyncPointList();     
4103         for (auto syncpnt : syncpoints)
4104         {
4105                 if (syncpnt[pane] == nLine)
4106                 {
4107                         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4108                                 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
4109                 }
4110         }
4111
4112         if (syncpoints.size() == 1)
4113                 m_bHasSyncPoints = false;
4114
4115         if (bRescan)
4116                 FlushAndRescan(true);
4117         return true;
4118 }
4119
4120 /**
4121  * @brief Clear Synchronization points
4122  */
4123 void CMergeDoc::ClearSyncPoints()
4124 {
4125         if (!m_bHasSyncPoints)
4126                 return;
4127
4128         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4129         {
4130                 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
4131                 for (int nLine = 0; nLine < nLineCount; ++nLine)
4132                 {
4133                         if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
4134                                 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
4135                 }
4136         }
4137         
4138         m_bHasSyncPoints = false;
4139
4140         FlushAndRescan(true);
4141 }
4142
4143 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
4144 {
4145         std::vector<std::vector<int> > list;
4146         if (!m_bHasSyncPoints)
4147                 return list;
4148         int idx[3] = {-1, -1, -1};
4149         std::vector<int> points(m_nBuffers);
4150         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4151                 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
4152         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4153         {
4154                 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
4155                 for (int nLine = 0; nLine < nLineCount; ++nLine)
4156                 {
4157                         if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
4158                         {
4159                                 idx[nBuffer]++;
4160                                 if (static_cast<int>(list.size()) <= idx[nBuffer])
4161                                         list.push_back(points);
4162                                 list[idx[nBuffer]][nBuffer] = nLine;
4163                         }
4164                 }
4165         }
4166         return list;
4167 }
4168