OSDN Git Service

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