OSDN Git Service

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