OSDN Git Service

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