1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
10 * @brief Implementation file for CMergeDoc
17 #include <Poco/Timestamp.h>
18 #include "UnicodeString.h"
21 #include "DiffTextBuffer.h"
22 #include "Environment.h"
23 #include "MovedLines.h"
24 #include "MergeEditView.h"
25 #include "MergeEditFrm.h"
28 #include "FileTransform.h"
31 #include "OptionsDef.h"
32 #include "DiffFileInfo.h"
33 #include "SaveClosingDlg.h"
34 #include "OpenTableDlg.h"
37 #include "OptionsMgr.h"
38 #include "OptionsDiffOptions.h"
39 #include "MergeLineFlags.h"
40 #include "FileOrFolderSelect.h"
41 #include "LineFiltersList.h"
43 #include "codepage_detect.h"
44 #include "SelectUnpackerDlg.h"
45 #include "EncodingErrorBar.h"
46 #include "MergeCmdLineInfo.h"
48 #include "Constants.h"
49 #include "Merge7zFormatMergePluginImpl.h"
51 #include "PatchTool.h"
54 #include "stringdiffs.h"
62 int CMergeDoc::m_nBuffersTemp = 2;
64 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
66 /////////////////////////////////////////////////////////////////////////////
69 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
71 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
72 //{{AFX_MSG_MAP(CMergeDoc)
73 ON_COMMAND(ID_FILE_SAVE, OnFileSave)
74 ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
75 ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
76 ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
77 ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
78 ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
79 ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
80 ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
81 ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
82 ON_UPDATE_COMMAND_UI(ID_STATUS_PLUGIN, OnUpdatePluginName)
83 ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
84 ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
85 ON_COMMAND(ID_RESCAN, OnFileReload)
86 ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
87 ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
88 ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
89 ON_COMMAND(ID_POPUP_OPEN_WITH_UNPACKER, OnCtxtOpenWithUnpacker)
90 ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
91 ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
92 ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
93 ON_COMMAND(IDOK, OnOK)
94 ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
95 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
96 ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
97 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
98 ON_COMMAND(ID_MERGE_COMPARE_XML, OnFileRecompareAsXML)
99 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_XML, OnUpdateFileRecompareAsXML)
100 ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
104 /////////////////////////////////////////////////////////////////////////////
105 // CMergeDoc construction/destruction
108 * @brief Constructor.
110 CMergeDoc::CMergeDoc()
111 : m_bEnableRescan(true)
113 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
116 , m_pInfoUnpacker(new PackingInfo)
117 , m_pEncodingErrorBar(nullptr)
118 , m_bHasSyncPoints(false)
119 , m_bAutoMerged(false)
123 DIFFOPTIONS options = {0};
125 m_nBuffers = m_nBuffersTemp;
126 m_filePaths.SetSize(m_nBuffers);
128 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
130 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
131 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
132 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
133 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
134 m_bEditAfterRescan[nBuffer] = false;
137 m_bEnableRescan = true;
138 // COleDateTime m_LastRescan
139 curUndo = undoTgt.begin();
140 m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
141 m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
143 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
144 Options::DiffOptions::Load(GetOptionsMgr(), options);
146 m_diffWrapper.SetOptions(&options);
147 m_diffWrapper.SetPrediffer(nullptr);
153 * Informs associated dirdoc that mergedoc is closing.
155 CMergeDoc::~CMergeDoc()
157 if (m_pDirDoc != nullptr)
159 m_pDirDoc->MergeDocClosing(this);
165 * @brief Deleted data associated with doc before closing.
167 void CMergeDoc::DeleteContents ()
169 CDocument::DeleteContents ();
170 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
172 m_ptBuf[nBuffer]->FreeAll ();
173 m_tempFiles[nBuffer].Delete();
178 * @brief Called when new document is created.
180 * Initialises buffers.
182 BOOL CMergeDoc::OnNewDocument()
184 if (!CDocument::OnNewDocument())
187 SetTitle(_("File Comparison").c_str());
189 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
190 m_ptBuf[nBuffer]->InitNew ();
195 * @brief Return active merge edit view (or left one if neither active)
197 CMergeEditView * CMergeDoc::GetActiveMergeView()
199 CView * pActiveView = GetParentFrame()->GetActiveView();
200 CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
201 if (pMergeEditView == nullptr)
202 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
203 return pMergeEditView;
206 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
208 return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
211 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
213 if (infoNewHandler != nullptr)
215 *m_pInfoUnpacker = *infoNewHandler;
219 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
221 m_diffWrapper.SetPrediffer(infoPrediffer);
223 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
225 m_diffWrapper.GetPrediffer(infoPrediffer);
228 /////////////////////////////////////////////////////////////////////////////
229 // CMergeDoc serialization
231 void CMergeDoc::Serialize(CArchive& ar)
233 ASSERT(false); // we do not use CDocument serialization
236 /////////////////////////////////////////////////////////////////////////////
237 // CMergeDoc commands
240 * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
243 * original file is Ansi :
244 * buffer -> save as Ansi -> Ansi plugins -> diffutils
245 * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
246 * buffer -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
247 * (the plugins are optional, not the conversion)
248 * @todo Show SaveToFile() errors?
250 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
252 // and we don't repack the file
253 PackingInfo * tempPacker = nullptr;
255 // write buffer out to temporary file
257 int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
258 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
262 * @brief Save files to temp files & compare again.
264 * @param bBinary [in,out] [in] If true, compare two binary files
265 * [out] If true binary file was detected.
266 * @param bIdentical [out] If true files were identical
267 * @param bForced [in] If true, suppressing is ignored and rescan
269 * @return Tells if rescan was successfully, was suppressed, or
271 * If this code is OK, Rescan has detached the views temporarily
272 * (positions of cursors have been lost)
273 * @note Rescan() ALWAYS compares temp files. Actual user files are not
274 * touched by Rescan().
275 * @sa CDiffWrapper::RunFileDiff()
277 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
278 bool bForced /* =false */)
280 DIFFOPTIONS diffOptions = {0};
281 DiffFileInfo fileInfo;
282 bool diffSuccess = false;
283 int nResult = RESCAN_OK;
284 FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
289 if (!m_bEnableRescan)
290 return RESCAN_SUPPRESSED;
293 ClearWordDiffCache();
295 if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
297 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
301 m_diffWrapper.SetFilterList(_T(""));
303 if (GetView(0, 0)->m_CurSourceDef->type != 0)
304 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
306 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
308 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
310 // Check if files have been modified since last rescan
311 // Ignore checking in case of scratchpads (empty filenames)
312 if (!m_filePaths[nBuffer].empty())
314 Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
315 fileInfo, false, nBuffer);
318 m_LastRescan = COleDateTime::GetCurrentTime();
320 LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
321 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
323 if (Changed[nBuffer] == FileChange::Removed)
325 String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
326 ShowMessageBox(msg, MB_ICONWARNING);
327 bool bSaveResult = false;
328 bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
329 if (!ok || !bSaveResult)
331 return RESCAN_FILE_ERR;
335 String temp = m_tempFiles[nBuffer].GetPath();
337 temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
339 return RESCAN_TEMP_ERR;
344 String tempPath = env::GetTemporaryPath();
346 // Set up DiffWrapper
347 m_diffWrapper.GetOptions(&diffOptions);
352 m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
353 // Clear moved lines lists
354 if (m_diffWrapper.GetDetectMovedBlocks())
356 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
357 m_diffWrapper.GetMovedLines(nBuffer)->Clear();
360 // Set paths for diffing and run diff
361 m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
363 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
365 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
366 m_diffWrapper.SetCompareFiles(m_filePaths);
370 if (!HasSyncPoints())
372 // Save text buffer to file
373 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
375 m_ptBuf[nBuffer]->SetTempPath(tempPath);
376 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
379 m_diffWrapper.SetCreateDiffList(&m_diffList);
380 diffSuccess = m_diffWrapper.RunFileDiff();
383 m_diffWrapper.GetDiffStatus(&status);
384 if (bBinary) // believe caller if we were told these are binaries
385 status.bBinaries = true;
389 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();
390 int nStartLine[3] = {0};
391 int nLines[3], nRealLine[3];
392 for (size_t i = 0; i <= syncpoints.size(); ++i)
394 // Save text buffer to file
395 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
397 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
398 m_ptBuf[nBuffer]->SetTempPath(tempPath);
399 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(),
400 nStartLine[nBuffer], nLines[nBuffer]);
404 m_diffWrapper.SetCreateDiffList(&templist);
405 diffSuccess = m_diffWrapper.RunFileDiff();
406 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
407 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
409 // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
410 if (i == 0 && templist.GetSize() > 0)
411 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
412 if (nStartLine[nBuffer] == 0)
414 bool isEmptyFile = true;
415 for (int j = 0; j < nLines[nBuffer]; j++)
417 if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
426 templist.GetDiff(0, di);
427 if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
429 di.end[nBuffer] = -1;
430 templist.SetDiff(0, di);
435 m_diffList.AppendDiffList(templist, nRealLine);
436 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
437 nStartLine[nBuffer] += nLines[nBuffer];
440 DIFFSTATUS status_part;
441 m_diffWrapper.GetDiffStatus(&status_part);
442 if (bBinary) // believe caller if we were told these are binaries
443 status.bBinaries = true;
444 status.MergeStatus(status_part);
446 m_diffWrapper.SetCreateDiffList(&m_diffList);
449 // If one file has EOL before EOF and other not...
450 if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
452 // ..last DIFFRANGE of file which has EOL must be
453 // fixed to contain last line too
454 int lineCount[3] = { 0,0,0 };
455 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
456 lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
457 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
460 // set identical/diff result as recorded by diffutils
461 identical = status.Identical;
463 // Determine errors and binary file compares
465 nResult = RESCAN_FILE_ERR;
466 else if (status.bBinaries)
472 // Now update views and buffers for ghost lines
474 // Prevent displaying views during this update
475 // BTW, this solves the problem of double asserts
476 // (during the display of an assert message box, a second assert in one of the
477 // display functions happens, and hides the first assert)
478 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
480 // Remove blank lines and clear winmerge flags
481 // this operation does not change the modified flag
482 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
483 m_ptBuf[nBuffer]->prepareForRescan();
485 // Divide diff blocks to match lines.
486 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
489 // Analyse diff-list (updating real line-numbers)
490 // this operation does not change the modified flag
493 // Hide identical lines if diff-context is not 'All'
496 // Apply flags to lines that are trivial
497 PrediffingInfo infoPrediffer;
498 GetPrediffer(&infoPrediffer);
499 if (!infoPrediffer.m_PluginName.empty())
502 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
503 if (m_diffWrapper.GetDetectMovedBlocks())
506 // After PrimeTextBuffers() we know amount of real diffs
507 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
509 // Identical files are also updated
510 if (!m_diffList.HasSignificantDiffs())
511 identical = IDENTLEVEL::ALL;
513 ForEachView([](auto& pView) {
514 // just apply some options to the views
515 pView->PrimeListWithFile();
516 // Now buffers data are valid
517 pView->ReAttachToBuffer();
519 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
521 m_bEditAfterRescan[nBuffer] = false;
525 if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
526 identical == IDENTLEVEL::ALL &&
527 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
528 [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
529 identical = IDENTLEVEL::NONE;
531 GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
536 void CMergeDoc::CheckFileChanged(void)
539 DiffFileInfo fileInfo;
540 FileChange FileChange[3];
542 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
544 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
547 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
550 bool bDoReload = false;
551 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
553 if (FileChange[nBuffer] == FileChange::Changed)
555 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]);
556 if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
563 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
565 if (FileChange[nBuffer] == FileChange::Changed)
567 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
568 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
574 /** @brief Apply flags to lines that are trivial */
575 void CMergeDoc::FlagTrivialLines(void)
577 for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
579 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
582 for (int file = 0; file < m_nBuffers; ++file)
584 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
585 str[file] = p ? p : _T("");
588 if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
590 DIFFOPTIONS diffOptions = {0};
591 m_diffWrapper.GetOptions(&diffOptions);
593 // Make the call to stringdiffs, which does all the hard & tedious computations
594 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
595 !diffOptions.bIgnoreCase,
596 !diffOptions.bIgnoreEol,
597 diffOptions.nIgnoreWhitespace,
598 GetBreakType(), // whitespace only or include punctuation
599 GetByteColoringOption());
600 if (!worddiffs.empty())
602 for (int file = 0; file < m_nBuffers; ++file)
603 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
610 /** @brief Adjust all different lines that were detected as actually matching moved lines */
611 void CMergeDoc::FlagMovedLines(void)
614 MovedLines *pMovedLines;
616 pMovedLines = m_diffWrapper.GetMovedLines(0);
617 for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
619 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
622 TRACE(_T("%d->%d\n"), i, j);
624 // We only flag lines that are already marked as being different
625 int apparent = m_ptBuf[0]->ComputeApparentLine(i);
626 if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
628 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
629 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
631 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
632 if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
633 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
639 pMovedLines = m_diffWrapper.GetMovedLines(1);
640 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
642 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
645 TRACE(_T("%d->%d\n"), i, j);
647 // We only flag lines that are already marked as being different
648 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
649 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
651 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
652 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
654 int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
655 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
656 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
665 pMovedLines = m_diffWrapper.GetMovedLines(1);
666 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
668 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
671 TRACE(_T("%d->%d\n"), i, j);
673 // We only flag lines that are already marked as being different
674 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
675 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
677 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
678 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
680 int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
681 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
682 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
688 pMovedLines = m_diffWrapper.GetMovedLines(2);
689 for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
691 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
694 TRACE(_T("%d->%d\n"), i, j);
696 // We only flag lines that are already marked as being different
697 int apparent = m_ptBuf[2]->ComputeApparentLine(i);
698 if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
700 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
701 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
703 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
704 if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
705 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
711 // todo: Need to record actual moved information
714 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
716 if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
718 GetParentFrame()->InitialUpdateFrame(this, true);
719 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
721 return AfxMessageBox(sText.c_str(), nType, nIDHelp);
725 * @brief Prints (error) message by rescan status.
727 * @param nRescanResult [in] Resultcocode from rescan().
728 * @param bIdentical [in] Were files identical?.
729 * @sa CMergeDoc::Rescan()
731 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
733 // Rescan was suppressed, there is no sensible status
734 if (nRescanResult == RESCAN_SUPPRESSED)
739 if (nRescanResult == RESCAN_FILE_ERR)
741 s = _("An error occurred while comparing the files.");
743 ShowMessageBox(s, MB_ICONSTOP);
747 if (nRescanResult == RESCAN_TEMP_ERR)
749 s = _("Temporary files could not be created. Check your temporary path settings.");
751 ShowMessageBox(s, MB_ICONSTOP);
755 // Files are not binaries, but they are identical
756 if (identical != IDENTLEVEL::NONE)
758 if (theApp.m_bExitIfNoDiff != MergeCmdLineInfo::ExitQuiet)
760 UINT nFlags = MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN;
762 if (theApp.m_bExitIfNoDiff == MergeCmdLineInfo::Exit)
764 // Show the "files are identical" for basic "exit no diff" flag
765 // If user don't want to see the message one uses the quiet version
766 // of the "exit no diff".
767 nFlags &= ~MB_DONT_DISPLAY_AGAIN;
769 if ((m_nBuffers == 2 && !m_filePaths.GetLeft().empty() && !m_filePaths.GetRight().empty() &&
770 strutils::compare_nocase(m_filePaths.GetLeft(), m_filePaths.GetRight()) == 0) ||
771 (m_nBuffers == 3 && !m_filePaths.GetLeft().empty() && !m_filePaths.GetMiddle().empty() && !m_filePaths.GetRight().empty() &&
772 (strutils::compare_nocase(m_filePaths.GetLeft(), m_filePaths.GetRight()) == 0 ||
773 strutils::compare_nocase(m_filePaths.GetMiddle(), m_filePaths.GetRight()) == 0 ||
774 strutils::compare_nocase(m_filePaths.GetLeft(), m_filePaths.GetMiddle()) == 0)))
776 // compare file to itself, a custom message so user may hide the message in this case only
777 s = _("The same file is opened in both panels.");
778 ShowMessageBox(s, nFlags, IDS_FILE_TO_ITSELF);
780 else if (identical == IDENTLEVEL::ALL)
782 s = _("The selected files are identical.");
783 ShowMessageBox(s, nFlags, IDS_FILESSAME);
787 if (identical == IDENTLEVEL::ALL)
789 // Exit application if files are identical.
790 if (theApp.m_bExitIfNoDiff == MergeCmdLineInfo::Exit ||
791 theApp.m_bExitIfNoDiff == MergeCmdLineInfo::ExitQuiet)
793 AfxGetMainWnd()->PostMessage(WM_COMMAND, ID_APP_EXIT);
799 bool CMergeDoc::Undo()
805 * @brief An instance of RescanSuppress prevents rescan during its lifetime
806 * (or until its Clear method is called, which ends its effect).
811 explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
814 m_bPrev = doc.m_bEnableRescan;
815 m_doc.m_bEnableRescan = false;
822 m_doc.m_bEnableRescan = m_bPrev;
836 * @brief Copy all diffs from one side to side.
837 * @param [in] srcPane Source side from which diff is copied
838 * @param [in] dstPane Destination side
840 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
842 CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
846 * @brief Copy range of diffs from one side to side.
847 * This function copies given range of differences from side to another.
848 * Ignored differences are skipped, and not copied.
849 * @param [in] srcPane Source side from which diff is copied
850 * @param [in] dstPane Destination side
851 * @param [in] firstDiff First diff copied (0-based index)
852 * @param [in] lastDiff Last diff copied (0-based index)
854 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
857 if (firstDiff > lastDiff)
858 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
860 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
861 if (lastDiff > m_diffList.GetSize() - 1)
862 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
865 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
866 firstDiff = max(0, firstDiff);
867 if (firstDiff > lastDiff)
870 RescanSuppress suppressRescan(*this);
872 // Note we don't care about m_nDiffs count to become zero,
873 // because we don't rescan() so it does not change
875 SetCurrentDiff(lastDiff);
877 bool bGroupWithPrevious = false;
878 if (firstWordDiff <= 0 && lastWordDiff == -1)
880 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
881 return; // sync failure
885 if (!WordListCopy(srcPane, dstPane, lastDiff,
886 (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
887 return; // sync failure
890 SetEditedAfterRescan(dstPane);
892 int nGroup = GetActiveMergeView()->m_nThisGroup;
893 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
894 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
895 CPoint currentPosSrc = pViewSrc->GetCursorPos();
897 CPoint currentPosDst = pViewDst->GetCursorPos();
901 pViewDst->SetCursorPos(pt);
902 pViewDst->SetNewSelection(pt, pt, false);
903 pViewDst->SetNewAnchor(pt);
905 // copy from bottom up is more efficient
906 for (int i = lastDiff - 1; i >= firstDiff; --i)
908 if (m_diffList.IsDiffSignificant(i))
911 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
912 if (currentPosDst.y > pdi->dend)
914 if (pdi->blank[dstPane] >= 0)
915 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
916 else if (pdi->blank[srcPane] >= 0)
917 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
919 // Group merge with previous (merge undo data to one action)
920 bGroupWithPrevious = true;
921 if (i > firstDiff || firstWordDiff <= 0)
923 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
924 break; // sync failure
928 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
929 break; // sync failure
934 ForEachView(dstPane, [currentPosDst](auto& pView) {
935 pView->SetCursorPos(currentPosDst);
936 pView->SetNewSelection(currentPosDst, currentPosDst, false);
937 pView->SetNewAnchor(currentPosDst);
940 suppressRescan.Clear(); // done suppress Rescan
944 enum MergeResult { NoMergeNeeded, Merged, Conflict };
947 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
949 bool equal_all = middle == left && middle == right && left == right;
951 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
952 bool conflict = middle != left && middle != right && left != right;
954 return std::pair<MergeResult, Type>(Conflict, left);
959 return std::pair<MergeResult, Type>(Merged, middle);
963 return std::pair<MergeResult, Type>(Merged, right);
965 return std::pair<MergeResult, Type>(Merged, left);
969 return std::pair<MergeResult, Type>(Merged, middle);
972 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
976 * @brief Do auto-merge.
977 * @param [in] dstPane Destination side
979 void CMergeDoc::DoAutoMerge(int dstPane)
981 const int lastDiff = m_diffList.GetSize() - 1;
982 const int firstDiff = 0;
983 bool bGroupWithPrevious = false;
984 int autoMergedCount = 0;
985 int unresolvedConflictCount = 0;
987 std::pair<MergeResult, FileTextEncoding> mergedEncoding =
988 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
989 if (mergedEncoding.first == Merged)
991 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
992 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
994 else if (mergedEncoding.first == Conflict)
995 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
997 std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle =
998 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
999 if (mergedEOLStyle.first == Merged)
1001 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1002 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1004 else if (mergedEOLStyle.first == Conflict)
1005 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1007 RescanSuppress suppressRescan(*this);
1009 // Note we don't care about m_nDiffs count to become zero,
1010 // because we don't rescan() so it does not change
1012 SetCurrentDiff(lastDiff);
1014 SetEditedAfterRescan(dstPane);
1016 int nGroup = GetActiveMergeView()->m_nThisGroup;
1017 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1018 CPoint currentPosDst = pViewDst->GetCursorPos();
1019 currentPosDst.x = 0;
1022 pViewDst->SetCursorPos(pt);
1023 pViewDst->SetNewSelection(pt, pt, false);
1024 pViewDst->SetNewAnchor(pt);
1026 // copy from bottom up is more efficient
1027 for (int i = lastDiff; i >= firstDiff; --i)
1029 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1030 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1034 if (currentPosDst.y > pdi->dend)
1036 if (pdi->blank[dstPane] >= 0)
1037 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1038 else if (pdi->blank[srcPane] >= 0)
1039 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1041 // Group merge with previous (merge undo data to one action)
1042 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1043 break; // sync failure
1044 if (!bGroupWithPrevious)
1045 bGroupWithPrevious = true;
1048 if (pdi->op == OP_DIFF)
1049 ++unresolvedConflictCount;
1052 ForEachView(dstPane, [currentPosDst](auto& pView) {
1053 pView->SetCursorPos(currentPosDst);
1054 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1055 pView->SetNewAnchor(currentPosDst);
1058 suppressRescan.Clear(); // done suppress Rescan
1060 UpdateHeaderPath(dstPane);
1062 if (autoMergedCount > 0)
1063 m_bAutoMerged = true;
1065 // move to first conflict
1066 const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1068 pViewDst->SelectDiff(nDiff, true, false);
1071 strutils::format_string2(
1072 _T("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"),
1073 strutils::format(_T("%d"), autoMergedCount),
1074 strutils::format(_T("%d"), unresolvedConflictCount)),
1075 MB_ICONINFORMATION);
1079 * @brief Sanity check difference.
1081 * Checks that lines in difference are inside difference in both files.
1082 * If file is edited, lines added or removed diff lines get out of sync and
1083 * merging fails miserably.
1085 * @param [in] dr Difference to check.
1086 * @return true if difference lines match, false otherwise.
1088 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1090 const int cd_dbegin = dr.dbegin;
1091 const int cd_dend = dr.dend;
1093 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1095 // Must ensure line number is in range before getting line flags
1096 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1099 // Optimization - check last line first so we don't need to
1100 // check whole diff for obvious cases
1101 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1102 if (!(dwFlags & LF_WINMERGE_FLAGS))
1106 for (int line = cd_dbegin; line < cd_dend; line++)
1108 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1110 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1111 if (!(dwFlags & LF_WINMERGE_FLAGS))
1119 * @brief Copy selected (=current) difference from from side to side.
1120 * @param [in] srcPane Source side from which diff is copied
1121 * @param [in] dstPane Destination side
1122 * @param [in] nDiff Diff to copy, if -1 function determines it.
1123 * @param [in] bGroupWithPrevious Adds diff to same undo group with
1124 * @return true if ok, false if sync failure & need to abort copy
1125 * previous action (allows one undo for copy all)
1127 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1128 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1130 int nGroup = GetActiveMergeView()->m_nThisGroup;
1131 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1132 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1133 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1135 // suppress Rescan during this method
1136 // (Not only do we not want to rescan a lot of times, but
1137 // it will wreck the line status array to rescan as we merge)
1138 RescanSuppress suppressRescan(*this);
1140 // If diff-number not given, determine it from active view
1143 nDiff = GetCurrentDiff();
1145 // No current diff, but maybe cursor is in diff?
1146 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1147 pViewDst->IsCursorInDiff()))
1149 // Find out diff under cursor
1150 CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1151 nDiff = m_diffList.LineToDiff(ptCursor.y);
1158 VERIFY(m_diffList.GetDiff(nDiff, cd));
1159 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1160 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1161 bool bSrcWasMod = sbuf.IsModified();
1162 const int cd_dbegin = cd.dbegin;
1163 const int cd_dend = cd.dend;
1164 const int cd_blank = cd.blank[srcPane];
1165 bool bInSync = SanityCheckDiff(cd);
1169 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1170 return false; // abort copying
1173 // If we remove whole diff from current view, we must fix cursor
1174 // position first. Normally we would move to end of previous line,
1175 // but we want to move to begin of that line for usability.
1178 CPoint currentPos = pViewDst->GetCursorPos();
1180 if (currentPos.y > cd_dend)
1182 if (cd.blank[dstPane] >= 0)
1183 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1184 else if (cd.blank[srcPane] >= 0)
1185 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1187 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1190 // if the current diff contains missing lines, remove them from both sides
1191 int limit = cd_dend;
1193 // curView is the view which is changed, so the opposite of the source view
1194 dbuf.BeginUndoGroup(bGroupWithPrevious);
1197 // text was missing, so delete rest of lines on both sides
1198 // delete only on destination side since rescan will clear the other side
1199 if (cd_dend + 1 < dbuf.GetLineCount())
1201 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1205 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1206 ASSERT(cd_blank > 0);
1207 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1211 dbuf.FlushUndoGroup(pSource);
1212 dbuf.BeginUndoGroup(true);
1216 // copy the selected text over
1217 if (cd_dbegin <= limit)
1219 // text exists on left side, so just replace
1220 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1221 dbuf.FlushUndoGroup(pSource);
1222 dbuf.BeginUndoGroup(true);
1224 dbuf.FlushUndoGroup(pSource);
1229 // reset the mod status of the source view because we do make some
1230 // changes, but none that concern the source text
1231 sbuf.SetModified(bSrcWasMod);
1234 suppressRescan.Clear(); // done suppress Rescan
1239 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1240 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1242 int nGroup = GetActiveMergeView()->m_nThisGroup;
1243 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1244 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1245 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1247 // suppress Rescan during this method
1248 // (Not only do we not want to rescan a lot of times, but
1249 // it will wreck the line status array to rescan as we merge)
1250 RescanSuppress suppressRescan(*this);
1253 VERIFY(m_diffList.GetDiff(nDiff, cd));
1254 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1255 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1256 bool bSrcWasMod = sbuf.IsModified();
1257 const int cd_dbegin = cd.dbegin;
1258 const int cd_dend = cd.dend;
1259 const int cd_blank = cd.blank[srcPane];
1260 bool bInSync = SanityCheckDiff(cd);
1264 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1265 return false; // abort copying
1268 std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1270 if (worddiffs.empty())
1273 if (cd.end[srcPane] < cd.begin[srcPane])
1274 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1276 if (firstWordDiff == -1)
1278 if (lastWordDiff == -1)
1279 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1281 // If we remove whole diff from current view, we must fix cursor
1282 // position first. Normally we would move to end of previous line,
1283 // but we want to move to begin of that line for usability.
1286 CPoint currentPos = pViewDst->GetCursorPos();
1288 if (currentPos.y > cd_dend)
1290 if (cd.blank[dstPane] >= 0)
1291 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1292 else if (cd.blank[srcPane] >= 0)
1293 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1295 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1298 // if the current diff contains missing lines, remove them from both sides
1299 int limit = cd_dend;
1301 // curView is the view which is changed, so the opposite of the source view
1302 dbuf.BeginUndoGroup(bGroupWithPrevious);
1304 CString srcText, dstText;
1305 CPoint ptDstStart, ptDstEnd;
1306 CPoint ptSrcStart, ptSrcEnd;
1308 ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1309 ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1310 ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1311 ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1312 ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1313 ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1314 ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1315 ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1317 std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1318 std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1320 dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1321 sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1324 for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1325 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1327 for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1328 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1330 for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1332 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1334 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1335 int srcEnd = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1336 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1337 int dstEnd = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1338 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1339 + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1340 + dstText.Mid(dstEnd - ptDstStart.x);
1343 dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1346 dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1348 dbuf.FlushUndoGroup(pSource);
1350 // reset the mod status of the source view because we do make some
1351 // changes, but none that concern the source text
1352 sbuf.SetModified(bSrcWasMod);
1354 suppressRescan.Clear(); // done suppress Rescan
1361 * @brief Save file with new filename.
1363 * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1364 * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1365 * normal filename fails, to let user choose another filename/location.
1366 * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1367 * using this function.
1368 * @param [in, out] strPath
1369 * - [in] : Initial path shown to user
1370 * - [out] : Path to new filename if saving succeeds
1371 * @param [in, out] nSaveResult
1372 * - [in] : Statuscode telling why we ended up here. Maybe the result of
1374 * - [out] : Statuscode of this saving try
1375 * @param [in, out] sError Error string from lower level saving code
1376 * @param [in] nBuffer Buffer we are saving
1377 * @return false as long as the user is not satisfied. Calling function
1378 * should not continue until true is returned.
1379 * @sa CMergeDoc::DoSave()
1380 * @sa CMergeDoc::DoSaveAs()
1381 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1383 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1384 int nBuffer, PackingInfo * pInfoTempUnpacker)
1388 String strSavePath; // New path for next saving try
1391 int answer = IDOK; // Set default we use for scratchpads
1393 // We shouldn't get here if saving is succeed before
1394 ASSERT(nSaveResult != SAVE_DONE);
1396 // Select message based on reason function called
1397 if (nSaveResult == SAVE_PACK_FAILED)
1399 if (m_nBuffers == 3)
1401 str = strutils::format_string2(
1403 _("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?")
1405 _("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?"):
1406 _("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?")),
1407 strPath, pInfoTempUnpacker->m_PluginName);
1411 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?") :
1412 _("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?"),
1413 strPath, pInfoTempUnpacker->m_PluginName);
1415 // replace the unpacker with a "do nothing" unpacker
1416 pInfoTempUnpacker->Initialize(PLUGIN_MODE::PLUGIN_MANUAL);
1420 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);
1423 // SAVE_NO_FILENAME is temporarily used for scratchpad.
1424 // So don't ask about saving in that case.
1425 if (nSaveResult != SAVE_NO_FILENAME)
1426 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1432 title = _("Save Left File As");
1433 else if (nBuffer == m_nBuffers - 1)
1434 title = _("Save Right File As");
1436 title = _("Save Middle File As");
1438 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1440 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1442 nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1445 if (nSaveResult == SAVE_DONE)
1447 // We are saving scratchpad (unnamed file)
1448 if (strPath.empty())
1450 m_nBufferType[nBuffer] = BUFFERTYPE::UNNAMED_SAVED;
1451 m_strDesc[nBuffer].erase();
1454 strPath = strSavePath;
1455 UpdateHeaderPath(nBuffer);
1461 nSaveResult = SAVE_CANCELLED;
1465 nSaveResult = SAVE_CANCELLED;
1472 * @brief Save file creating backups etc.
1474 * Safe top-level file saving function. Checks validity of given path.
1475 * Creates backup file if wanted to. And if saving to given path fails,
1476 * allows user to select new location/name for file.
1477 * @param [in] szPath Path where to save including filename. Can be
1478 * empty/`nullptr` if new file is created (scratchpad) without filename.
1479 * @param [out] bSaveSuccess Will contain information about save success with
1480 * the original name (to determine if file statuses should be changed)
1481 * @param [in] nBuffer Index (0-based) of buffer to save
1482 * @return Tells if caller can continue (no errors happened)
1483 * @note Return value does not tell if SAVING succeeded. Caller must
1484 * Check value of bSaveSuccess parameter after calling this function!
1485 * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1486 * to directory it points to. If m_strSaveAsPath contains filename,
1487 * that filename is used.
1488 * @sa CMergeDoc::TrySaveAs()
1489 * @sa CMainFrame::CheckSavePath()
1490 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1492 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1494 DiffFileInfo fileInfo;
1495 String strSavePath(szPath);
1496 FileChange fileChanged;
1497 bool bApplyToAll = false;
1500 fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1501 if (fileChanged == FileChange::Changed)
1503 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1504 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1506 bSaveSuccess = true;
1511 // use a temp packer
1512 // first copy the m_pInfoUnpacker
1513 // if an error arises during packing, change and take a "do nothing" packer
1514 PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1516 bSaveSuccess = false;
1518 // Check third arg possibly given from command-line
1519 if (!theApp.m_strSaveAsPath.empty())
1521 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1523 // third arg was a directory, so get append the filename
1525 paths::SplitFilename(szPath, 0, &sname, 0);
1526 strSavePath = theApp.m_strSaveAsPath;
1527 strSavePath = paths::ConcatPath(strSavePath, sname);
1530 strSavePath = theApp.m_strSaveAsPath;
1533 nRetVal = theApp.HandleReadonlySave(strSavePath, false, bApplyToAll);
1534 if (nRetVal == IDCANCEL)
1537 if (!theApp.CreateBackup(false, strSavePath))
1540 // false as long as the user is not satisfied
1541 // true if saving succeeds, even with another filename, or if the user cancels
1542 bool result = false;
1543 // the error code from the latest save operation,
1544 // or SAVE_DONE when the save succeeds
1545 // TODO: Shall we return this code in addition to bSaveSuccess ?
1546 int nSaveErrorCode = SAVE_DONE;
1547 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1549 // Assume empty filename means Scratchpad (unnamed file)
1550 // Todo: This is not needed? - buffer type check should be enough
1551 if (strSavePath.empty())
1552 nSaveErrorCode = SAVE_NO_FILENAME;
1554 // Handle unnamed buffers
1555 if (m_nBufferType[nBuffer] == BUFFERTYPE::UNNAMED)
1556 nSaveErrorCode = SAVE_NO_FILENAME;
1559 if (nSaveErrorCode == SAVE_DONE)
1560 // We have a filename, just try to save
1561 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, &infoTempUnpacker);
1563 if (nSaveErrorCode != SAVE_DONE)
1565 // Saving failed, user may save to another location if wants to
1567 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1571 // Saving succeeded with given/selected filename
1572 if (nSaveErrorCode == SAVE_DONE)
1574 // Preserve file times if user wants to
1575 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1577 fileInfo.SetFile(strSavePath);
1580 TFile file(strSavePath);
1581 file.setLastModified(fileInfo.mtime);
1588 m_ptBuf[nBuffer]->SetModified(false);
1589 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1590 m_filePaths[nBuffer] = strSavePath;
1591 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1592 UpdateHeaderPath(nBuffer);
1593 bSaveSuccess = true;
1596 else if (nSaveErrorCode == SAVE_CANCELLED)
1598 // User cancelled current operation, lets do what user wanted to do
1605 * @brief Save file with different filename.
1607 * Safe top-level file saving function. Asks user to select filename
1608 * and path. Does not create backups.
1609 * @param [in] szPath Path where to save including filename. Can be
1610 * empty/`nullptr` if new file is created (scratchpad) without filename.
1611 * @param [out] bSaveSuccess Will contain information about save success with
1612 * the original name (to determine if file statuses should be changed)
1613 * @param [in] nBuffer Index (0-based) of buffer to save
1614 * @return Tells if caller can continue (no errors happened)
1615 * @note Return value does not tell if SAVING succeeded. Caller must
1616 * Check value of bSaveSuccess parameter after calling this function!
1617 * @sa CMergeDoc::TrySaveAs()
1618 * @sa CMainFrame::CheckSavePath()
1619 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1621 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1623 String strSavePath(szPath);
1625 // use a temp packer
1626 // first copy the m_pInfoUnpacker
1627 // if an error arises during packing, change and take a "do nothing" packer
1628 PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1630 bSaveSuccess = false;
1631 // false as long as the user is not satisfied
1632 // true if saving succeeds, even with another filename, or if the user cancels
1633 bool result = false;
1634 // the error code from the latest save operation,
1635 // or SAVE_DONE when the save succeeds
1636 // TODO: Shall we return this code in addition to bSaveSuccess ?
1637 int nSaveErrorCode = SAVE_DONE;
1639 // Use SAVE_NO_FILENAME to prevent asking about error
1640 nSaveErrorCode = SAVE_NO_FILENAME;
1642 // Loop until user succeeds saving or cancels
1645 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1648 // Saving succeeded with given/selected filename
1649 if (nSaveErrorCode == SAVE_DONE)
1651 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1652 m_filePaths[nBuffer] = strSavePath;
1653 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1654 UpdateHeaderPath(nBuffer);
1655 bSaveSuccess = true;
1662 * @brief Get left->right info for a moved line (apparent line number)
1664 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1666 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1669 int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1670 int realRightLine = -1;
1671 if (m_diffWrapper.GetDetectMovedBlocks())
1673 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1674 MovedLines::SIDE::RIGHT);
1676 if (realRightLine != -1)
1677 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1683 * @brief Get right->left info for a moved line (apparent line number)
1685 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1687 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1690 int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1691 int realLeftLine = -1;
1692 if (m_diffWrapper.GetDetectMovedBlocks())
1694 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1695 MovedLines::SIDE::LEFT);
1697 if (realLeftLine != -1)
1698 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1704 * @brief Save modified documents.
1705 * This function asks if user wants to save modified documents. We also
1706 * allow user to cancel the closing.
1708 * There is a special trick avoiding showing two save-dialogs, as MFC framework
1709 * sometimes calls this function twice. We use static counter for these calls
1710 * and if we already have saving in progress (counter == 1) we skip the new
1713 * @return true if docs are closed, false if closing is cancelled.
1715 BOOL CMergeDoc::SaveModified()
1722 if (PromptAndSaveIfNeeded(true))
1735 * @brief Sets the current difference.
1736 * @param [in] nDiff Difference to set as current difference.
1738 void CMergeDoc::SetCurrentDiff(int nDiff)
1740 if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1747 * @brief Take care of rescanning document.
1749 * Update view and restore cursor and scroll position after
1750 * rescanning document.
1751 * @param [in] bForced If true rescan cannot be suppressed
1753 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1755 // Ignore suppressing when forced rescan
1757 if (!m_bEnableRescan) return;
1759 CWaitCursor waitstatus;
1761 CMergeEditView *pActiveView = GetActiveMergeView();
1763 // store cursors and hide caret
1764 ForEachView([](auto& pView) { pView->PushCursors(); });
1765 pActiveView->HideCursor();
1767 bool bBinary = false;
1768 IDENTLEVEL identical = IDENTLEVEL::NONE;
1769 int nRescanResult = Rescan(bBinary, identical, bForced);
1771 // restore cursors and caret
1772 ForEachView([](auto& pView) { pView->PopCursors(); });
1773 pActiveView->ShowCursor();
1775 ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1776 // because of ghostlines, m_nTopLine may differ just after Rescan
1777 // scroll both views to the same top line
1778 pView->UpdateSiblingScrollPos(false);
1780 // make sure we see the cursor from the curent view
1781 pView->EnsureVisible(pView->GetCursorPos());
1785 UpdateAllViews(nullptr);
1787 // Show possible error after updating screen
1788 if (nRescanResult != RESCAN_SUPPRESSED)
1789 ShowRescanError(nRescanResult, identical);
1790 m_LastRescan = COleDateTime::GetCurrentTime();
1794 * @brief Saves both files
1796 void CMergeDoc::OnFileSave()
1798 // We will need to know if either of the originals actually changed
1799 // so we know whether to update the diff status
1800 bool bChangedOriginal = false;
1802 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1804 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1806 // (why we don't use return value of DoSave)
1807 // DoSave will return true if it wrote to something successfully
1808 // but we have to know if it overwrote the original file
1809 bool bSaveOriginal = false;
1810 DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1812 bChangedOriginal = true;
1816 // If either of the actual source files being compared was changed
1817 // we need to update status in the dir view
1818 if (bChangedOriginal)
1820 // If DirDoc contains diffs
1821 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1823 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1825 if (m_bEditAfterRescan[nBuffer])
1827 FlushAndRescan(false);
1832 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1833 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1834 m_nTrivialDiffs, bIdentical);
1839 void CMergeDoc::DoFileSave(int nBuffer)
1841 bool bSaveSuccess = false;
1842 bool bModified = false;
1844 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1847 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
1850 // If file were modified and saving succeeded,
1851 // update status on dir view
1852 if (bModified && bSaveSuccess)
1854 // If DirDoc contains compare results
1855 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1857 for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
1859 if (m_bEditAfterRescan[nBuffer1])
1861 FlushAndRescan(false);
1866 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1867 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1868 m_nTrivialDiffs, bIdentical);
1874 * @brief Saves left-side file
1876 void CMergeDoc::OnFileSaveLeft()
1882 * @brief Saves middle-side file
1884 void CMergeDoc::OnFileSaveMiddle()
1890 * @brief Saves right-side file
1892 void CMergeDoc::OnFileSaveRight()
1894 DoFileSave(m_nBuffers - 1);
1898 * @brief Saves left-side file with name asked
1900 void CMergeDoc::OnFileSaveAsLeft()
1902 bool bSaveResult = false;
1903 DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
1907 * @brief Called when "Save middle (as...)" item is updated
1909 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
1911 pCmdUI->Enable(m_nBuffers == 3);
1915 * @brief Saves right-side file with name asked
1917 void CMergeDoc::OnFileSaveAsMiddle()
1919 bool bSaveResult = false;
1920 DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
1924 * @brief Saves right-side file with name asked
1926 void CMergeDoc::OnFileSaveAsRight()
1928 bool bSaveResult = false;
1929 DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
1933 * @brief Update diff-number pane text in file compare.
1934 * The diff number pane shows selected difference/amount of differences when
1935 * there is difference selected. If there is no difference selected, then
1936 * the panel shows amount of differences. If there are ignored differences,
1937 * those are not count into numbers.
1938 * @param [in] pCmdUI UI component to update.
1940 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
1942 TCHAR sIdx[32] = { 0 };
1943 TCHAR sCnt[32] = { 0 };
1945 const int nDiffs = m_diffList.GetSignificantDiffs();
1947 // Files are identical - show text "Identical"
1951 // There are differences, but no selected diff
1952 // - show amount of diffs
1953 else if (GetCurrentDiff() < 0)
1955 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
1956 _itot_s(nDiffs, sCnt, 10);
1957 strutils::replace(s, _T("%1"), sCnt);
1960 // There are differences and diff selected
1961 // - show diff number and amount of diffs
1964 s = _("Difference %1 of %2");
1965 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
1966 _itot_s(signInd + 1, sIdx, 10);
1967 strutils::replace(s, _T("%1"), sIdx);
1968 _itot_s(nDiffs, sCnt, 10);
1969 strutils::replace(s, _T("%2"), sCnt);
1971 pCmdUI->SetText(s.c_str());
1975 * @brief Update plugin name
1976 * @param [in] pCmdUI UI component to update.
1978 void CMergeDoc::OnUpdatePluginName(CCmdUI* pCmdUI)
1981 if (m_pInfoUnpacker && !m_pInfoUnpacker->m_PluginName.empty())
1982 pluginNames += m_pInfoUnpacker->m_PluginName + _T("&");
1983 PrediffingInfo prediffer;
1984 GetPrediffer(&prediffer);
1985 if (!prediffer.m_PluginName.empty())
1986 pluginNames += prediffer.m_PluginName + _T("&");
1987 pCmdUI->SetText(pluginNames.substr(0, pluginNames.length() - 1).c_str());
1991 * @brief Change number of diff context lines
1993 void CMergeDoc::OnDiffContext(UINT nID)
1997 case ID_VIEW_DIFFCONTEXT_0:
1998 m_nDiffContext = 0; break;
1999 case ID_VIEW_DIFFCONTEXT_1:
2000 m_nDiffContext = 1; break;
2001 case ID_VIEW_DIFFCONTEXT_3:
2002 m_nDiffContext = 3; break;
2003 case ID_VIEW_DIFFCONTEXT_5:
2004 m_nDiffContext = 5; break;
2005 case ID_VIEW_DIFFCONTEXT_7:
2006 m_nDiffContext = 7; break;
2007 case ID_VIEW_DIFFCONTEXT_9:
2008 m_nDiffContext = 9; break;
2009 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2010 m_nDiffContext = -m_nDiffContext - 1; break;
2011 case ID_VIEW_DIFFCONTEXT_ALL:
2012 if (m_nDiffContext >= 0)
2013 m_nDiffContext = -m_nDiffContext - 1;
2015 case ID_VIEW_DIFFCONTEXT_INVERT:
2016 m_bInvertDiffContext = !m_bInvertDiffContext;
2019 GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2020 GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2021 FlushAndRescan(true);
2025 * @brief Update number of diff context lines
2027 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2030 switch (pCmdUI->m_nID)
2032 case ID_VIEW_DIFFCONTEXT_0:
2033 bCheck = (m_nDiffContext == 0); break;
2034 case ID_VIEW_DIFFCONTEXT_1:
2035 bCheck = (m_nDiffContext == 1); break;
2036 case ID_VIEW_DIFFCONTEXT_3:
2037 bCheck = (m_nDiffContext == 3); break;
2038 case ID_VIEW_DIFFCONTEXT_5:
2039 bCheck = (m_nDiffContext == 5); break;
2040 case ID_VIEW_DIFFCONTEXT_7:
2041 bCheck = (m_nDiffContext == 7); break;
2042 case ID_VIEW_DIFFCONTEXT_9:
2043 bCheck = (m_nDiffContext == 9); break;
2044 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2045 bCheck = false; break;
2046 case ID_VIEW_DIFFCONTEXT_INVERT:
2047 bCheck = m_bInvertDiffContext; break;
2049 bCheck = (m_nDiffContext < 0); break;
2051 pCmdUI->SetCheck(bCheck);
2052 pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2056 * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2058 * @note After PrimeTextBuffers(), all buffers should have the same length.
2060 void CMergeDoc::PrimeTextBuffers()
2063 m_nTrivialDiffs = 0;
2065 int nDiffCount = m_diffList.GetSize();
2068 // walk the diff list and calculate numbers of extra lines to add
2069 int extras[3] = {0, 0, 0}; // extra lines added to each view
2070 m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2072 // resize m_aLines once for each view
2073 UINT lcount[3] = {0, 0, 0};
2074 UINT lcountnew[3] = {0, 0, 0};
2077 for (file = 0; file < m_nBuffers; file++)
2079 lcount[file] = m_ptBuf[file]->GetLineCount();
2080 lcountnew[file] = lcount[file] + extras[file];
2081 lcountmax = max(lcountmax, lcountnew[file]);
2083 for (file = 0; file < m_nBuffers; file++)
2085 m_ptBuf[file]->m_aLines.resize(lcountmax);
2088 // walk the diff list backward, move existing lines to proper place,
2089 // add ghost lines, and set flags
2090 for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2093 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2095 // move matched lines after curDiff
2096 int nline[3] = { 0, 0, 0 };
2097 for (file = 0; file < m_nBuffers; file++)
2098 nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2099 // Matched lines should really match...
2100 // But matched lines after last diff may differ because of empty last line (see function's note)
2101 if (nDiff < nDiffCount - 1)
2102 ASSERT(nline[0] == nline[1]);
2105 for (file = 0; file < m_nBuffers; file++)
2107 // Move all lines after current diff down as far as needed
2108 // for any ghost lines we're about to insert
2109 m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2110 lcountnew[file] -= nline[file];
2111 lcount[file] -= nline[file];
2112 // move unmatched lines and add ghost lines
2113 nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2114 nmaxline = max(nmaxline, nline[file]);
2117 for (file = 0; file < m_nBuffers; file++)
2119 DWORD dflag = LF_GHOST;
2120 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2122 m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2123 int nextra = nmaxline - nline[file];
2126 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2127 for (int i = 1; i <= nextra; i++)
2128 m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2130 lcountnew[file] -= nmaxline;
2132 lcount[file] -= nline[file];
2135 // set dbegin, dend, blank, and line flags
2136 curDiff.dbegin = lcountnew[0];
2149 curDiff.dend = lcountnew[0]+nmaxline-1;
2150 for (file = 0; file < m_nBuffers; file++)
2152 curDiff.blank[file] = -1;
2153 int nextra = nmaxline - nline[file];
2154 if (nmaxline > nline[file])
2156 // more lines on left, ghost lines on right side
2157 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2163 for (file = 0; file < m_nBuffers; file++)
2167 for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2169 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2171 // set diff or trivial flag
2172 DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2173 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2175 m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2176 m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2180 // ghost lines are already inserted (and flagged)
2181 // ghost lines opposite to trivial lines are ghost and trivial
2182 if (curDiff.op == OP_TRIVIAL)
2183 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2189 } // switch (curDiff.op)
2190 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2191 } // for (nDiff = nDiffCount; nDiff-- > 0; )
2193 m_diffList.ConstructSignificantChain();
2196 // Note: By this point all `m_ptBuf[]` buffers must have the same
2197 // number of line entries; eventual buffer processing typically
2198 // uses the line count from `m_ptBuf[0]` for all buffer processing.
2200 for (file = 0; file < m_nBuffers; file++)
2202 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2206 for (file = 0; file < m_nBuffers; file++)
2207 m_ptBuf[file]->FinishLoading();
2211 * @brief Checks if file has changed since last update (save or rescan).
2212 * @param [in] szPath File to check
2213 * @param [in] dfi Previous fileinfo of file
2214 * @param [in] bSave If true Compare to last save-info, else to rescan-info
2215 * @param [in] nBuffer Index (0-based) of buffer
2216 * @return true if file is changed.
2218 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2219 bool bSave, int nBuffer)
2221 DiffFileInfo *fileInfo = nullptr;
2222 bool bFileChanged = false;
2223 bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2225 if (bIgnoreSmallDiff)
2226 tolerance = SmallTimeDiff; // From MainFrm.h
2229 fileInfo = m_pSaveFileInfo[nBuffer].get();
2231 fileInfo = m_pRescanFileInfo[nBuffer].get();
2233 // We assume file existed, so disappearing means removal
2234 if (!dfi.Update(szPath))
2235 return FileChange::Removed;
2237 int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2238 if (timeDiff < 0) timeDiff = -timeDiff;
2239 if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2241 bFileChanged = true;
2245 return FileChange::Changed;
2247 return FileChange::NoChange;
2250 void CMergeDoc::HideLines()
2255 if (m_nDiffContext < 0)
2257 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2261 int nLineCount = 0x7fffffff;
2262 for (file = 0; file < m_nBuffers; file++)
2264 if (nLineCount > m_ptBuf[file]->GetLineCount())
2265 nLineCount = m_ptBuf[file]->GetLineCount();
2268 for (nLine = 0; nLine < nLineCount;)
2270 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2271 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2273 for (file = 0; file < m_nBuffers; file++)
2274 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2279 int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2280 for (; nLine2 < nLine; nLine2++)
2282 for (file = 0; file < m_nBuffers; file++)
2283 m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2286 for (; nLine < nLineCount; nLine++)
2288 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2289 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2291 for (file = 0; file < m_nBuffers; file++)
2292 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2295 int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2296 for (; nLine < nLineEnd2; nLine++)
2298 for (file = 0; file < m_nBuffers; file++)
2299 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2300 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2301 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2302 nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2307 ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2311 * @brief Asks and then saves modified files.
2313 * This function saves modified files. Dialog is shown for user to select
2314 * modified file(s) one wants to save or discard changed. Cancelling of
2315 * save operation is allowed unless denied by parameter. After successfully
2316 * save operation file statuses are updated to directory compare.
2317 * @param [in] bAllowCancel If false "Cancel" button is disabled.
2318 * @return true if user selected "OK" so next operation can be
2319 * executed. If false user choosed "Cancel".
2320 * @note If filename is empty, we assume scratchpads are saved,
2321 * so instead of filename, description is shown.
2322 * @todo If we have filename and description for file, what should
2323 * we do after saving to different filename? Empty description?
2324 * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2326 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2328 bool bLModified = false, bMModified = false, bRModified = false;
2330 bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2332 if (m_nBuffers == 3)
2334 bLModified = m_ptBuf[0]->IsModified();
2335 bMModified = m_ptBuf[1]->IsModified();
2336 bRModified = m_ptBuf[2]->IsModified();
2340 bLModified = m_ptBuf[0]->IsModified();
2341 bRModified = m_ptBuf[1]->IsModified();
2343 if (!bLModified && !bMModified && !bRModified)
2347 dlg.DoAskFor(bLModified, bMModified, bRModified);
2349 dlg.m_bDisableCancel = true;
2350 if (!m_filePaths.GetLeft().empty())
2352 if (theApp.m_strSaveAsPath.empty())
2353 dlg.m_sLeftFile = m_filePaths.GetLeft();
2355 dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2358 dlg.m_sLeftFile = m_strDesc[0];
2359 if (m_nBuffers == 3)
2361 if (!m_filePaths.GetMiddle().empty())
2363 if (theApp.m_strSaveAsPath.empty())
2364 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2366 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2369 dlg.m_sMiddleFile = m_strDesc[1];
2371 if (!m_filePaths.GetRight().empty())
2373 if (theApp.m_strSaveAsPath.empty())
2374 dlg.m_sRightFile = m_filePaths.GetRight();
2376 dlg.m_sRightFile = theApp.m_strSaveAsPath;
2379 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2381 if (dlg.DoModal() == IDOK)
2383 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2385 if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2389 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2391 if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2395 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2397 if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2406 // If file were modified and saving was successfull,
2407 // update status on dir view
2408 if ((bLModified && bLSaveSuccess) ||
2409 (bMModified && bMSaveSuccess) ||
2410 (bRModified && bRSaveSuccess))
2412 // If directory compare has results
2413 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2415 if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2416 FlushAndRescan(false);
2418 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2419 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2420 m_nTrivialDiffs, bIdentical);
2427 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2428 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2430 // if we did not rescan during the request timeOut, Rescan
2431 // else we did Rescan after the request, so do nothing
2432 COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2433 if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2434 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2439 * @brief We have two child views (left & right), so we keep pointers directly
2440 * at them (the MFC view list doesn't have them both)
2442 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2445 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2446 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2450 void CMergeDoc::RemoveMergeViews(int nGroup)
2453 for (; nGroup < m_nGroups - 1; nGroup++)
2455 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2457 m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2458 m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2465 * @brief DirDoc gives us its identity just after it creates us
2467 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2469 ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2470 m_pDirDoc = pDirDoc;
2474 * @brief Return pointer to parent frame
2476 CMergeEditFrame * CMergeDoc::GetParentFrame()
2478 return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame());
2482 * @brief DirDoc is closing
2484 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2486 ASSERT(m_pDirDoc == pDirDoc);
2487 m_pDirDoc = nullptr;
2488 // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2492 * @brief DirDoc commanding us to close
2494 bool CMergeDoc::CloseNow()
2496 // Allow user to cancel closing
2497 if (!PromptAndSaveIfNeeded(true))
2500 GetParentFrame()->CloseNow();
2505 * @brief Loads file to buffer and shows load-errors
2506 * @param [in] sFileName File to open
2507 * @param [in] nBuffer Index (0-based) of buffer to load
2508 * @param [out] readOnly whether file is read-only
2509 * @param [in] encoding encoding used
2510 * @return Tells if files were loaded successfully
2511 * @sa CMergeDoc::OpenDocs()
2513 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2516 DWORD retVal = FileLoadResult::FRESULT_ERROR;
2518 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2519 m_filePaths[nBuffer] = sFileName;
2521 CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2523 retVal = pBuf->LoadFromFile(sFileName, m_pInfoUnpacker.get(),
2524 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2526 // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2527 // it left the pBuf in a valid (but empty) state via a call to InitNew
2529 if (FileLoadResult::IsOkImpure(retVal))
2531 // File loaded, and multiple EOL types in this file
2532 FileLoadResult::SetMainOk(retVal);
2534 // If mixed EOLs are not enabled, enable them for this doc.
2535 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2537 pBuf->SetMixedEOL(true);
2541 if (FileLoadResult::IsError(retVal))
2543 // Error from Unifile/system
2544 if (!sOpenError.IsEmpty())
2545 sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2547 sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2548 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2550 else if (FileLoadResult::IsErrorUnpack(retVal))
2552 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2553 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2559 * @brief Check if specified codepage number is valid for WinMerge Editor.
2560 * @param [in] cp Codepage number to check.
2561 * @return true if codepage is valid, false otherwise.
2563 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2565 if (cp == 0) // 0 is our signal value for invalid
2567 return GetEncodingNameFromCodePage(cp) != nullptr;
2571 * @brief Sanity check file's specified codepage.
2572 * This function checks if file's specified codepage is valid for WinMerge
2573 * editor and if not resets the codepage to default.
2574 * @param [in,out] fileinfo Class containing file's codepage.
2576 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2578 if (fileinfo.encoding.m_unicoding == ucr::NONE
2579 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2581 int cp = ucr::getDefaultCodepage();
2582 if (!IsValidCodepageForMergeEditor(cp))
2584 fileinfo.encoding.SetCodepage(cp);
2589 * @brief Loads one file from disk and updates file infos.
2590 * @param [in] index Index of file in internal buffers.
2591 * @param [in] filename File's name.
2592 * @param [in] readOnly Is file read-only?
2593 * @param [in] encoding File's encoding.
2594 * @return One of FileLoadResult values.
2596 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc,
2597 const FileTextEncoding & encoding)
2599 DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2601 m_strDesc[index] = strDesc;
2602 if (!filename.empty())
2604 if (strDesc.empty())
2605 m_nBufferType[index] = BUFFERTYPE::NORMAL;
2607 m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2608 m_pSaveFileInfo[index]->Update(filename);
2609 m_pRescanFileInfo[index]->Update(filename);
2611 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2612 if (FileLoadResult::IsLossy(loadSuccess))
2614 m_ptBuf[index]->FreeAll();
2615 loadSuccess = LoadFile(filename.c_str(), index, readOnly,
2616 GuessCodepageEncoding(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1));
2621 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2622 m_ptBuf[index]->InitNew();
2623 m_ptBuf[index]->m_encoding = encoding;
2624 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer
2625 loadSuccess = FileLoadResult::FRESULT_OK;
2630 void CMergeDoc::SetTableProperties()
2632 struct TableProps { bool istable; TCHAR delimiter; TCHAR quote; bool allowNewlinesInQuotes; };
2633 auto getTablePropsByFileName = [](const String& path, const std::optional<bool>& enableTableEditing)-> TableProps
2635 const TCHAR quote = GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR).c_str()[0];
2636 FileFilterHelper filterCSV, filterTSV, filterDSV;
2637 bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2638 const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2639 if (!csvFilePattern.empty())
2641 filterCSV.UseMask(true);
2642 filterCSV.SetMask(csvFilePattern);
2643 if (filterCSV.includeFile(path))
2644 return { true, ',', quote, allowNewlineIQuotes };
2646 const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2647 if (!tsvFilePattern.empty())
2649 filterTSV.UseMask(true);
2650 filterTSV.SetMask(tsvFilePattern);
2651 if (filterTSV.includeFile(path))
2652 return { true, '\t', quote, allowNewlineIQuotes };
2654 const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2655 if (!dsvFilePattern.empty())
2657 filterDSV.UseMask(true);
2658 filterDSV.SetMask(dsvFilePattern);
2659 if (filterDSV.includeFile(path))
2660 return { true, GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR).c_str()[0], quote };
2662 if (enableTableEditing.value_or(false))
2665 if (dlg.DoModal() == IDOK)
2666 return { true, dlg.m_sDelimiterChar.c_str()[0], dlg.m_sQuoteChar.c_str()[0], dlg.m_bAllowNewlinesInQuotes };
2668 return { false, 0, 0, false };
2671 TableProps tableProps[3] = {};
2672 int nTableFileIndex = -1;
2673 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2676 paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2677 tableProps[nBuffer] = getTablePropsByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2679 tableProps[nBuffer] = tableProps[nBuffer - 1];
2680 if (tableProps[nBuffer].istable)
2681 nTableFileIndex = nBuffer;
2683 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2685 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
2687 int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
2688 m_ptBuf[nBuffer]->SetTableEditing(true);
2689 m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
2690 m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
2691 m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
2692 m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
2693 m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
2697 m_ptBuf[nBuffer]->SetTableEditing(false);
2703 * @brief Loads files and does initial rescan.
2704 * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
2705 * @param bRO [in] Is left/middle/right file read-only
2706 * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
2707 * @todo Options are still read from CMainFrame, this will change
2708 * @sa CMainFrame::ShowMergeDoc()
2710 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
2711 const bool bRO[], const String strDesc[])
2713 IDENTLEVEL identical = IDENTLEVEL::NONE;
2714 int nRescanResult = RESCAN_OK;
2716 FileLocation fileloc[3];
2718 std::copy_n(ifileloc, 3, fileloc);
2720 // Filter out invalid codepages, or editor will display all blank
2721 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2722 SanityCheckCodepage(fileloc[nBuffer]);
2726 curUndo = undoTgt.begin();
2728 // Prevent displaying views during LoadFile
2729 // Note : attach buffer again only if both loads succeed
2730 m_strBothFilenames.erase();
2732 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
2734 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2736 // clear undo buffers
2737 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
2740 m_ptBuf[nBuffer]->FreeAll ();
2742 // build the text being filtered, "|" separates files as it is forbidden in filenames
2743 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
2745 m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
2749 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2751 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
2752 fileloc[nBuffer].encoding);
2755 SetTableProperties();
2757 const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
2759 // scratchpad : we don't call LoadFile, so
2760 // we need to initialize the unpacker as a "do nothing" one
2761 if (bFiltersEnabled)
2763 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
2765 m_pInfoUnpacker->Initialize(PLUGIN_MODE::PLUGIN_MANUAL);
2769 // Bail out if either side failed
2770 if (std::find_if(nSuccess, nSuccess + m_nBuffers, [](DWORD d){return !FileLoadResult::IsOk(d);} ) != nSuccess + m_nBuffers)
2772 CMergeEditFrame *pFrame = GetParentFrame();
2773 if (pFrame != nullptr)
2775 // Use verify macro to trap possible error in debug.
2776 VERIFY(pFrame->DestroyWindow());
2781 // Warn user if file load was lossy (bad encoding)
2783 int nLossyBuffers = 0;
2784 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2786 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
2788 // TODO: It would be nice to report how many lines were lossy
2789 // we did calculate those numbers when we loaded the files, in the text stats
2791 idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
2795 if (nLossyBuffers > 1)
2796 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
2798 if (nLossyBuffers > 0)
2800 if (m_pEncodingErrorBar == nullptr)
2802 m_pEncodingErrorBar.reset(new CEncodingErrorBar());
2803 m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
2805 m_pEncodingErrorBar->SetText(LoadResString(idres));
2806 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
2809 ForEachView([](auto& pView) {
2810 // Now buffers data are valid
2811 pView->AttachToBuffer();
2812 // Currently there is only one set of syntax colors, which all documents & views share
2813 pView->SetColorContext(theApp.GetMainSyntaxColors());
2814 // Currently there is only one set of markers, which all documents & views share
2815 pView->SetMarkersContext(theApp.GetMainMarkers());
2817 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2819 // Set read-only statuses
2820 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
2823 // Check the EOL sensitivity option (do it before Rescan)
2824 DIFFOPTIONS diffOptions = {0};
2825 m_diffWrapper.GetOptions(&diffOptions);
2826 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
2828 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2829 if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
2832 if (nBuffer < m_nBuffers)
2834 // Options and files not are not compatible :
2835 // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
2836 // All lines will differ, that is not very interesting and probably not wanted.
2837 // Propose to turn off the option 'sensitive to EOL'
2838 String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
2839 if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
2841 diffOptions.bIgnoreEol = true;
2842 m_diffWrapper.SetOptions(&diffOptions);
2844 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
2845 const int nFormerResult = dlg.GetFormerResult();
2846 if (nFormerResult != -1)
2848 // "Don't ask this question again" checkbox is checked
2849 GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
2855 // Define the prediffer
2856 PackingInfo * infoUnpacker = nullptr;
2857 PrediffingInfo * infoPrediffer = nullptr;
2858 if (bFiltersEnabled && m_pDirDoc != nullptr)
2860 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
2861 m_diffWrapper.SetPrediffer(infoPrediffer);
2862 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
2865 bool bBinary = false;
2866 nRescanResult = Rescan(bBinary, identical);
2868 // Open filed if rescan succeed and files are not binaries
2869 if (nRescanResult == RESCAN_OK)
2871 // set the document types
2872 // Warning : it is the first thing to do (must be done before UpdateView,
2873 // or any function that calls UpdateView, like SelectDiff)
2874 // Note: If option enabled, and another side type is not recognized,
2875 // we use recognized type for unrecognized side too.
2880 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2882 if (bFiltersEnabled && m_pInfoUnpacker->m_textType.length())
2883 sext[nBuffer] = m_pInfoUnpacker->m_textType;
2885 sext[nBuffer] = GetFileExt(fileloc[nBuffer].filepath.c_str(), m_strDesc[nBuffer].c_str());
2886 ForEachView(nBuffer, [&](auto& pView) {
2887 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
2888 if (bTyped[nBuffer])
2889 paneTyped = nBuffer;
2893 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2895 if (bTyped[0] != bTyped[nBuffer])
2899 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
2900 if (syntaxHLEnabled && nBuffer < m_nBuffers)
2902 if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
2905 m_ptBuf[0]->GetLine(0, sFirstLine);
2906 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2908 bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
2913 if (syntaxHLEnabled)
2915 CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
2916 ForEachView([&bTyped, enuType](auto& pView) {
2917 if (!bTyped[pView->m_nThisPane])
2918 pView->SetTextType(enuType);
2922 int nNormalBuffer = 0;
2923 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2925 // set the frame window header
2926 UpdateHeaderPath(nBuffer);
2928 ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
2930 if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
2931 (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
2938 // Inform user that files are identical
2939 // Don't show message if new buffers created
2940 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
2942 ShowRescanError(nRescanResult, identical);
2945 // Exit if files are identical should only work for the first
2946 // comparison and must be disabled afterward.
2947 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
2951 // CMergeDoc::Rescan fails if files do not exist on both sides
2952 // or the really arcane case that the temp files couldn't be created,
2953 // which is too obscure to bother reporting if you can't write to
2954 // your temp directory, doing nothing is graceful enough for that).
2955 ShowRescanError(nRescanResult, identical);
2956 GetParentFrame()->DestroyWindow();
2960 // Force repaint of location pane to update it in case we had some warning
2961 // dialog visible and it got painted before files were loaded
2962 if (m_pView[0][0] != nullptr)
2963 m_pView[0][0]->RepaintLocationPane();
2968 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex)
2972 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
2973 if (nPane < 0 || nPane >= m_nBuffers)
2976 if (nLineIndex == -1)
2978 // scroll to first diff
2979 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
2980 m_diffList.HasSignificantDiffs())
2982 int nDiff = m_diffList.FirstSignificantDiff();
2984 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
2985 m_pView[0][nPane]->SetActivePane();
2989 m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, false, nPane);
2992 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
2994 if (!PromptAndSaveIfNeeded(true))
2997 FileLocation fileloc[3];
3000 for (int pane = 0; pane < m_nBuffers; pane++)
3002 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3003 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3004 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3005 fileloc[pane].setPath(m_filePaths[pane]);
3007 std::copy_n(m_strDesc, m_nBuffers, strDesc);
3009 strDesc[nBuffer] = _T("");
3010 fileloc[nBuffer].setPath(path);
3011 fileloc[nBuffer].encoding = GuessCodepageEncoding(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3013 if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3014 MoveOnLoad(nBuffer, nLineIndex);
3018 * @brief Re-load a document.
3019 * This methods re-loads the file compare document. The re-loaded document is
3020 * one side of the file compare.
3021 * @param [in] index The document to re-load.
3022 * @return Open result code.
3024 void CMergeDoc::RefreshOptions()
3026 DIFFOPTIONS options = {0};
3028 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3029 Options::DiffOptions::Load(GetOptionsMgr(), options);
3031 m_diffWrapper.SetOptions(&options);
3033 // Refresh view options
3034 ForEachView([](auto& pView) { pView->RefreshOptions(); });
3038 * @brief Write path and filename to headerbar
3039 * @note SetText() does not repaint unchanged text
3041 void CMergeDoc::UpdateHeaderPath(int pane)
3043 CMergeEditFrame *pf = GetParentFrame();
3044 ASSERT(pf != nullptr);
3046 bool bChanges = false;
3048 if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3049 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3051 sText = m_strDesc[pane];
3055 sText = m_filePaths[pane];
3056 if (m_pDirDoc != nullptr)
3058 m_pDirDoc->ApplyDisplayRoot(pane, sText);
3061 bChanges = m_ptBuf[pane]->IsModified();
3064 sText.insert(0, _T("* "));
3066 pf->GetHeaderInterface()->SetText(pane, sText);
3072 * @brief Paint differently the headerbar of the active view
3074 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3076 CMergeEditFrame *pf = GetParentFrame();
3077 ASSERT(pf != nullptr);
3078 pf->GetHeaderInterface()->SetActive(pane, bActivate);
3082 * @brief Set detect/not detect Moved Blocks
3084 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3086 if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3089 GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3090 m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3095 * @brief Check if given buffer has mixed EOL style.
3096 * @param [in] nBuffer Buffer to check.
3097 * @return true if buffer's EOL style is mixed, false otherwise.
3099 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3101 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3102 return pBuf->IsMixedEOL();
3105 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3107 m_bEditAfterRescan[nBuffer] = true;
3110 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3112 if (nBuffer >= 0 && nBuffer < m_nBuffers)
3113 return m_bEditAfterRescan[nBuffer];
3115 for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3117 if (m_bEditAfterRescan[nBuffer])
3125 * @brief Update document filenames to title
3127 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3130 String sFileName[3];
3132 if (lpszTitle != nullptr)
3136 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3137 sFileName[nBuffer] = !m_strDesc[nBuffer].empty() ? m_strDesc[nBuffer] : paths::FindFileName(m_filePaths[nBuffer]);
3138 if (std::count(&sFileName[0], &sFileName[0] + m_nBuffers, sFileName[0]) == m_nBuffers)
3139 sTitle = sFileName[0] + strutils::format(_T(" x %d"), m_nBuffers);
3141 sTitle = strutils::join(&sFileName[0], &sFileName[0] + m_nBuffers, _T(" - "));
3143 CDocument::SetTitle(sTitle.c_str());
3147 * @brief Update any resources necessary after a GUI language change
3149 void CMergeDoc::UpdateResources()
3151 if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3152 m_strDesc[0] = _("Untitled left");
3153 if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3154 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3155 if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3156 m_strDesc[1] = _("Untitled middle");
3157 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3158 UpdateHeaderPath(nBuffer);
3160 GetParentFrame()->UpdateResources();
3161 ForEachView([](auto& pView) { pView->UpdateResources(); });
3164 // Return current word breaking break type setting (whitespace only or include punctuation)
3165 bool CMergeDoc::GetBreakType() const
3167 bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3171 // Return true to do line diff colors at the byte level (false to do them at word level)
3172 bool CMergeDoc::GetByteColoringOption() const
3174 // color at byte level if 'break_on_words' option not set
3175 bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3179 /// Swap files and update views
3180 void CMergeDoc::SwapFiles()
3183 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3185 int nLeftViewId = m_pView[nGroup][0]->GetDlgCtrlID();
3186 int nRightViewId = m_pView[nGroup][m_nBuffers - 1]->GetDlgCtrlID();
3187 m_pView[nGroup][0]->SetDlgCtrlID(nRightViewId);
3188 m_pView[nGroup][m_nBuffers - 1]->SetDlgCtrlID(nLeftViewId);
3192 // Swap buffers and so on
3193 std::swap(m_ptBuf[0], m_ptBuf[m_nBuffers - 1]);
3194 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3195 std::swap(m_pView[nGroup][0], m_pView[nGroup][m_nBuffers - 1]);
3196 std::swap(m_pSaveFileInfo[0], m_pSaveFileInfo[m_nBuffers - 1]);
3197 std::swap(m_pRescanFileInfo[0], m_pRescanFileInfo[m_nBuffers - 1]);
3198 std::swap(m_nBufferType[0], m_nBufferType[m_nBuffers - 1]);
3199 std::swap(m_bEditAfterRescan[0], m_bEditAfterRescan[m_nBuffers - 1]);
3200 std::swap(m_strDesc[0], m_strDesc[m_nBuffers - 1]);
3203 m_diffList.Swap(0, m_nBuffers - 1);
3204 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3205 swap(m_pView[nGroup][0]->m_piMergeEditStatus, m_pView[nGroup][m_nBuffers - 1]->m_piMergeEditStatus);
3207 ClearWordDiffCache();
3209 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3211 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3212 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3213 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3216 UpdateHeaderPath(nBuffer);
3218 GetParentFrame()->UpdateSplitter();
3219 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3221 UpdateAllViews(nullptr);
3225 * @brief Display unpacker dialog to user & handle user's choices
3227 bool CMergeDoc::OpenWithUnpackerDialog()
3229 // let the user choose a handler
3230 CSelectUnpackerDlg dlg(m_filePaths[0], nullptr);
3231 // create now a new infoUnpacker to initialize the manual/automatic flag
3232 PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_AUTO);
3233 dlg.SetInitialInfoHandler(&infoUnpacker);
3235 if (dlg.DoModal() == IDOK)
3237 infoUnpacker = dlg.GetInfoHandler();
3238 Merge7zFormatMergePluginScope scope(&infoUnpacker);
3239 if (HasZipSupport() && std::count_if(m_filePaths.begin(), m_filePaths.end(), ArchiveGuessFormat) == m_nBuffers)
3241 DWORD dwFlags[3] = {FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU};
3242 GetMainFrame()->DoFileOpen(&m_filePaths, dwFlags, m_strDesc, _T(""),
3243 GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, _T(""), &infoUnpacker);
3248 SetUnpacker(&infoUnpacker);
3260 * @brief Reloads the opened files
3262 void CMergeDoc::OnFileReload()
3264 if (!PromptAndSaveIfNeeded(true))
3267 FileLocation fileloc[3];
3269 for (int pane = 0; pane < m_nBuffers; pane++)
3271 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3272 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3273 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3274 fileloc[pane].setPath(m_filePaths[pane]);
3276 CPoint pt = GetActiveMergeView()->GetCursorPos();
3277 if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3278 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3282 * @brief Display encodings to user
3284 void CMergeDoc::OnFileEncoding()
3286 DoFileEncodingDialog();
3289 void CMergeDoc::OnCtxtOpenWithUnpacker()
3291 OpenWithUnpackerDialog();
3294 void CMergeDoc::OnBnClickedFileEncoding()
3296 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3298 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3299 DoFileEncodingDialog();
3302 void CMergeDoc::OnBnClickedPlugin()
3304 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3306 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3307 OpenWithUnpackerDialog();
3310 void CMergeDoc::OnBnClickedHexView()
3312 OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3315 void CMergeDoc::OnOK()
3317 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3319 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3322 void CMergeDoc::OnFileRecompareAsText()
3324 m_bEnableTableEditing = false;
3325 PackingInfo infoUnpacker;
3326 SetUnpacker(&infoUnpacker);
3330 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3332 pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode == PLUGIN_MODE::PLUGIN_BUILTIN_XML ||
3333 m_ptBuf[0]->GetTableEditing());
3336 void CMergeDoc::OnFileRecompareAsTable()
3338 m_bEnableTableEditing = true;
3339 PackingInfo infoUnpacker;
3340 SetUnpacker(&infoUnpacker);
3344 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3346 pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3349 void CMergeDoc::OnFileRecompareAsXML()
3351 m_bEnableTableEditing = false;
3352 PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_BUILTIN_XML);
3353 SetUnpacker(&infoUnpacker);
3357 void CMergeDoc::OnUpdateFileRecompareAsXML(CCmdUI *pCmdUI)
3359 pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode != PLUGIN_MODE::PLUGIN_BUILTIN_XML);
3362 void CMergeDoc::OnFileRecompareAs(UINT nID)
3364 DWORD dwFlags[3] = { 0 };
3365 FileLocation fileloc[3];
3366 for (int pane = 0; pane < m_nBuffers; pane++)
3368 fileloc[pane].setPath(m_filePaths[pane]);
3369 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3371 if (m_pEncodingErrorBar!=nullptr && m_pEncodingErrorBar->IsWindowVisible())
3372 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3373 if (nID == ID_MERGE_COMPARE_HEX)
3374 GetMainFrame()->ShowHexMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3376 GetMainFrame()->ShowImgMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3377 GetParentFrame()->ShowWindow(SW_RESTORE);
3378 GetParentFrame()->DestroyWindow();
3381 // Return file extension either from file name
3382 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3385 paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3390 * @brief Generate report from file compare results.
3392 bool CMergeDoc::GenerateReport(const String& sFileName) const
3394 // calculate HTML font size
3397 dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3398 m_pView[0][0]->GetFont(lf);
3399 int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3401 // create HTML report
3403 if (!file.Open(sFileName, _T("wt")))
3405 String errMsg = GetSysError(GetLastError());
3406 String msg = strutils::format_string1(
3407 _("Error creating the report:\n%1"), errMsg);
3408 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3412 file.SetCodepage(ucr::CP_UTF_8);
3414 CString headerText =
3415 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3416 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3419 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3420 _T("<title>WinMerge File Compare Report</title>\n")
3421 _T("<style type=\"text/css\">\n")
3423 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3424 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3425 _T("tr { vertical-align: top; }\n")
3426 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3432 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3436 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3437 file.WriteString(header);
3440 // If archive, use archive path + folder + filename inside archive
3441 // If desc text given, use it
3442 PathContext paths = m_filePaths;
3443 if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3445 for (int i = 0; i < paths.GetSize(); i++)
3446 m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3450 for (int i = 0; i < paths.GetSize(); i++)
3452 if (!m_strDesc[i].empty())
3453 paths[i] = m_strDesc[i];
3459 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3461 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3462 (double)100 / m_nBuffers);
3463 file.WriteString(data);
3464 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3465 file.WriteString(_T("</th>\n"));
3472 // write the body of the report
3474 int nLineCount[3] = {0};
3476 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3477 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3481 file.WriteString(_T("<tr>\n"));
3482 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3484 for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3486 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3490 if (idx[nBuffer] < nLineCount[nBuffer])
3493 int iVisibleLineNumber = 0;
3494 String tdtag = _T("<td class=\"ln\">");
3495 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3496 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3498 iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3501 (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3502 (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3505 if (iVisibleLineNumber > 0)
3507 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3508 iVisibleLineNumber = 0;
3511 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3513 if (iVisibleLineNumber > 0)
3514 tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3516 tdtag += _T("</td>");
3517 file.WriteString(tdtag);
3519 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3523 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3524 file.WriteString(_T("\n"));
3526 file.WriteString(_T("</tr>\n"));
3528 bool bBorderLine = false;
3529 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3531 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3537 file.WriteString(_T("<tr height=1>"));
3538 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3540 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3541 file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3543 file.WriteString(_T("<td></td><td></td>"));
3545 file.WriteString(_T("</tr>\n"));
3548 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3563 * @brief Generate report from file compare results.
3565 void CMergeDoc::OnToolsGenerateReport()
3570 if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3573 if (GenerateReport(s.c_str()))
3574 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3578 * @brief Generate patch from files selected.
3580 * Creates a patch from selected files in active directory compare, or
3581 * active file compare. Files in file compare must be saved before
3584 void CMergeDoc::OnToolsGeneratePatch()
3586 // If there are changes in files, tell user to save them first
3589 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3594 patcher.AddFiles(m_filePaths.GetLeft(),
3595 m_filePaths.GetRight());
3596 patcher.CreatePatch();
3600 * @brief Add synchronization point
3602 void CMergeDoc::AddSyncPoint()
3605 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3607 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3608 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3611 // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
3612 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3613 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
3615 LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
3619 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3620 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3621 DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3623 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3624 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3626 m_bHasSyncPoints = true;
3628 ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3630 FlushAndRescan(true);
3634 * @brief Delete a synchronization point
3636 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3638 const auto syncpoints = GetSyncPointList();
3639 for (auto syncpnt : syncpoints)
3641 if (syncpnt[pane] == nLine)
3643 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3644 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3648 if (syncpoints.size() == 1)
3649 m_bHasSyncPoints = false;
3652 FlushAndRescan(true);
3657 * @brief Clear Synchronization points
3659 void CMergeDoc::ClearSyncPoints()
3661 if (!m_bHasSyncPoints)
3664 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3666 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3667 for (int nLine = 0; nLine < nLineCount; ++nLine)
3669 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3670 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3674 m_bHasSyncPoints = false;
3676 FlushAndRescan(true);
3679 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3681 std::vector<std::vector<int> > list;
3682 if (!m_bHasSyncPoints)
3684 int idx[3] = {-1, -1, -1};
3685 std::vector<int> points(m_nBuffers);
3686 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3687 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3688 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3690 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3691 for (int nLine = 0; nLine < nLineCount; ++nLine)
3693 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3696 if (static_cast<int>(list.size()) <= idx[nBuffer])
3697 list.push_back(points);
3698 list[idx[nBuffer]][nBuffer] = nLine;