1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
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.
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.
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.
20 /////////////////////////////////////////////////////////////////////////////
24 * @brief Implementation file for CMergeDoc
31 #include <Poco/Timestamp.h>
32 #include "UnicodeString.h"
35 #include "DiffTextBuffer.h"
36 #include "Environment.h"
37 #include "MovedLines.h"
38 #include "MergeEditView.h"
39 #include "MergeEditFrm.h"
42 #include "FileTransform.h"
45 #include "OptionsDef.h"
46 #include "DiffFileInfo.h"
47 #include "SaveClosingDlg.h"
50 #include "OptionsMgr.h"
51 #include "OptionsDiffOptions.h"
52 #include "MergeLineFlags.h"
53 #include "FileOrFolderSelect.h"
54 #include "LineFiltersList.h"
56 #include "codepage_detect.h"
57 #include "SelectUnpackerDlg.h"
58 #include "EncodingErrorBar.h"
59 #include "MergeCmdLineInfo.h"
61 #include "Constants.h"
62 #include "Merge7zFormatMergePluginImpl.h"
64 #include "PatchTool.h"
67 #include "stringdiffs.h"
75 /** @brief Max len of path in caption. */
76 static const UINT CAPTION_PATH_MAX = 50;
78 int CMergeDoc::m_nBuffersTemp = 2;
80 /** @brief EOL types */
81 static LPCTSTR crlfs[] =
83 _T ("\x0d\x0a"), // DOS/Windows style
84 _T ("\x0a"), // UNIX style
85 _T ("\x0d") // Macintosh style
88 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
90 /////////////////////////////////////////////////////////////////////////////
93 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
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)
126 /////////////////////////////////////////////////////////////////////////////
127 // CMergeDoc construction/destruction
130 * @brief Constructor.
132 CMergeDoc::CMergeDoc()
133 : m_bEnableRescan(true)
137 , m_pInfoUnpacker(new PackingInfo)
138 , m_pEncodingErrorBar(nullptr)
139 , m_bHasSyncPoints(false)
140 , m_bAutoMerged(false)
144 DIFFOPTIONS options = {0};
146 m_nBuffers = m_nBuffersTemp;
147 m_filePaths.SetSize(m_nBuffers);
149 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
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;
159 m_bEnableRescan = true;
160 // COleDateTime m_LastRescan
161 curUndo = undoTgt.begin();
162 m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
164 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
165 Options::DiffOptions::Load(GetOptionsMgr(), options);
167 m_diffWrapper.SetOptions(&options);
168 m_diffWrapper.SetPrediffer(nullptr);
174 * Informs associated dirdoc that mergedoc is closing.
176 CMergeDoc::~CMergeDoc()
178 if (m_pDirDoc != nullptr)
180 m_pDirDoc->MergeDocClosing(this);
186 * @brief Deleted data associated with doc before closing.
188 void CMergeDoc::DeleteContents ()
190 CDocument::DeleteContents ();
191 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
193 m_ptBuf[nBuffer]->FreeAll ();
194 m_tempFiles[nBuffer].Delete();
199 * @brief Called when new document is created.
201 * Initialises buffers.
203 BOOL CMergeDoc::OnNewDocument()
205 if (!CDocument::OnNewDocument())
208 SetTitle(_("File Comparison").c_str());
210 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
211 m_ptBuf[nBuffer]->InitNew ();
216 * @brief Return active merge edit view (or left one if neither active)
218 CMergeEditView * CMergeDoc::GetActiveMergeView()
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;
227 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
229 return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
232 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
234 if (infoNewHandler != nullptr)
236 *m_pInfoUnpacker = *infoNewHandler;
240 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
242 m_diffWrapper.SetPrediffer(infoPrediffer);
244 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
246 m_diffWrapper.GetPrediffer(infoPrediffer);
249 /////////////////////////////////////////////////////////////////////////////
250 // CMergeDoc serialization
252 void CMergeDoc::Serialize(CArchive& ar)
254 ASSERT(false); // we do not use CDocument serialization
257 /////////////////////////////////////////////////////////////////////////////
258 // CMergeDoc commands
261 * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
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?
271 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
273 // and we don't repack the file
274 PackingInfo * tempPacker = nullptr;
276 // write buffer out to temporary file
278 int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
279 CRLF_STYLE_AUTOMATIC, false, nStartLine, nLines);
283 * @brief Save files to temp files & compare again.
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
290 * @return Tells if rescan was successfully, was suppressed, or
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()
298 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
299 bool bForced /* =false */)
301 DIFFOPTIONS diffOptions = {0};
302 DiffFileInfo fileInfo;
303 bool diffSuccess = false;
304 int nResult = RESCAN_OK;
305 FileChange FileChanged[3] = {FileNoChange, FileNoChange, FileNoChange};
310 if (!m_bEnableRescan)
311 return RESCAN_SUPPRESSED;
314 ClearWordDiffCache();
316 if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
318 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
322 m_diffWrapper.SetFilterList(_T(""));
324 m_diffWrapper.SetFilterCommentsManager(theApp.m_pFilterCommentsManager.get());
326 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
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())
332 FileChanged[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
333 fileInfo, false, nBuffer);
336 m_LastRescan = COleDateTime::GetCurrentTime();
338 LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
339 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
341 if (FileChanged[nBuffer] == FileRemoved)
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)
349 return RESCAN_FILE_ERR;
353 String temp = m_tempFiles[nBuffer].GetPath();
355 temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
357 return RESCAN_TEMP_ERR;
362 String tempPath = env::GetTemporaryPath();
364 // Set up DiffWrapper
365 m_diffWrapper.GetOptions(&diffOptions);
370 // Clear moved lines lists
371 if (m_diffWrapper.GetDetectMovedBlocks())
373 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
374 m_diffWrapper.GetMovedLines(nBuffer)->Clear();
377 // Set paths for diffing and run diff
378 m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
380 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
382 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
383 m_diffWrapper.SetCompareFiles(m_filePaths);
387 if (!HasSyncPoints())
389 // Save text buffer to file
390 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
392 m_ptBuf[nBuffer]->SetTempPath(tempPath);
393 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
396 m_diffWrapper.SetCreateDiffList(&m_diffList);
397 diffSuccess = m_diffWrapper.RunFileDiff();
400 m_diffWrapper.GetDiffStatus(&status);
401 if (bBinary) // believe caller if we were told these are binaries
402 status.bBinaries = true;
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)
411 // Save text buffer to file
412 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
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]);
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];
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);
436 m_diffWrapper.SetCreateDiffList(&m_diffList);
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)
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);
450 // set identical/diff result as recorded by diffutils
451 identical = status.Identical;
453 // Determine errors and binary file compares
455 nResult = RESCAN_FILE_ERR;
456 else if (status.bBinaries)
462 // Now update views and buffers for ghost lines
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(); });
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();
475 // Divide diff blocks to match lines.
476 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
479 // Analyse diff-list (updating real line-numbers)
480 // this operation does not change the modified flag
483 // Hide identical lines if diff-context is not 'All'
486 // Apply flags to lines that are trivial
487 PrediffingInfo infoPrediffer;
488 GetPrediffer(&infoPrediffer);
489 if (!infoPrediffer.m_PluginName.empty())
492 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
493 if (m_diffWrapper.GetDetectMovedBlocks())
496 // After PrimeTextBuffers() we know amount of real diffs
497 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
499 // Identical files are also updated
500 if (!m_diffList.HasSignificantDiffs())
501 identical = IDENTLEVEL_ALL;
503 ForEachView([](auto& pView) {
504 // just apply some options to the views
505 pView->PrimeListWithFile();
506 // Now buffers data are valid
507 pView->ReAttachToBuffer();
509 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
511 m_bEditAfterRescan[nBuffer] = false;
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;
521 GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL_ALL ? 1 : 0);
526 void CMergeDoc::CheckFileChanged(void)
529 DiffFileInfo fileInfo;
530 FileChange FileChange[3];
532 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
534 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
537 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
540 bool bDoReload = false;
541 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
543 if (FileChange[nBuffer] == FileChanged)
545 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge scanned it last time.\n\nDo you want to reload the file?"), m_filePaths[nBuffer]);
546 if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
553 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
555 if (FileChange[nBuffer] == FileChanged)
556 ChangeFile(nBuffer, m_filePaths[nBuffer]);
561 /** @brief Apply flags to lines that are trivial */
562 void CMergeDoc::FlagTrivialLines(void)
564 for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
566 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
569 for (int file = 0; file < m_nBuffers; ++file)
571 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
572 str[file] = p ? p : _T("");
575 if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
577 DIFFOPTIONS diffOptions = {0};
578 m_diffWrapper.GetOptions(&diffOptions);
580 // Make the call to stringdiffs, which does all the hard & tedious computations
581 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
582 !diffOptions.bIgnoreCase,
583 !diffOptions.bIgnoreEol,
584 diffOptions.nIgnoreWhitespace,
585 GetBreakType(), // whitespace only or include punctuation
586 GetByteColoringOption());
587 if (!worddiffs.empty())
589 for (int file = 0; file < m_nBuffers; ++file)
590 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
597 /** @brief Adjust all different lines that were detected as actually matching moved lines */
598 void CMergeDoc::FlagMovedLines(void)
601 MovedLines *pMovedLines;
603 pMovedLines = m_diffWrapper.GetMovedLines(0);
604 for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
606 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_RIGHT);
609 TRACE(_T("%d->%d\n"), i, j);
611 // We only flag lines that are already marked as being different
612 int apparent = m_ptBuf[0]->ComputeApparentLine(i);
613 if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
615 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
620 pMovedLines = m_diffWrapper.GetMovedLines(1);
621 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
623 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_LEFT);
626 TRACE(_T("%d->%d\n"), i, j);
628 // We only flag lines that are already marked as being different
629 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
630 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
632 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
640 pMovedLines = m_diffWrapper.GetMovedLines(1);
641 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
643 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_RIGHT);
646 TRACE(_T("%d->%d\n"), i, j);
648 // We only flag lines that are already marked as being different
649 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
650 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
652 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
657 pMovedLines = m_diffWrapper.GetMovedLines(2);
658 for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
660 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_LEFT);
663 TRACE(_T("%d->%d\n"), i, j);
665 // We only flag lines that are already marked as being different
666 int apparent = m_ptBuf[2]->ComputeApparentLine(i);
667 if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
669 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
674 // todo: Need to record actual moved information
677 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
679 if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
681 GetParentFrame()->InitialUpdateFrame(this, true);
682 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
684 return AfxMessageBox(sText.c_str(), nType, nIDHelp);
688 * @brief Prints (error) message by rescan status.
690 * @param nRescanResult [in] Resultcocode from rescan().
691 * @param bIdentical [in] Were files identical?.
692 * @sa CMergeDoc::Rescan()
694 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
696 // Rescan was suppressed, there is no sensible status
697 if (nRescanResult == RESCAN_SUPPRESSED)
702 if (nRescanResult == RESCAN_FILE_ERR)
704 s = _("An error occurred while comparing the files.");
706 ShowMessageBox(s, MB_ICONSTOP);
710 if (nRescanResult == RESCAN_TEMP_ERR)
712 s = _("Temporary files could not be created. Check your temporary path settings.");
714 ShowMessageBox(s, MB_ICONSTOP);
718 // Files are not binaries, but they are identical
719 if (identical != IDENTLEVEL_NONE)
721 if (!m_filePaths.GetLeft().empty() && !m_filePaths.GetMiddle().empty() && !m_filePaths.GetRight().empty() &&
722 m_filePaths.GetLeft() == m_filePaths.GetRight() && m_filePaths.GetMiddle() == m_filePaths.GetRight())
724 // compare file to itself, a custom message so user may hide the message in this case only
725 s = _("The same file is opened in both panels.");
726 ShowMessageBox(s, MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN, IDS_FILE_TO_ITSELF);
728 else if (identical == IDENTLEVEL_ALL)
730 UINT nFlags = MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN;
732 if (theApp.m_bExitIfNoDiff == MergeCmdLineInfo::Exit)
734 // Show the "files are identical" for basic "exit no diff" flag
735 // If user don't want to see the message one uses the quiet version
736 // of the "exit no diff".
737 nFlags &= ~MB_DONT_DISPLAY_AGAIN;
740 if (theApp.m_bExitIfNoDiff != MergeCmdLineInfo::ExitQuiet)
742 s = _("The selected files are identical.");
743 ShowMessageBox(s, nFlags, IDS_FILESSAME);
746 // Exit application if files are identical.
747 if (theApp.m_bExitIfNoDiff == MergeCmdLineInfo::Exit ||
748 theApp.m_bExitIfNoDiff == MergeCmdLineInfo::ExitQuiet)
750 AfxGetMainWnd()->PostMessage(WM_COMMAND, ID_APP_EXIT);
756 bool CMergeDoc::Undo()
762 * @brief An instance of RescanSuppress prevents rescan during its lifetime
763 * (or until its Clear method is called, which ends its effect).
768 explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
771 m_bPrev = doc.m_bEnableRescan;
772 m_doc.m_bEnableRescan = false;
779 m_doc.m_bEnableRescan = m_bPrev;
793 * @brief Copy all diffs from one side to side.
794 * @param [in] srcPane Source side from which diff is copied
795 * @param [in] dstPane Destination side
797 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
799 CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
803 * @brief Copy range of diffs from one side to side.
804 * This function copies given range of differences from side to another.
805 * Ignored differences are skipped, and not copied.
806 * @param [in] srcPane Source side from which diff is copied
807 * @param [in] dstPane Destination side
808 * @param [in] firstDiff First diff copied (0-based index)
809 * @param [in] lastDiff Last diff copied (0-based index)
811 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
814 if (firstDiff > lastDiff)
815 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
817 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
818 if (lastDiff > m_diffList.GetSize() - 1)
819 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
822 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
823 firstDiff = max(0, firstDiff);
824 if (firstDiff > lastDiff)
827 RescanSuppress suppressRescan(*this);
829 // Note we don't care about m_nDiffs count to become zero,
830 // because we don't rescan() so it does not change
832 SetCurrentDiff(lastDiff);
834 bool bGroupWithPrevious = false;
835 if (firstWordDiff <= 0 && lastWordDiff == -1)
837 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
838 return; // sync failure
842 if (!WordListCopy(srcPane, dstPane, lastDiff,
843 (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
844 return; // sync failure
847 SetEditedAfterRescan(dstPane);
849 int nGroup = GetActiveMergeView()->m_nThisGroup;
850 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
851 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
852 CPoint currentPosSrc = pViewSrc->GetCursorPos();
854 CPoint currentPosDst = pViewDst->GetCursorPos();
858 pViewDst->SetCursorPos(pt);
859 pViewDst->SetNewSelection(pt, pt, false);
860 pViewDst->SetNewAnchor(pt);
862 // copy from bottom up is more efficient
863 for (int i = lastDiff - 1; i >= firstDiff; --i)
865 if (m_diffList.IsDiffSignificant(i))
868 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
869 if (currentPosDst.y > pdi->dend)
871 if (pdi->blank[dstPane] >= 0)
872 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
873 else if (pdi->blank[srcPane] >= 0)
874 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
876 // Group merge with previous (merge undo data to one action)
877 bGroupWithPrevious = true;
878 if (i > firstDiff || firstWordDiff <= 0)
880 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
881 break; // sync failure
885 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
886 break; // sync failure
891 ForEachView(dstPane, [currentPosDst](auto& pView) {
892 pView->SetCursorPos(currentPosDst);
893 pView->SetNewSelection(currentPosDst, currentPosDst, false);
894 pView->SetNewAnchor(currentPosDst);
897 suppressRescan.Clear(); // done suppress Rescan
901 enum MergeResult { NoMergeNeeded, Merged, Conflict };
904 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
906 bool equal_all = middle == left && middle == right && left == right;
908 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
909 bool conflict = middle != left && middle != right && left != right;
911 return std::pair<MergeResult, Type>(Conflict, left);
916 return std::pair<MergeResult, Type>(Merged, middle);
920 return std::pair<MergeResult, Type>(Merged, right);
922 return std::pair<MergeResult, Type>(Merged, left);
926 return std::pair<MergeResult, Type>(Merged, middle);
929 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
933 * @brief Do auto-merge.
934 * @param [in] dstPane Destination side
936 void CMergeDoc::DoAutoMerge(int dstPane)
938 const int lastDiff = m_diffList.GetSize() - 1;
939 const int firstDiff = 0;
940 bool bGroupWithPrevious = false;
941 int autoMergedCount = 0;
942 int unresolvedConflictCount = 0;
944 std::pair<MergeResult, FileTextEncoding> mergedEncoding =
945 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
946 if (mergedEncoding.first == Merged)
948 ShowMessageBox(_("The change of codepage has been merged"), MB_ICONINFORMATION);
949 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
951 else if (mergedEncoding.first == Conflict)
952 ShowMessageBox(_("The changes of codepage are conflicting"), MB_ICONINFORMATION);
954 std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle =
955 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
956 if (mergedEOLStyle.first == Merged)
958 ShowMessageBox(_("The change of EOL has been merged"), MB_ICONINFORMATION);
959 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
961 else if (mergedEOLStyle.first == Conflict)
962 ShowMessageBox(_("The changes of EOL are conflicting"), MB_ICONINFORMATION);
964 RescanSuppress suppressRescan(*this);
966 // Note we don't care about m_nDiffs count to become zero,
967 // because we don't rescan() so it does not change
969 SetCurrentDiff(lastDiff);
971 SetEditedAfterRescan(dstPane);
973 int nGroup = GetActiveMergeView()->m_nThisGroup;
974 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
975 CPoint currentPosDst = pViewDst->GetCursorPos();
979 pViewDst->SetCursorPos(pt);
980 pViewDst->SetNewSelection(pt, pt, false);
981 pViewDst->SetNewAnchor(pt);
983 // copy from bottom up is more efficient
984 for (int i = lastDiff; i >= firstDiff; --i)
986 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
987 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
991 if (currentPosDst.y > pdi->dend)
993 if (pdi->blank[dstPane] >= 0)
994 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
995 else if (pdi->blank[srcPane] >= 0)
996 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
998 // Group merge with previous (merge undo data to one action)
999 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1000 break; // sync failure
1001 if (!bGroupWithPrevious)
1002 bGroupWithPrevious = true;
1005 if (pdi->op == OP_DIFF)
1006 ++unresolvedConflictCount;
1009 ForEachView(dstPane, [currentPosDst](auto& pView) {
1010 pView->SetCursorPos(currentPosDst);
1011 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1012 pView->SetNewAnchor(currentPosDst);
1015 suppressRescan.Clear(); // done suppress Rescan
1017 UpdateHeaderPath(dstPane);
1019 if (autoMergedCount > 0)
1020 m_bAutoMerged = true;
1022 // move to first conflict
1023 const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1025 pViewDst->SelectDiff(nDiff, true, false);
1028 strutils::format_string2(
1029 _T("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"),
1030 strutils::format(_T("%d"), autoMergedCount),
1031 strutils::format(_T("%d"), unresolvedConflictCount)),
1032 MB_ICONINFORMATION);
1036 * @brief Sanity check difference.
1038 * Checks that lines in difference are inside difference in both files.
1039 * If file is edited, lines added or removed diff lines get out of sync and
1040 * merging fails miserably.
1042 * @param [in] dr Difference to check.
1043 * @return true if difference lines match, false otherwise.
1045 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1047 const int cd_dbegin = dr.dbegin;
1048 const int cd_dend = dr.dend;
1050 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1052 // Must ensure line number is in range before getting line flags
1053 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1056 // Optimization - check last line first so we don't need to
1057 // check whole diff for obvious cases
1058 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1059 if (!(dwFlags & LF_WINMERGE_FLAGS))
1063 for (int line = cd_dbegin; line < cd_dend; line++)
1065 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1067 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1068 if (!(dwFlags & LF_WINMERGE_FLAGS))
1076 * @brief Copy selected (=current) difference from from side to side.
1077 * @param [in] srcPane Source side from which diff is copied
1078 * @param [in] dstPane Destination side
1079 * @param [in] nDiff Diff to copy, if -1 function determines it.
1080 * @param [in] bGroupWithPrevious Adds diff to same undo group with
1081 * @return true if ok, false if sync failure & need to abort copy
1082 * previous action (allows one undo for copy all)
1084 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1085 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1087 int nGroup = GetActiveMergeView()->m_nThisGroup;
1088 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1089 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1090 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1092 // suppress Rescan during this method
1093 // (Not only do we not want to rescan a lot of times, but
1094 // it will wreck the line status array to rescan as we merge)
1095 RescanSuppress suppressRescan(*this);
1097 // If diff-number not given, determine it from active view
1100 nDiff = GetCurrentDiff();
1102 // No current diff, but maybe cursor is in diff?
1103 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1104 pViewDst->IsCursorInDiff()))
1106 // Find out diff under cursor
1107 CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1108 nDiff = m_diffList.LineToDiff(ptCursor.y);
1115 VERIFY(m_diffList.GetDiff(nDiff, cd));
1116 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1117 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1118 bool bSrcWasMod = sbuf.IsModified();
1119 const int cd_dbegin = cd.dbegin;
1120 const int cd_dend = cd.dend;
1121 const int cd_blank = cd.blank[srcPane];
1122 bool bInSync = SanityCheckDiff(cd);
1126 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1127 return false; // abort copying
1130 // If we remove whole diff from current view, we must fix cursor
1131 // position first. Normally we would move to end of previous line,
1132 // but we want to move to begin of that line for usability.
1135 CPoint currentPos = pViewDst->GetCursorPos();
1137 if (currentPos.y > cd_dend)
1139 if (cd.blank[dstPane] >= 0)
1140 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1141 else if (cd.blank[srcPane] >= 0)
1142 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1144 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1147 // if the current diff contains missing lines, remove them from both sides
1148 int limit = cd_dend;
1150 // curView is the view which is changed, so the opposite of the source view
1151 dbuf.BeginUndoGroup(bGroupWithPrevious);
1154 // text was missing, so delete rest of lines on both sides
1155 // delete only on destination side since rescan will clear the other side
1156 if (cd_dend + 1 < dbuf.GetLineCount())
1158 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1162 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1163 ASSERT(cd_blank > 0);
1164 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1168 dbuf.FlushUndoGroup(pSource);
1169 dbuf.BeginUndoGroup(true);
1173 // copy the selected text over
1174 if (cd_dbegin <= limit)
1176 // text exists on left side, so just replace
1177 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1178 dbuf.FlushUndoGroup(pSource);
1179 dbuf.BeginUndoGroup(true);
1181 dbuf.FlushUndoGroup(pSource);
1186 // reset the mod status of the source view because we do make some
1187 // changes, but none that concern the source text
1188 sbuf.SetModified(bSrcWasMod);
1191 suppressRescan.Clear(); // done suppress Rescan
1196 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1197 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1199 int nGroup = GetActiveMergeView()->m_nThisGroup;
1200 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1201 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1202 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1204 // suppress Rescan during this method
1205 // (Not only do we not want to rescan a lot of times, but
1206 // it will wreck the line status array to rescan as we merge)
1207 RescanSuppress suppressRescan(*this);
1210 VERIFY(m_diffList.GetDiff(nDiff, cd));
1211 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1212 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1213 bool bSrcWasMod = sbuf.IsModified();
1214 const int cd_dbegin = cd.dbegin;
1215 const int cd_dend = cd.dend;
1216 const int cd_blank = cd.blank[srcPane];
1217 bool bInSync = SanityCheckDiff(cd);
1221 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1222 return false; // abort copying
1225 std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1227 if (worddiffs.empty())
1230 if (cd.end[srcPane] < cd.begin[srcPane])
1231 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1233 if (firstWordDiff == -1)
1235 if (lastWordDiff == -1)
1236 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1238 // If we remove whole diff from current view, we must fix cursor
1239 // position first. Normally we would move to end of previous line,
1240 // but we want to move to begin of that line for usability.
1243 CPoint currentPos = pViewDst->GetCursorPos();
1245 if (currentPos.y > cd_dend)
1247 if (cd.blank[dstPane] >= 0)
1248 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1249 else if (cd.blank[srcPane] >= 0)
1250 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1252 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1255 // if the current diff contains missing lines, remove them from both sides
1256 int limit = cd_dend;
1258 // curView is the view which is changed, so the opposite of the source view
1259 dbuf.BeginUndoGroup(bGroupWithPrevious);
1261 CString srcText, dstText;
1262 CPoint ptDstStart, ptDstEnd;
1263 CPoint ptSrcStart, ptSrcEnd;
1265 ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1266 ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1267 ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1268 ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1269 ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1270 ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1271 ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1272 ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1274 std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1275 std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1277 dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1278 sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1281 for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1282 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1284 for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1285 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1287 for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1289 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1291 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1292 int srcEnd = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1293 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1294 int dstEnd = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1295 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1296 + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1297 + dstText.Mid(dstEnd - ptDstStart.x);
1300 dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1303 dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1305 dbuf.FlushUndoGroup(pSource);
1307 // reset the mod status of the source view because we do make some
1308 // changes, but none that concern the source text
1309 sbuf.SetModified(bSrcWasMod);
1311 suppressRescan.Clear(); // done suppress Rescan
1318 * @brief Save file with new filename.
1320 * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1321 * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1322 * normal filename fails, to let user choose another filename/location.
1323 * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1324 * using this function.
1325 * @param [in, out] strPath
1326 * - [in] : Initial path shown to user
1327 * - [out] : Path to new filename if saving succeeds
1328 * @param [in, out] nSaveResult
1329 * - [in] : Statuscode telling why we ended up here. Maybe the result of
1331 * - [out] : Statuscode of this saving try
1332 * @param [in, out] sError Error string from lower level saving code
1333 * @param [in] nBuffer Buffer we are saving
1334 * @return false as long as the user is not satisfied. Calling function
1335 * should not continue until true is returned.
1336 * @sa CMergeDoc::DoSave()
1337 * @sa CMergeDoc::DoSaveAs()
1338 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1340 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1341 int nBuffer, PackingInfo * pInfoTempUnpacker)
1345 String strSavePath; // New path for next saving try
1348 int answer = IDOK; // Set default we use for scratchpads
1350 // We shouldn't get here if saving is succeed before
1351 ASSERT(nSaveResult != SAVE_DONE);
1353 // Select message based on reason function called
1354 if (nSaveResult == SAVE_PACK_FAILED)
1356 if (m_nBuffers == 3)
1358 str = strutils::format_string2(
1360 _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")
1362 _("Plugin '%2' cannot pack your changes to the middle file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"):
1363 _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")),
1364 strPath, pInfoTempUnpacker->m_PluginName);
1368 str = strutils::format_string2(nBuffer == 0 ? _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?") :
1369 _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"),
1370 strPath, pInfoTempUnpacker->m_PluginName);
1372 // replace the unpacker with a "do nothing" unpacker
1373 pInfoTempUnpacker->Initialize(PLUGIN_MANUAL);
1377 str = strutils::format_string2(_("Saving file failed.\n%1\n%2\nDo you want to:\n\t-use a different filename (Press Ok)\n\t-abort the current operation (Press Cancel)?"), strPath, sError);
1380 // SAVE_NO_FILENAME is temporarily used for scratchpad.
1381 // So don't ask about saving in that case.
1382 if (nSaveResult != SAVE_NO_FILENAME)
1383 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1389 title = _("Save Left File As");
1390 else if (nBuffer == m_nBuffers - 1)
1391 title = _("Save Right File As");
1393 title = _("Save Middle File As");
1395 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1397 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1399 nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1402 if (nSaveResult == SAVE_DONE)
1404 // We are saving scratchpad (unnamed file)
1405 if (strPath.empty())
1407 m_nBufferType[nBuffer] = BUFFER_UNNAMED_SAVED;
1408 m_strDesc[nBuffer].erase();
1411 strPath = strSavePath;
1412 UpdateHeaderPath(nBuffer);
1418 nSaveResult = SAVE_CANCELLED;
1422 nSaveResult = SAVE_CANCELLED;
1429 * @brief Save file creating backups etc.
1431 * Safe top-level file saving function. Checks validity of given path.
1432 * Creates backup file if wanted to. And if saving to given path fails,
1433 * allows user to select new location/name for file.
1434 * @param [in] szPath Path where to save including filename. Can be
1435 * empty/`nullptr` if new file is created (scratchpad) without filename.
1436 * @param [out] bSaveSuccess Will contain information about save success with
1437 * the original name (to determine if file statuses should be changed)
1438 * @param [in] nBuffer Index (0-based) of buffer to save
1439 * @return Tells if caller can continue (no errors happened)
1440 * @note Return value does not tell if SAVING succeeded. Caller must
1441 * Check value of bSaveSuccess parameter after calling this function!
1442 * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1443 * to directory it points to. If m_strSaveAsPath contains filename,
1444 * that filename is used.
1445 * @sa CMergeDoc::TrySaveAs()
1446 * @sa CMainFrame::CheckSavePath()
1447 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1449 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1451 DiffFileInfo fileInfo;
1452 String strSavePath(szPath);
1453 FileChange fileChanged;
1454 bool bApplyToAll = false;
1457 fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1458 if (fileChanged == FileChanged)
1460 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1461 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1463 bSaveSuccess = true;
1468 // use a temp packer
1469 // first copy the m_pInfoUnpacker
1470 // if an error arises during packing, change and take a "do nothing" packer
1471 PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1473 bSaveSuccess = false;
1475 // Check third arg possibly given from command-line
1476 if (!theApp.m_strSaveAsPath.empty())
1478 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1480 // third arg was a directory, so get append the filename
1482 paths::SplitFilename(szPath, 0, &sname, 0);
1483 strSavePath = theApp.m_strSaveAsPath;
1484 strSavePath = paths::ConcatPath(strSavePath, sname);
1487 strSavePath = theApp.m_strSaveAsPath;
1490 nRetVal = theApp.HandleReadonlySave(strSavePath, false, bApplyToAll);
1491 if (nRetVal == IDCANCEL)
1494 if (!theApp.CreateBackup(false, strSavePath))
1497 // false as long as the user is not satisfied
1498 // true if saving succeeds, even with another filename, or if the user cancels
1499 bool result = false;
1500 // the error code from the latest save operation,
1501 // or SAVE_DONE when the save succeeds
1502 // TODO: Shall we return this code in addition to bSaveSuccess ?
1503 int nSaveErrorCode = SAVE_DONE;
1504 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1506 // Assume empty filename means Scratchpad (unnamed file)
1507 // Todo: This is not needed? - buffer type check should be enough
1508 if (strSavePath.empty())
1509 nSaveErrorCode = SAVE_NO_FILENAME;
1511 // Handle unnamed buffers
1512 if (m_nBufferType[nBuffer] == BUFFER_UNNAMED)
1513 nSaveErrorCode = SAVE_NO_FILENAME;
1516 if (nSaveErrorCode == SAVE_DONE)
1517 // We have a filename, just try to save
1518 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, &infoTempUnpacker);
1520 if (nSaveErrorCode != SAVE_DONE)
1522 // Saving failed, user may save to another location if wants to
1524 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1528 // Saving succeeded with given/selected filename
1529 if (nSaveErrorCode == SAVE_DONE)
1531 // Preserve file times if user wants to
1532 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1534 fileInfo.SetFile(strSavePath);
1537 TFile file(strSavePath);
1538 file.setLastModified(fileInfo.mtime);
1545 m_ptBuf[nBuffer]->SetModified(false);
1546 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1547 m_filePaths[nBuffer] = strSavePath;
1548 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1549 UpdateHeaderPath(nBuffer);
1550 bSaveSuccess = true;
1553 else if (nSaveErrorCode == SAVE_CANCELLED)
1555 // User cancelled current operation, lets do what user wanted to do
1562 * @brief Save file with different filename.
1564 * Safe top-level file saving function. Asks user to select filename
1565 * and path. Does not create backups.
1566 * @param [in] szPath Path where to save including filename. Can be
1567 * empty/`nullptr` if new file is created (scratchpad) without filename.
1568 * @param [out] bSaveSuccess Will contain information about save success with
1569 * the original name (to determine if file statuses should be changed)
1570 * @param [in] nBuffer Index (0-based) of buffer to save
1571 * @return Tells if caller can continue (no errors happened)
1572 * @note Return value does not tell if SAVING succeeded. Caller must
1573 * Check value of bSaveSuccess parameter after calling this function!
1574 * @sa CMergeDoc::TrySaveAs()
1575 * @sa CMainFrame::CheckSavePath()
1576 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1578 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1580 String strSavePath(szPath);
1582 // use a temp packer
1583 // first copy the m_pInfoUnpacker
1584 // if an error arises during packing, change and take a "do nothing" packer
1585 PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1587 bSaveSuccess = false;
1588 // false as long as the user is not satisfied
1589 // true if saving succeeds, even with another filename, or if the user cancels
1590 bool result = false;
1591 // the error code from the latest save operation,
1592 // or SAVE_DONE when the save succeeds
1593 // TODO: Shall we return this code in addition to bSaveSuccess ?
1594 int nSaveErrorCode = SAVE_DONE;
1596 // Use SAVE_NO_FILENAME to prevent asking about error
1597 nSaveErrorCode = SAVE_NO_FILENAME;
1599 // Loop until user succeeds saving or cancels
1602 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1605 // Saving succeeded with given/selected filename
1606 if (nSaveErrorCode == SAVE_DONE)
1608 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1609 m_filePaths[nBuffer] = strSavePath;
1610 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1611 UpdateHeaderPath(nBuffer);
1612 bSaveSuccess = true;
1619 * @brief Get left->right info for a moved line (apparent line number)
1621 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1623 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1626 int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1627 int realRightLine = -1;
1628 if (m_diffWrapper.GetDetectMovedBlocks())
1630 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1631 MovedLines::SIDE_RIGHT);
1633 if (realRightLine != -1)
1634 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1640 * @brief Get right->left info for a moved line (apparent line number)
1642 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1644 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1647 int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1648 int realLeftLine = -1;
1649 if (m_diffWrapper.GetDetectMovedBlocks())
1651 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1652 MovedLines::SIDE_LEFT);
1654 if (realLeftLine != -1)
1655 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1661 * @brief Save modified documents.
1662 * This function asks if user wants to save modified documents. We also
1663 * allow user to cancel the closing.
1665 * There is a special trick avoiding showing two save-dialogs, as MFC framework
1666 * sometimes calls this function twice. We use static counter for these calls
1667 * and if we already have saving in progress (counter == 1) we skip the new
1670 * @return true if docs are closed, false if closing is cancelled.
1672 BOOL CMergeDoc::SaveModified()
1679 if (PromptAndSaveIfNeeded(true))
1692 * @brief Sets the current difference.
1693 * @param [in] nDiff Difference to set as current difference.
1695 void CMergeDoc::SetCurrentDiff(int nDiff)
1697 if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1704 * @brief Take care of rescanning document.
1706 * Update view and restore cursor and scroll position after
1707 * rescanning document.
1708 * @param [in] bForced If true rescan cannot be suppressed
1710 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1712 // Ignore suppressing when forced rescan
1714 if (!m_bEnableRescan) return;
1716 CWaitCursor waitstatus;
1718 CMergeEditView *pActiveView = GetActiveMergeView();
1720 // store cursors and hide caret
1721 ForEachView([](auto& pView) { pView->PushCursors(); });
1722 pActiveView->HideCursor();
1724 bool bBinary = false;
1725 IDENTLEVEL identical = IDENTLEVEL_NONE;
1726 int nRescanResult = Rescan(bBinary, identical, bForced);
1728 // restore cursors and caret
1729 ForEachView([](auto& pView) { pView->PopCursors(); });
1730 pActiveView->ShowCursor();
1732 ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1733 // because of ghostlines, m_nTopLine may differ just after Rescan
1734 // scroll both views to the same top line
1735 pView->UpdateSiblingScrollPos(false);
1737 // make sure we see the cursor from the curent view
1738 pView->EnsureVisible(pView->GetCursorPos());
1742 UpdateAllViews(nullptr);
1744 // Show possible error after updating screen
1745 if (nRescanResult != RESCAN_SUPPRESSED)
1746 ShowRescanError(nRescanResult, identical);
1747 m_LastRescan = COleDateTime::GetCurrentTime();
1751 * @brief Saves both files
1753 void CMergeDoc::OnFileSave()
1755 // We will need to know if either of the originals actually changed
1756 // so we know whether to update the diff status
1757 bool bChangedOriginal = false;
1759 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1761 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1763 // (why we don't use return value of DoSave)
1764 // DoSave will return true if it wrote to something successfully
1765 // but we have to know if it overwrote the original file
1766 bool bSaveOriginal = false;
1767 DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1769 bChangedOriginal = true;
1773 // If either of the actual source files being compared was changed
1774 // we need to update status in the dir view
1775 if (bChangedOriginal)
1777 // If DirDoc contains diffs
1778 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1780 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1782 if (m_bEditAfterRescan[nBuffer])
1784 FlushAndRescan(false);
1789 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1790 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1791 m_nTrivialDiffs, bIdentical);
1796 void CMergeDoc::DoFileSave(int nBuffer)
1798 bool bSaveSuccess = false;
1799 bool bModified = false;
1801 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1804 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
1807 // If file were modified and saving succeeded,
1808 // update status on dir view
1809 if (bModified && bSaveSuccess)
1811 // If DirDoc contains compare results
1812 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1814 for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
1816 if (m_bEditAfterRescan[nBuffer1])
1818 FlushAndRescan(false);
1823 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1824 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1825 m_nTrivialDiffs, bIdentical);
1831 * @brief Saves left-side file
1833 void CMergeDoc::OnFileSaveLeft()
1839 * @brief Saves middle-side file
1841 void CMergeDoc::OnFileSaveMiddle()
1847 * @brief Saves right-side file
1849 void CMergeDoc::OnFileSaveRight()
1851 DoFileSave(m_nBuffers - 1);
1855 * @brief Saves left-side file with name asked
1857 void CMergeDoc::OnFileSaveAsLeft()
1859 bool bSaveResult = false;
1860 DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
1864 * @brief Called when "Save middle (as...)" item is updated
1866 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
1868 pCmdUI->Enable(m_nBuffers == 3);
1872 * @brief Saves right-side file with name asked
1874 void CMergeDoc::OnFileSaveAsMiddle()
1876 bool bSaveResult = false;
1877 DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
1881 * @brief Saves right-side file with name asked
1883 void CMergeDoc::OnFileSaveAsRight()
1885 bool bSaveResult = false;
1886 DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
1890 * @brief Update diff-number pane text in file compare.
1891 * The diff number pane shows selected difference/amount of differences when
1892 * there is difference selected. If there is no difference selected, then
1893 * the panel shows amount of differences. If there are ignored differences,
1894 * those are not count into numbers.
1895 * @param [in] pCmdUI UI component to update.
1897 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
1899 TCHAR sIdx[32] = { 0 };
1900 TCHAR sCnt[32] = { 0 };
1902 const int nDiffs = m_diffList.GetSignificantDiffs();
1904 // Files are identical - show text "Identical"
1908 // There are differences, but no selected diff
1909 // - show amount of diffs
1910 else if (GetCurrentDiff() < 0)
1912 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
1913 _itot_s(nDiffs, sCnt, 10);
1914 strutils::replace(s, _T("%1"), sCnt);
1917 // There are differences and diff selected
1918 // - show diff number and amount of diffs
1921 s = _("Difference %1 of %2");
1922 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
1923 _itot_s(signInd + 1, sIdx, 10);
1924 strutils::replace(s, _T("%1"), sIdx);
1925 _itot_s(nDiffs, sCnt, 10);
1926 strutils::replace(s, _T("%2"), sCnt);
1928 pCmdUI->SetText(s.c_str());
1932 * @brief Update plugin name
1933 * @param [in] pCmdUI UI component to update.
1935 void CMergeDoc::OnUpdatePluginName(CCmdUI* pCmdUI)
1938 if (m_pInfoUnpacker && !m_pInfoUnpacker->m_PluginName.empty())
1939 pluginNames += m_pInfoUnpacker->m_PluginName + _T("&");
1940 PrediffingInfo prediffer;
1941 GetPrediffer(&prediffer);
1942 if (!prediffer.m_PluginName.empty())
1943 pluginNames += prediffer.m_PluginName + _T("&");
1944 pCmdUI->SetText(pluginNames.substr(0, pluginNames.length() - 1).c_str());
1948 * @brief Change number of diff context lines
1950 void CMergeDoc::OnDiffContext(UINT nID)
1954 case ID_VIEW_DIFFCONTEXT_0:
1955 m_nDiffContext = 0; break;
1956 case ID_VIEW_DIFFCONTEXT_1:
1957 m_nDiffContext = 1; break;
1958 case ID_VIEW_DIFFCONTEXT_3:
1959 m_nDiffContext = 3; break;
1960 case ID_VIEW_DIFFCONTEXT_5:
1961 m_nDiffContext = 5; break;
1962 case ID_VIEW_DIFFCONTEXT_7:
1963 m_nDiffContext = 7; break;
1964 case ID_VIEW_DIFFCONTEXT_9:
1965 m_nDiffContext = 9; break;
1966 case ID_VIEW_DIFFCONTEXT_TOGGLE:
1967 m_nDiffContext = -m_nDiffContext - 1; break;
1968 case ID_VIEW_DIFFCONTEXT_ALL:
1969 if (m_nDiffContext >= 0)
1970 m_nDiffContext = -m_nDiffContext - 1;
1973 GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
1974 FlushAndRescan(true);
1978 * @brief Update number of diff context lines
1980 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
1983 switch (pCmdUI->m_nID)
1985 case ID_VIEW_DIFFCONTEXT_0:
1986 bCheck = (m_nDiffContext == 0); break;
1987 case ID_VIEW_DIFFCONTEXT_1:
1988 bCheck = (m_nDiffContext == 1); break;
1989 case ID_VIEW_DIFFCONTEXT_3:
1990 bCheck = (m_nDiffContext == 3); break;
1991 case ID_VIEW_DIFFCONTEXT_5:
1992 bCheck = (m_nDiffContext == 5); break;
1993 case ID_VIEW_DIFFCONTEXT_7:
1994 bCheck = (m_nDiffContext == 7); break;
1995 case ID_VIEW_DIFFCONTEXT_9:
1996 bCheck = (m_nDiffContext == 9); break;
1997 case ID_VIEW_DIFFCONTEXT_TOGGLE:
1998 bCheck = false; break;
2000 bCheck = (m_nDiffContext < 0); break;
2002 pCmdUI->SetCheck(bCheck);
2003 pCmdUI->Enable(true);
2007 * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2009 * @note After PrimeTextBuffers(), all buffers should have the same length.
2011 void CMergeDoc::PrimeTextBuffers()
2014 m_nTrivialDiffs = 0;
2016 int nDiffCount = m_diffList.GetSize();
2019 // walk the diff list and calculate numbers of extra lines to add
2020 int extras[3] = {0, 0, 0}; // extra lines added to each view
2021 m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2023 // resize m_aLines once for each view
2024 UINT lcount[3] = {0, 0, 0};
2025 UINT lcountnew[3] = {0, 0, 0};
2028 for (file = 0; file < m_nBuffers; file++)
2030 lcount[file] = m_ptBuf[file]->GetLineCount();
2031 lcountnew[file] = lcount[file] + extras[file];
2032 lcountmax = max(lcountmax, lcountnew[file]);
2034 for (file = 0; file < m_nBuffers; file++)
2036 m_ptBuf[file]->m_aLines.resize(lcountmax);
2039 // walk the diff list backward, move existing lines to proper place,
2040 // add ghost lines, and set flags
2041 for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2044 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2046 // move matched lines after curDiff
2047 int nline[3] = { 0, 0, 0 };
2048 for (file = 0; file < m_nBuffers; file++)
2049 nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2050 // Matched lines should really match...
2051 // But matched lines after last diff may differ because of empty last line (see function's note)
2052 if (nDiff < nDiffCount - 1)
2053 ASSERT(nline[0] == nline[1]);
2056 for (file = 0; file < m_nBuffers; file++)
2058 // Move all lines after current diff down as far as needed
2059 // for any ghost lines we're about to insert
2060 m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2061 lcountnew[file] -= nline[file];
2062 lcount[file] -= nline[file];
2063 // move unmatched lines and add ghost lines
2064 nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2065 nmaxline = max(nmaxline, nline[file]);
2068 for (file = 0; file < m_nBuffers; file++)
2070 DWORD dflag = LF_GHOST;
2071 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2073 m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2074 int nextra = nmaxline - nline[file];
2077 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2078 for (int i = 1; i <= nextra; i++)
2079 m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2081 lcountnew[file] -= nmaxline;
2083 lcount[file] -= nline[file];
2086 // set dbegin, dend, blank, and line flags
2087 curDiff.dbegin = lcountnew[0];
2093 // fall through and handle as diff
2100 curDiff.dend = lcountnew[0]+nmaxline-1;
2101 for (file = 0; file < m_nBuffers; file++)
2103 curDiff.blank[file] = -1;
2104 int nextra = nmaxline - nline[file];
2105 if (nmaxline > nline[file])
2107 // more lines on left, ghost lines on right side
2108 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2114 for (file = 0; file < m_nBuffers; file++)
2118 for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2120 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2122 // set diff or trivial flag
2123 DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2124 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2126 m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2127 m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2131 // ghost lines are already inserted (and flagged)
2132 // ghost lines opposite to trivial lines are ghost and trivial
2133 if (curDiff.op == OP_TRIVIAL)
2134 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2140 } // switch (curDiff.op)
2141 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2142 } // for (nDiff = nDiffCount; nDiff-- > 0; )
2144 m_diffList.ConstructSignificantChain();
2147 // Note: By this point all `m_ptBuf[]` buffers must have the same
2148 // number of line entries; eventual buffer processing typically
2149 // uses the line count from `m_ptBuf[0]` for all buffer processing.
2151 for (file = 0; file < m_nBuffers; file++)
2153 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2157 for (file = 0; file < m_nBuffers; file++)
2158 m_ptBuf[file]->FinishLoading();
2162 * @brief Checks if file has changed since last update (save or rescan).
2163 * @param [in] szPath File to check
2164 * @param [in] dfi Previous fileinfo of file
2165 * @param [in] bSave If true Compare to last save-info, else to rescan-info
2166 * @param [in] nBuffer Index (0-based) of buffer
2167 * @return true if file is changed.
2169 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2170 bool bSave, int nBuffer)
2172 DiffFileInfo *fileInfo = nullptr;
2173 bool bFileChanged = false;
2174 bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2176 if (bIgnoreSmallDiff)
2177 tolerance = SmallTimeDiff; // From MainFrm.h
2180 fileInfo = m_pSaveFileInfo[nBuffer].get();
2182 fileInfo = m_pRescanFileInfo[nBuffer].get();
2184 // We assume file existed, so disappearing means removal
2185 if (!dfi.Update(szPath))
2188 int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2189 if (timeDiff < 0) timeDiff = -timeDiff;
2190 if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2192 bFileChanged = true;
2198 return FileNoChange;
2201 void CMergeDoc::HideLines()
2206 if (m_nDiffContext < 0)
2208 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2212 int nLineCount = 0x7fffffff;
2213 for (file = 0; file < m_nBuffers; file++)
2215 if (nLineCount > m_ptBuf[file]->GetLineCount())
2216 nLineCount = m_ptBuf[file]->GetLineCount();
2219 for (nLine = 0; nLine < nLineCount;)
2221 if (!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST)))
2223 for (file = 0; file < m_nBuffers; file++)
2224 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2229 int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2230 for (; nLine2 < nLine; nLine2++)
2232 for (file = 0; file < m_nBuffers; file++)
2233 m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2236 for (; nLine < nLineCount; nLine++)
2238 if (!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST)))
2240 for (file = 0; file < m_nBuffers; file++)
2241 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2244 int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2245 for (; nLine < nLineEnd2; nLine++)
2247 for (file = 0; file < m_nBuffers; file++)
2248 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2249 if (m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST))
2250 nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2255 ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2259 * @brief Asks and then saves modified files.
2261 * This function saves modified files. Dialog is shown for user to select
2262 * modified file(s) one wants to save or discard changed. Cancelling of
2263 * save operation is allowed unless denied by parameter. After successfully
2264 * save operation file statuses are updated to directory compare.
2265 * @param [in] bAllowCancel If false "Cancel" button is disabled.
2266 * @return true if user selected "OK" so next operation can be
2267 * executed. If false user choosed "Cancel".
2268 * @note If filename is empty, we assume scratchpads are saved,
2269 * so instead of filename, description is shown.
2270 * @todo If we have filename and description for file, what should
2271 * we do after saving to different filename? Empty description?
2272 * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2274 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2276 bool bLModified = false, bMModified = false, bRModified = false;
2278 bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2280 if (m_nBuffers == 3)
2282 bLModified = m_ptBuf[0]->IsModified();
2283 bMModified = m_ptBuf[1]->IsModified();
2284 bRModified = m_ptBuf[2]->IsModified();
2288 bLModified = m_ptBuf[0]->IsModified();
2289 bRModified = m_ptBuf[1]->IsModified();
2291 if (!bLModified && !bMModified && !bRModified)
2295 dlg.DoAskFor(bLModified, bMModified, bRModified);
2297 dlg.m_bDisableCancel = true;
2298 if (!m_filePaths.GetLeft().empty())
2300 if (theApp.m_strSaveAsPath.empty())
2301 dlg.m_sLeftFile = m_filePaths.GetLeft();
2303 dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2306 dlg.m_sLeftFile = m_strDesc[0];
2307 if (m_nBuffers == 3)
2309 if (!m_filePaths.GetMiddle().empty())
2311 if (theApp.m_strSaveAsPath.empty())
2312 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2314 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2317 dlg.m_sMiddleFile = m_strDesc[1];
2319 if (!m_filePaths.GetRight().empty())
2321 if (theApp.m_strSaveAsPath.empty())
2322 dlg.m_sRightFile = m_filePaths.GetRight();
2324 dlg.m_sRightFile = theApp.m_strSaveAsPath;
2327 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2329 if (dlg.DoModal() == IDOK)
2331 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2333 if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2337 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2339 if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2343 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2345 if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2354 // If file were modified and saving was successfull,
2355 // update status on dir view
2356 if ((bLModified && bLSaveSuccess) ||
2357 (bMModified && bMSaveSuccess) ||
2358 (bRModified && bRSaveSuccess))
2360 // If directory compare has results
2361 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2363 if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2364 FlushAndRescan(false);
2366 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2367 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2368 m_nTrivialDiffs, bIdentical);
2375 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2376 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2378 // if we did not rescan during the request timeOut, Rescan
2379 // else we did Rescan after the request, so do nothing
2380 COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2381 if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2382 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2387 * @brief We have two child views (left & right), so we keep pointers directly
2388 * at them (the MFC view list doesn't have them both)
2390 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2393 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2394 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2398 void CMergeDoc::RemoveMergeViews(int nGroup)
2401 for (; nGroup < m_nGroups - 1; nGroup++)
2403 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2405 m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2406 m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2413 * @brief DirDoc gives us its identity just after it creates us
2415 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2417 ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2418 m_pDirDoc = pDirDoc;
2422 * @brief Return pointer to parent frame
2424 CMergeEditFrame * CMergeDoc::GetParentFrame()
2426 return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame());
2430 * @brief DirDoc is closing
2432 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2434 ASSERT(m_pDirDoc == pDirDoc);
2435 m_pDirDoc = nullptr;
2436 // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2440 * @brief DirDoc commanding us to close
2442 bool CMergeDoc::CloseNow()
2444 // Allow user to cancel closing
2445 if (!PromptAndSaveIfNeeded(true))
2448 GetParentFrame()->CloseNow();
2453 * @brief Loads file to buffer and shows load-errors
2454 * @param [in] sFileName File to open
2455 * @param [in] nBuffer Index (0-based) of buffer to load
2456 * @param [out] readOnly whether file is read-only
2457 * @param [in] encoding encoding used
2458 * @return Tells if files were loaded successfully
2459 * @sa CMergeDoc::OpenDocs()
2461 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2464 DWORD retVal = FileLoadResult::FRESULT_ERROR;
2466 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2467 m_filePaths[nBuffer] = sFileName;
2469 CRLFSTYLE nCrlfStyle = CRLF_STYLE_AUTOMATIC;
2471 retVal = pBuf->LoadFromFile(sFileName, m_pInfoUnpacker.get(),
2472 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2474 // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2475 // it left the pBuf in a valid (but empty) state via a call to InitNew
2477 if (FileLoadResult::IsOkImpure(retVal))
2479 // File loaded, and multiple EOL types in this file
2480 FileLoadResult::SetMainOk(retVal);
2482 // If mixed EOLs are not enabled, enable them for this doc.
2483 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2485 pBuf->SetMixedEOL(true);
2489 if (FileLoadResult::IsError(retVal))
2491 // Error from Unifile/system
2492 if (!sOpenError.IsEmpty())
2493 sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2495 sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2496 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2498 else if (FileLoadResult::IsErrorUnpack(retVal))
2500 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2501 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2507 * @brief Check if specified codepage number is valid for WinMerge Editor.
2508 * @param [in] cp Codepage number to check.
2509 * @return true if codepage is valid, false otherwise.
2511 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2513 if (cp == 0) // 0 is our signal value for invalid
2515 return GetEncodingNameFromCodePage(cp) != nullptr;
2519 * @brief Sanity check file's specified codepage.
2520 * This function checks if file's specified codepage is valid for WinMerge
2521 * editor and if not resets the codepage to default.
2522 * @param [in,out] fileinfo Class containing file's codepage.
2524 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2526 if (fileinfo.encoding.m_unicoding == ucr::NONE
2527 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2529 int cp = ucr::getDefaultCodepage();
2530 if (!IsValidCodepageForMergeEditor(cp))
2532 fileinfo.encoding.SetCodepage(cp);
2537 * @brief Loads one file from disk and updates file infos.
2538 * @param [in] index Index of file in internal buffers.
2539 * @param [in] filename File's name.
2540 * @param [in] readOnly Is file read-only?
2541 * @param [in] encoding File's encoding.
2542 * @return One of FileLoadResult values.
2544 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc,
2545 const FileTextEncoding & encoding)
2547 DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2549 m_strDesc[index] = strDesc;
2550 if (!filename.empty())
2552 if (strDesc.empty())
2553 m_nBufferType[index] = BUFFER_NORMAL;
2555 m_nBufferType[index] = BUFFER_NORMAL_NAMED;
2556 m_pSaveFileInfo[index]->Update(filename);
2557 m_pRescanFileInfo[index]->Update(filename);
2559 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2560 if (FileLoadResult::IsLossy(loadSuccess))
2562 m_ptBuf[index]->FreeAll();
2563 loadSuccess = LoadFile(filename.c_str(), index, readOnly,
2564 GuessCodepageEncoding(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1));
2569 m_nBufferType[index] = BUFFER_UNNAMED;
2570 m_ptBuf[index]->InitNew();
2571 m_ptBuf[index]->m_encoding = encoding;
2572 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer
2573 loadSuccess = FileLoadResult::FRESULT_OK;
2579 * @brief Loads files and does initial rescan.
2580 * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
2581 * @param bRO [in] Is left/middle/right file read-only
2582 * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
2583 * @todo Options are still read from CMainFrame, this will change
2584 * @sa CMainFrame::ShowMergeDoc()
2586 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
2587 const bool bRO[], const String strDesc[])
2589 IDENTLEVEL identical = IDENTLEVEL_NONE;
2590 int nRescanResult = RESCAN_OK;
2592 FileLocation fileloc[3];
2594 std::copy_n(ifileloc, 3, fileloc);
2596 // Filter out invalid codepages, or editor will display all blank
2597 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2598 SanityCheckCodepage(fileloc[nBuffer]);
2602 curUndo = undoTgt.begin();
2604 // Prevent displaying views during LoadFile
2605 // Note : attach buffer again only if both loads succeed
2606 m_strBothFilenames.erase();
2608 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
2610 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2612 // clear undo buffers
2613 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
2616 m_ptBuf[nBuffer]->FreeAll ();
2618 // build the text being filtered, "|" separates files as it is forbidden in filenames
2619 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
2621 m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
2625 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2627 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
2628 fileloc[nBuffer].encoding);
2630 const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
2632 // scratchpad : we don't call LoadFile, so
2633 // we need to initialize the unpacker as a "do nothing" one
2634 if (bFiltersEnabled)
2636 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFER_UNNAMED) == m_nBuffers)
2638 m_pInfoUnpacker->Initialize(PLUGIN_MANUAL);
2642 // Bail out if either side failed
2643 if (std::find_if(nSuccess, nSuccess + m_nBuffers, [](DWORD d){return !FileLoadResult::IsOk(d);} ) != nSuccess + m_nBuffers)
2645 CMergeEditFrame *pFrame = GetParentFrame();
2646 if (pFrame != nullptr)
2648 // Use verify macro to trap possible error in debug.
2649 VERIFY(pFrame->DestroyWindow());
2654 // Warn user if file load was lossy (bad encoding)
2656 int nLossyBuffers = 0;
2657 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2659 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
2661 // TODO: It would be nice to report how many lines were lossy
2662 // we did calculate those numbers when we loaded the files, in the text stats
2664 idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
2668 if (nLossyBuffers > 1)
2669 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
2671 if (nLossyBuffers > 0)
2673 if (m_pEncodingErrorBar == nullptr)
2675 m_pEncodingErrorBar.reset(new CEncodingErrorBar());
2676 m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
2678 m_pEncodingErrorBar->SetText(LoadResString(idres));
2679 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
2682 ForEachView([](auto& pView) {
2683 // Now buffers data are valid
2684 pView->AttachToBuffer();
2685 // Currently there is only one set of syntax colors, which all documents & views share
2686 pView->SetColorContext(theApp.GetMainSyntaxColors());
2687 // Currently there is only one set of markers, which all documents & views share
2688 pView->SetMarkersContext(theApp.GetMainMarkers());
2690 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2692 // Set read-only statuses
2693 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
2696 // Check the EOL sensitivity option (do it before Rescan)
2697 DIFFOPTIONS diffOptions = {0};
2698 m_diffWrapper.GetOptions(&diffOptions);
2699 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
2701 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2702 if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
2705 if (nBuffer < m_nBuffers)
2707 // Options and files not are not compatible :
2708 // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
2709 // All lines will differ, that is not very interesting and probably not wanted.
2710 // Propose to turn off the option 'sensitive to EOL'
2711 String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
2712 if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
2714 diffOptions.bIgnoreEol = true;
2715 m_diffWrapper.SetOptions(&diffOptions);
2717 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
2718 const int nFormerResult = dlg.GetFormerResult();
2719 if (nFormerResult != -1)
2721 // "Don't ask this question again" checkbox is checked
2722 GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
2728 // Define the prediffer
2729 PackingInfo * infoUnpacker = nullptr;
2730 PrediffingInfo * infoPrediffer = nullptr;
2731 if (bFiltersEnabled && m_pDirDoc != nullptr)
2733 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
2734 m_diffWrapper.SetPrediffer(infoPrediffer);
2735 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
2738 bool bBinary = false;
2739 nRescanResult = Rescan(bBinary, identical);
2741 // Open filed if rescan succeed and files are not binaries
2742 if (nRescanResult == RESCAN_OK)
2744 // set the document types
2745 // Warning : it is the first thing to do (must be done before UpdateView,
2746 // or any function that calls UpdateView, like SelectDiff)
2747 // Note: If option enabled, and another side type is not recognized,
2748 // we use recognized type for unrecognized side too.
2753 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2755 if (bFiltersEnabled && m_pInfoUnpacker->m_textType.length())
2756 sext[nBuffer] = m_pInfoUnpacker->m_textType;
2758 sext[nBuffer] = GetFileExt(fileloc[nBuffer].filepath.c_str(), m_strDesc[nBuffer].c_str());
2759 ForEachView(nBuffer, [&](auto& pView) {
2760 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
2761 if (bTyped[nBuffer])
2762 paneTyped = nBuffer;
2766 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2768 if (bTyped[0] != bTyped[nBuffer])
2772 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
2773 if (syntaxHLEnabled && nBuffer < m_nBuffers)
2775 if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
2778 m_ptBuf[0]->GetLine(0, sFirstLine);
2779 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2781 bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
2786 if (syntaxHLEnabled)
2788 CCrystalTextView::TextDefinition *enuType = GetView(0, paneTyped)->GetTextType(sext[paneTyped].c_str());
2789 ForEachView([&bTyped, enuType](auto& pView) {
2790 if (!bTyped[pView->m_nThisPane])
2791 pView->SetTextType(enuType);
2795 int nNormalBuffer = 0;
2796 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2798 // set the frame window header
2799 UpdateHeaderPath(nBuffer);
2801 ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
2803 if ((m_nBufferType[nBuffer] == BUFFER_NORMAL) ||
2804 (m_nBufferType[nBuffer] == BUFFER_NORMAL_NAMED))
2811 // Inform user that files are identical
2812 // Don't show message if new buffers created
2813 if (identical == IDENTLEVEL_ALL && nNormalBuffer > 0)
2815 ShowRescanError(nRescanResult, identical);
2818 // Exit if files are identical should only work for the first
2819 // comparison and must be disabled afterward.
2820 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
2824 // CMergeDoc::Rescan fails if files do not exist on both sides
2825 // or the really arcane case that the temp files couldn't be created,
2826 // which is too obscure to bother reporting if you can't write to
2827 // your temp directory, doing nothing is graceful enough for that).
2828 ShowRescanError(nRescanResult, identical);
2829 GetParentFrame()->DestroyWindow();
2833 // Force repaint of location pane to update it in case we had some warning
2834 // dialog visible and it got painted before files were loaded
2835 if (m_pView[0][0] != nullptr)
2836 m_pView[0][0]->RepaintLocationPane();
2841 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex)
2845 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
2846 if (nPane < 0 || nPane >= m_nBuffers)
2849 if (nLineIndex == -1)
2851 // scroll to first diff
2852 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
2853 m_diffList.HasSignificantDiffs())
2855 int nDiff = m_diffList.FirstSignificantDiff();
2856 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
2857 nLineIndex = m_pView[0][nPane]->GetCursorPos().y;
2860 m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, false, nPane);
2863 void CMergeDoc::ChangeFile(int nBuffer, const String& path)
2865 if (!PromptAndSaveIfNeeded(true))
2868 FileLocation fileloc[3];
2871 for (int pane = 0; pane < m_nBuffers; pane++)
2873 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
2874 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
2875 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
2876 fileloc[pane].setPath(m_filePaths[pane]);
2878 std::copy_n(m_strDesc, m_nBuffers, strDesc);
2880 strDesc[nBuffer] = _T("");
2881 fileloc[nBuffer].setPath(path);
2882 fileloc[nBuffer].encoding = GuessCodepageEncoding(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
2884 OpenDocs(m_nBuffers, fileloc, bRO, strDesc);
2885 MoveOnLoad(nBuffer, 0);
2889 * @brief Re-load a document.
2890 * This methods re-loads the file compare document. The re-loaded document is
2891 * one side of the file compare.
2892 * @param [in] index The document to re-load.
2893 * @return Open result code.
2895 void CMergeDoc::RefreshOptions()
2897 DIFFOPTIONS options = {0};
2899 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
2900 Options::DiffOptions::Load(GetOptionsMgr(), options);
2902 m_diffWrapper.SetOptions(&options);
2904 // Refresh view options
2905 ForEachView([](auto& pView) { pView->RefreshOptions(); });
2909 * @brief Write path and filename to headerbar
2910 * @note SetText() does not repaint unchanged text
2912 void CMergeDoc::UpdateHeaderPath(int pane)
2914 CMergeEditFrame *pf = GetParentFrame();
2915 ASSERT(pf != nullptr);
2917 bool bChanges = false;
2919 if (m_nBufferType[pane] == BUFFER_UNNAMED ||
2920 m_nBufferType[pane] == BUFFER_NORMAL_NAMED)
2922 sText = m_strDesc[pane];
2926 sText = m_filePaths[pane];
2927 if (m_pDirDoc != nullptr)
2929 m_pDirDoc->ApplyDisplayRoot(pane, sText);
2932 bChanges = m_ptBuf[pane]->IsModified();
2935 sText.insert(0, _T("* "));
2937 pf->GetHeaderInterface()->SetText(pane, sText);
2943 * @brief Paint differently the headerbar of the active view
2945 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
2947 CMergeEditFrame *pf = GetParentFrame();
2948 ASSERT(pf != nullptr);
2949 pf->GetHeaderInterface()->SetActive(pane, bActivate);
2953 * @brief Set detect/not detect Moved Blocks
2955 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
2957 if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
2960 GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
2961 m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
2966 * @brief Check if given buffer has mixed EOL style.
2967 * @param [in] nBuffer Buffer to check.
2968 * @return true if buffer's EOL style is mixed, false otherwise.
2970 bool CMergeDoc::IsMixedEOL(int nBuffer) const
2972 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2973 return pBuf->IsMixedEOL();
2976 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
2978 m_bEditAfterRescan[nBuffer] = true;
2982 * @brief Update document filenames to title
2984 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
2987 String sFileName[3];
2989 if (lpszTitle != nullptr)
2993 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2994 sFileName[nBuffer] = !m_strDesc[nBuffer].empty() ? m_strDesc[nBuffer] : paths::FindFileName(m_filePaths[nBuffer]);
2995 if (std::count(&sFileName[0], &sFileName[0] + m_nBuffers, sFileName[0]) == m_nBuffers)
2996 sTitle = sFileName[0] + strutils::format(_T(" x %d"), m_nBuffers);
2998 sTitle = strutils::join(&sFileName[0], &sFileName[0] + m_nBuffers, _T(" - "));
3000 CDocument::SetTitle(sTitle.c_str());
3004 * @brief Update any resources necessary after a GUI language change
3006 void CMergeDoc::UpdateResources()
3011 m_strDesc[0] = _("Untitled left");
3012 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3013 if (m_nBuffers == 3)
3014 m_strDesc[1] = _("Untitled middle");
3015 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3016 UpdateHeaderPath(nBuffer);
3018 GetParentFrame()->UpdateResources();
3019 ForEachView([](auto& pView) { pView->UpdateResources(); });
3022 // Return current word breaking break type setting (whitespace only or include punctuation)
3023 bool CMergeDoc::GetBreakType() const
3025 bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3029 // Return true to do line diff colors at the byte level (false to do them at word level)
3030 bool CMergeDoc::GetByteColoringOption() const
3032 // color at byte level if 'break_on_words' option not set
3033 bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3037 /// Swap files and update views
3038 void CMergeDoc::SwapFiles()
3041 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3043 int nLeftViewId = m_pView[nGroup][0]->GetDlgCtrlID();
3044 int nRightViewId = m_pView[nGroup][m_nBuffers - 1]->GetDlgCtrlID();
3045 m_pView[nGroup][0]->SetDlgCtrlID(nRightViewId);
3046 m_pView[nGroup][m_nBuffers - 1]->SetDlgCtrlID(nLeftViewId);
3050 // Swap buffers and so on
3051 std::swap(m_ptBuf[0], m_ptBuf[m_nBuffers - 1]);
3052 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3053 std::swap(m_pView[nGroup][0], m_pView[nGroup][m_nBuffers - 1]);
3054 std::swap(m_pSaveFileInfo[0], m_pSaveFileInfo[m_nBuffers - 1]);
3055 std::swap(m_pRescanFileInfo[0], m_pRescanFileInfo[m_nBuffers - 1]);
3056 std::swap(m_nBufferType[0], m_nBufferType[m_nBuffers - 1]);
3057 std::swap(m_bEditAfterRescan[0], m_bEditAfterRescan[m_nBuffers - 1]);
3058 std::swap(m_strDesc[0], m_strDesc[m_nBuffers - 1]);
3061 m_diffList.Swap(0, m_nBuffers - 1);
3062 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3063 swap(m_pView[nGroup][0]->m_piMergeEditStatus, m_pView[nGroup][m_nBuffers - 1]->m_piMergeEditStatus);
3065 ClearWordDiffCache();
3067 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3069 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3070 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3071 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3074 UpdateHeaderPath(nBuffer);
3076 GetParentFrame()->UpdateSplitter();
3077 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3079 UpdateAllViews(nullptr);
3083 * @brief Display unpacker dialog to user & handle user's choices
3085 bool CMergeDoc::OpenWithUnpackerDialog()
3087 // let the user choose a handler
3088 CSelectUnpackerDlg dlg(m_filePaths[0], nullptr);
3089 // create now a new infoUnpacker to initialize the manual/automatic flag
3090 PackingInfo infoUnpacker(PLUGIN_AUTO);
3091 dlg.SetInitialInfoHandler(&infoUnpacker);
3093 if (dlg.DoModal() == IDOK)
3095 infoUnpacker = dlg.GetInfoHandler();
3096 Merge7zFormatMergePluginScope scope(&infoUnpacker);
3097 if (HasZipSupport() && std::count_if(m_filePaths.begin(), m_filePaths.end(), ArchiveGuessFormat) == m_nBuffers)
3099 DWORD dwFlags[3] = {FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU};
3100 GetMainFrame()->DoFileOpen(&m_filePaths, dwFlags, m_strDesc, _T(""),
3101 GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, _T(""), &infoUnpacker);
3106 SetUnpacker(&infoUnpacker);
3118 * @brief Reloads the opened files
3120 void CMergeDoc::OnFileReload()
3122 if (!PromptAndSaveIfNeeded(true))
3125 FileLocation fileloc[3];
3127 for (int pane = 0; pane < m_nBuffers; pane++)
3129 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3130 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3131 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3132 fileloc[pane].setPath(m_filePaths[pane]);
3134 CPoint pt = GetActiveMergeView()->GetCursorPos();
3135 OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc);
3136 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3140 * @brief Display encodings to user
3142 void CMergeDoc::OnFileEncoding()
3144 DoFileEncodingDialog();
3147 void CMergeDoc::OnCtxtOpenWithUnpacker()
3149 OpenWithUnpackerDialog();
3152 void CMergeDoc::OnBnClickedFileEncoding()
3154 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3156 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3157 DoFileEncodingDialog();
3160 void CMergeDoc::OnBnClickedPlugin()
3162 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3164 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3165 OpenWithUnpackerDialog();
3168 void CMergeDoc::OnBnClickedHexView()
3170 OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3173 void CMergeDoc::OnOK()
3175 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3177 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3180 void CMergeDoc::OnFileRecompareAsText()
3182 PackingInfo infoUnpacker;
3183 SetUnpacker(&infoUnpacker);
3187 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3189 pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode == PLUGIN_BUILTIN_XML);
3192 void CMergeDoc::OnFileRecompareAsXML()
3194 PackingInfo infoUnpacker(PLUGIN_BUILTIN_XML);
3195 SetUnpacker(&infoUnpacker);
3199 void CMergeDoc::OnUpdateFileRecompareAsXML(CCmdUI *pCmdUI)
3201 pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode != PLUGIN_BUILTIN_XML);
3204 void CMergeDoc::OnFileRecompareAs(UINT nID)
3206 DWORD dwFlags[3] = { 0 };
3207 FileLocation fileloc[3];
3208 for (int pane = 0; pane < m_nBuffers; pane++)
3210 fileloc[pane].setPath(m_filePaths[pane]);
3211 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3213 if (m_pEncodingErrorBar!=nullptr && m_pEncodingErrorBar->IsWindowVisible())
3214 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3215 if (nID == ID_MERGE_COMPARE_HEX)
3216 GetMainFrame()->ShowHexMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3218 GetMainFrame()->ShowImgMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3219 GetParentFrame()->ShowWindow(SW_RESTORE);
3220 GetParentFrame()->DestroyWindow();
3223 // Return file extension either from file name
3224 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3227 paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3232 * @brief Generate report from file compare results.
3234 bool CMergeDoc::GenerateReport(const String& sFileName) const
3236 // calculate HTML font size
3239 dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3240 m_pView[0][0]->GetFont(lf);
3241 int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3243 // create HTML report
3245 if (!file.Open(sFileName, _T("wt")))
3247 String errMsg = GetSysError(GetLastError());
3248 String msg = strutils::format_string1(
3249 _("Error creating the report:\n%1"), errMsg);
3250 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3254 file.SetCodepage(ucr::CP_UTF_8);
3256 CString headerText =
3257 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3258 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3261 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3262 _T("<title>WinMerge File Compare Report</title>\n")
3263 _T("<style type=\"text/css\">\n")
3265 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3266 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3267 _T("tr { vertical-align: top; }\n")
3268 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3274 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3278 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3279 file.WriteString(header);
3282 // If archive, use archive path + folder + filename inside archive
3283 // If desc text given, use it
3284 PathContext paths = m_filePaths;
3285 if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3287 for (int i = 0; i < paths.GetSize(); i++)
3288 m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3292 for (int i = 0; i < paths.GetSize(); i++)
3294 if (!m_strDesc[i].empty())
3295 paths[i] = m_strDesc[i];
3301 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3303 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3304 (double)100 / m_nBuffers);
3305 file.WriteString(data);
3306 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3307 file.WriteString(_T("</th>\n"));
3314 // write the body of the report
3316 int nLineCount[3] = {0};
3318 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3319 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3323 file.WriteString(_T("<tr>\n"));
3324 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3326 for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3328 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3332 if (idx[nBuffer] < nLineCount[nBuffer])
3335 int iVisibleLineNumber = 0;
3336 String tdtag = _T("<td class=\"ln\">");
3337 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3338 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3340 iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3343 (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3344 (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3347 if (iVisibleLineNumber > 0)
3349 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3350 iVisibleLineNumber = 0;
3353 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3355 if (iVisibleLineNumber > 0)
3356 tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3358 tdtag += _T("</td>");
3359 file.WriteString(tdtag);
3361 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3365 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3366 file.WriteString(_T("\n"));
3368 file.WriteString(_T("</tr>\n"));
3370 bool bBorderLine = false;
3371 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3373 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3379 file.WriteString(_T("<tr height=1>"));
3380 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3382 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3383 file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3385 file.WriteString(_T("<td></td><td></td>"));
3387 file.WriteString(_T("</tr>\n"));
3390 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3405 * @brief Generate report from file compare results.
3407 void CMergeDoc::OnToolsGenerateReport()
3412 if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3415 GenerateReport(s.c_str());
3417 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3421 * @brief Generate patch from files selected.
3423 * Creates a patch from selected files in active directory compare, or
3424 * active file compare. Files in file compare must be saved before
3427 void CMergeDoc::OnToolsGeneratePatch()
3429 // If there are changes in files, tell user to save them first
3432 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3437 patcher.AddFiles(m_filePaths.GetLeft(),
3438 m_filePaths.GetRight());
3439 patcher.CreatePatch();
3443 * @brief Add synchronization point
3445 void CMergeDoc::AddSyncPoint()
3448 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3450 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3451 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3453 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3454 DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3457 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3458 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3460 m_bHasSyncPoints = true;
3462 ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3464 FlushAndRescan(true);
3468 * @brief Delete a synchronization point
3470 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3472 const auto syncpoints = GetSyncPointList();
3473 for (auto syncpnt : syncpoints)
3475 if (syncpnt[pane] == nLine)
3477 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3478 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3482 if (syncpoints.size() == 1)
3483 m_bHasSyncPoints = false;
3486 FlushAndRescan(true);
3491 * @brief Clear Synchronization points
3493 void CMergeDoc::ClearSyncPoints()
3495 if (!m_bHasSyncPoints)
3498 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3500 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3501 for (int nLine = 0; nLine < nLineCount; ++nLine)
3503 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3504 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3508 m_bHasSyncPoints = false;
3510 FlushAndRescan(true);
3513 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3515 std::vector<std::vector<int> > list;
3516 if (!m_bHasSyncPoints)
3518 int idx[3] = {-1, -1, -1};
3519 std::vector<int> points(m_nBuffers);
3520 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3521 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3522 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3524 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3525 for (int nLine = 0; nLine < nLineCount; ++nLine)
3527 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3530 if (static_cast<int>(list.size()) <= idx[nBuffer])
3531 list.push_back(points);
3532 list[idx[nBuffer]][nBuffer] = nLine;