OSDN Git Service

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