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"
42 #include "SubstitutionFiltersList.h"
44 #include "codepage_detect.h"
45 #include "SelectPluginDlg.h"
46 #include "EncodingErrorBar.h"
47 #include "MergeCmdLineInfo.h"
49 #include "Constants.h"
50 #include "Merge7zFormatMergePluginImpl.h"
52 #include "PatchTool.h"
55 #include "stringdiffs.h"
63 int CMergeDoc::m_nBuffersTemp = 2;
65 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
67 /////////////////////////////////////////////////////////////////////////////
70 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
72 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
73 //{{AFX_MSG_MAP(CMergeDoc)
74 ON_COMMAND(ID_FILE_SAVE, OnFileSave)
75 ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
76 ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
77 ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
78 ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
79 ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
80 ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
81 ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
82 ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
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_OPEN_WITH_UNPACKER, OnOpenWithUnpacker)
90 ON_COMMAND(ID_APPLY_PREDIFFER, OnApplyPrediffer)
91 ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
92 ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
93 ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
94 ON_COMMAND(IDOK, OnOK)
95 ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
96 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
97 ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
98 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
99 ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
100 ON_COMMAND_RANGE(ID_UNPACKERS_FIRST, ID_UNPACKERS_LAST, OnFileRecompareAs)
101 ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
105 /////////////////////////////////////////////////////////////////////////////
106 // CMergeDoc construction/destruction
109 * @brief Constructor.
111 CMergeDoc::CMergeDoc()
112 : m_bEnableRescan(true)
114 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
117 , m_pEncodingErrorBar(nullptr)
118 , m_bHasSyncPoints(false)
119 , m_bAutoMerged(false)
122 , m_bAutomaticRescan(false)
124 DIFFOPTIONS options = {0};
126 m_nBuffers = m_nBuffersTemp;
127 m_filePaths.SetSize(m_nBuffers);
129 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
131 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
132 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
133 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
134 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
135 m_bEditAfterRescan[nBuffer] = false;
138 m_bEnableRescan = true;
139 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
141 // COleDateTime m_LastRescan
142 curUndo = undoTgt.begin();
143 m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
144 m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
146 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
147 Options::DiffOptions::Load(GetOptionsMgr(), options);
149 m_diffWrapper.SetOptions(&options);
150 m_diffWrapper.SetPrediffer(nullptr);
156 * Informs associated dirdoc that mergedoc is closing.
158 CMergeDoc::~CMergeDoc()
160 if (m_pDirDoc != nullptr)
162 m_pDirDoc->MergeDocClosing(this);
168 * @brief Deleted data associated with doc before closing.
170 void CMergeDoc::DeleteContents ()
172 CDocument::DeleteContents ();
173 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
175 m_ptBuf[nBuffer]->FreeAll ();
176 m_tempFiles[nBuffer].Delete();
181 * @brief Called when new document is created.
183 * Initialises buffers.
185 BOOL CMergeDoc::OnNewDocument()
187 if (!CDocument::OnNewDocument())
190 SetTitle(_("File Comparison").c_str());
192 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
193 m_ptBuf[nBuffer]->InitNew ();
198 * @brief Return active merge edit view (or left one if neither active)
200 CMergeEditView * CMergeDoc::GetActiveMergeView()
202 CView * pActiveView = GetParentFrame()->GetActiveView();
203 CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
204 if (pMergeEditView == nullptr)
205 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
206 return pMergeEditView;
209 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
211 return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
214 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
216 if (infoNewHandler != nullptr)
218 m_infoUnpacker = *infoNewHandler;
222 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
224 m_diffWrapper.SetPrediffer(infoPrediffer);
227 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
229 m_diffWrapper.GetPrediffer(infoPrediffer);
232 const PrediffingInfo* CMergeDoc::GetPrediffer() const
234 static PrediffingInfo infoPrediffer;
235 m_diffWrapper.GetPrediffer(&infoPrediffer);
236 return &infoPrediffer;
239 /////////////////////////////////////////////////////////////////////////////
240 // CMergeDoc serialization
242 void CMergeDoc::Serialize(CArchive& ar)
244 ASSERT(false); // we do not use CDocument serialization
247 /////////////////////////////////////////////////////////////////////////////
248 // CMergeDoc commands
251 * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
254 * original file is Ansi :
255 * buffer -> save as Ansi -> Ansi plugins -> diffutils
256 * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
257 * buffer -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
258 * (the plugins are optional, not the conversion)
259 * @todo Show SaveToFile() errors?
261 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
263 // and we don't repack the file
264 PackingInfo tempPacker(false);
266 // write buffer out to temporary file
268 int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
269 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
273 * @brief Save files to temp files & compare again.
275 * @param bBinary [in,out] [in] If true, compare two binary files
276 * [out] If true binary file was detected.
277 * @param bIdentical [out] If true files were identical
278 * @param bForced [in] If true, suppressing is ignored and rescan
280 * @return Tells if rescan was successfully, was suppressed, or
282 * If this code is OK, Rescan has detached the views temporarily
283 * (positions of cursors have been lost)
284 * @note Rescan() ALWAYS compares temp files. Actual user files are not
285 * touched by Rescan().
286 * @sa CDiffWrapper::RunFileDiff()
288 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
289 bool bForced /* =false */)
291 DIFFOPTIONS diffOptions = {0};
292 DiffFileInfo fileInfo;
293 bool diffSuccess = false;
294 int nResult = RESCAN_OK;
295 FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
300 if (!m_bEnableRescan)
301 return RESCAN_SUPPRESSED;
304 ClearWordDiffCache();
306 if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
308 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
312 m_diffWrapper.SetFilterList(_T(""));
315 if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
317 m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
321 m_diffWrapper.SetSubstitutionList(nullptr);
324 if (GetView(0, 0)->m_CurSourceDef->type != 0)
325 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
327 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
329 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
331 // Check if files have been modified since last rescan
332 // Ignore checking in case of scratchpads (empty filenames)
333 if (!m_filePaths[nBuffer].empty())
335 Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
336 fileInfo, false, nBuffer);
339 m_LastRescan = COleDateTime::GetCurrentTime();
341 LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
342 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
344 if (Changed[nBuffer] == FileChange::Removed)
346 String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
347 ShowMessageBox(msg, MB_ICONWARNING);
348 bool bSaveResult = false;
349 bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
350 if (!ok || !bSaveResult)
352 return RESCAN_FILE_ERR;
356 String temp = m_tempFiles[nBuffer].GetPath();
358 temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
360 return RESCAN_TEMP_ERR;
365 String tempPath = env::GetTemporaryPath();
367 // Set up DiffWrapper
368 m_diffWrapper.GetOptions(&diffOptions);
373 m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
374 // Clear moved lines lists
375 if (m_diffWrapper.GetDetectMovedBlocks())
377 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
378 m_diffWrapper.GetMovedLines(nBuffer)->Clear();
381 // Set paths for diffing and run diff
382 m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
384 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
386 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
387 m_diffWrapper.SetCompareFiles(m_filePaths);
391 if (!HasSyncPoints())
393 // Save text buffer to file
394 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
396 m_ptBuf[nBuffer]->SetTempPath(tempPath);
397 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
400 m_diffWrapper.SetCreateDiffList(&m_diffList);
401 diffSuccess = m_diffWrapper.RunFileDiff();
404 m_diffWrapper.GetDiffStatus(&status);
405 if (bBinary) // believe caller if we were told these are binaries
406 status.bBinaries = true;
410 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();
411 int nStartLine[3] = {0};
412 int nLines[3], nRealLine[3];
413 for (size_t i = 0; i <= syncpoints.size(); ++i)
415 // Save text buffer to file
416 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
418 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
419 m_ptBuf[nBuffer]->SetTempPath(tempPath);
420 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(),
421 nStartLine[nBuffer], nLines[nBuffer]);
425 m_diffWrapper.SetCreateDiffList(&templist);
426 diffSuccess = m_diffWrapper.RunFileDiff();
427 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
428 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
430 // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
431 if (i == 0 && templist.GetSize() > 0)
432 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
433 if (nStartLine[nBuffer] == 0)
435 bool isEmptyFile = true;
436 for (int j = 0; j < nLines[nBuffer]; j++)
438 if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
447 templist.GetDiff(0, di);
448 if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
450 di.end[nBuffer] = -1;
451 templist.SetDiff(0, di);
456 m_diffList.AppendDiffList(templist, nRealLine);
457 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
458 nStartLine[nBuffer] += nLines[nBuffer];
461 DIFFSTATUS status_part;
462 m_diffWrapper.GetDiffStatus(&status_part);
463 if (bBinary) // believe caller if we were told these are binaries
464 status.bBinaries = true;
465 status.MergeStatus(status_part);
467 m_diffWrapper.SetCreateDiffList(&m_diffList);
470 // If one file has EOL before EOF and other not...
471 if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
473 // ..last DIFFRANGE of file which has EOL must be
474 // fixed to contain last line too
475 int lineCount[3] = { 0,0,0 };
476 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
477 lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
478 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
481 // set identical/diff result as recorded by diffutils
482 identical = status.Identical;
484 // Determine errors and binary file compares
486 nResult = RESCAN_FILE_ERR;
487 else if (status.bBinaries)
493 // Now update views and buffers for ghost lines
495 // Prevent displaying views during this update
496 // BTW, this solves the problem of double asserts
497 // (during the display of an assert message box, a second assert in one of the
498 // display functions happens, and hides the first assert)
499 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
501 // Remove blank lines and clear winmerge flags
502 // this operation does not change the modified flag
503 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
504 m_ptBuf[nBuffer]->prepareForRescan();
506 // Divide diff blocks to match lines.
507 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
510 // Analyse diff-list (updating real line-numbers)
511 // this operation does not change the modified flag
514 // Hide identical lines if diff-context is not 'All'
517 // Apply flags to lines that are trivial
518 PrediffingInfo infoPrediffer;
519 GetPrediffer(&infoPrediffer);
520 if (!infoPrediffer.GetPluginPipeline().empty())
523 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
524 if (m_diffWrapper.GetDetectMovedBlocks())
527 // After PrimeTextBuffers() we know amount of real diffs
528 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
530 // Identical files are also updated
531 if (!m_diffList.HasSignificantDiffs())
532 identical = IDENTLEVEL::ALL;
534 ForEachView([](auto& pView) {
535 // just apply some options to the views
536 pView->PrimeListWithFile();
537 // Now buffers data are valid
538 pView->ReAttachToBuffer();
540 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
542 m_bEditAfterRescan[nBuffer] = false;
546 if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
547 identical == IDENTLEVEL::ALL &&
548 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
549 [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
550 identical = IDENTLEVEL::NONE;
552 GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
557 void CMergeDoc::CheckFileChanged(void)
560 DiffFileInfo fileInfo;
561 FileChange FileChange[3];
563 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
565 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
568 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
571 bool bDoReload = false;
572 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
574 if (FileChange[nBuffer] == FileChange::Changed)
576 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]);
577 if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
584 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
586 if (FileChange[nBuffer] == FileChange::Changed)
588 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
589 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
595 /** @brief Apply flags to lines that are trivial */
596 void CMergeDoc::FlagTrivialLines(void)
598 for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
600 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
603 for (int file = 0; file < m_nBuffers; ++file)
605 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
606 str[file] = p ? p : _T("");
609 if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
611 DIFFOPTIONS diffOptions = {0};
612 m_diffWrapper.GetOptions(&diffOptions);
614 // Make the call to stringdiffs, which does all the hard & tedious computations
615 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
616 !diffOptions.bIgnoreCase,
617 !diffOptions.bIgnoreEol,
618 diffOptions.nIgnoreWhitespace,
619 GetBreakType(), // whitespace only or include punctuation
620 GetByteColoringOption());
621 if (!worddiffs.empty())
623 for (int file = 0; file < m_nBuffers; ++file)
624 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
631 /** @brief Adjust all different lines that were detected as actually matching moved lines */
632 void CMergeDoc::FlagMovedLines(void)
635 MovedLines *pMovedLines;
637 pMovedLines = m_diffWrapper.GetMovedLines(0);
638 for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
640 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
643 TRACE(_T("%d->%d\n"), i, j);
645 // We only flag lines that are already marked as being different
646 int apparent = m_ptBuf[0]->ComputeApparentLine(i);
647 if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
649 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
650 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
652 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
653 if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
654 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
660 pMovedLines = m_diffWrapper.GetMovedLines(1);
661 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
663 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
666 TRACE(_T("%d->%d\n"), i, j);
668 // We only flag lines that are already marked as being different
669 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
670 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
672 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
673 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
675 int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
676 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
677 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
686 pMovedLines = m_diffWrapper.GetMovedLines(1);
687 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
689 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
692 TRACE(_T("%d->%d\n"), i, j);
694 // We only flag lines that are already marked as being different
695 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
696 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
698 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
699 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
701 int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
702 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
703 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
709 pMovedLines = m_diffWrapper.GetMovedLines(2);
710 for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
712 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
715 TRACE(_T("%d->%d\n"), i, j);
717 // We only flag lines that are already marked as being different
718 int apparent = m_ptBuf[2]->ComputeApparentLine(i);
719 if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
721 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
722 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
724 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
725 if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
726 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
732 // todo: Need to record actual moved information
735 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
737 if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
739 GetParentFrame()->InitialUpdateFrame(this, true);
740 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
742 return AfxMessageBox(sText.c_str(), nType, nIDHelp);
746 * @brief Prints (error) message by rescan status.
748 * @param nRescanResult [in] Resultcocode from rescan().
749 * @param bIdentical [in] Were files identical?.
750 * @sa CMergeDoc::Rescan()
752 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
754 // Rescan was suppressed, there is no sensible status
755 if (nRescanResult == RESCAN_SUPPRESSED)
760 if (nRescanResult == RESCAN_FILE_ERR)
762 s = _("An error occurred while comparing the files.");
764 ShowMessageBox(s, MB_ICONSTOP);
768 if (nRescanResult == RESCAN_TEMP_ERR)
770 s = _("Temporary files could not be created. Check your temporary path settings.");
772 ShowMessageBox(s, MB_ICONSTOP);
776 // Files are not binaries, but they are identical
777 if (identical != IDENTLEVEL::NONE)
779 CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
780 [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
784 bool CMergeDoc::Undo()
790 * @brief An instance of RescanSuppress prevents rescan during its lifetime
791 * (or until its Clear method is called, which ends its effect).
796 explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
799 m_bPrev = doc.m_bEnableRescan;
800 m_doc.m_bEnableRescan = false;
807 m_doc.m_bEnableRescan = m_bPrev;
821 * @brief Copy all diffs from one side to side.
822 * @param [in] srcPane Source side from which diff is copied
823 * @param [in] dstPane Destination side
825 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
827 CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
831 * @brief Copy range of diffs from one side to side.
832 * This function copies given range of differences from side to another.
833 * Ignored differences are skipped, and not copied.
834 * @param [in] srcPane Source side from which diff is copied
835 * @param [in] dstPane Destination side
836 * @param [in] firstDiff First diff copied (0-based index)
837 * @param [in] lastDiff Last diff copied (0-based index)
839 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
842 if (firstDiff > lastDiff)
843 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
845 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
846 if (lastDiff > m_diffList.GetSize() - 1)
847 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
850 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
851 firstDiff = max(0, firstDiff);
852 if (firstDiff > lastDiff)
855 RescanSuppress suppressRescan(*this);
857 // Note we don't care about m_nDiffs count to become zero,
858 // because we don't rescan() so it does not change
860 SetCurrentDiff(lastDiff);
862 bool bGroupWithPrevious = false;
863 if (firstWordDiff <= 0 && lastWordDiff == -1)
865 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
866 return; // sync failure
870 if (!WordListCopy(srcPane, dstPane, lastDiff,
871 (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
872 return; // sync failure
875 SetEditedAfterRescan(dstPane);
877 int nGroup = GetActiveMergeView()->m_nThisGroup;
878 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
879 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
880 CPoint currentPosSrc = pViewSrc->GetCursorPos();
882 CPoint currentPosDst = pViewDst->GetCursorPos();
886 pViewDst->SetCursorPos(pt);
887 pViewDst->SetNewSelection(pt, pt, false);
888 pViewDst->SetNewAnchor(pt);
890 // copy from bottom up is more efficient
891 for (int i = lastDiff - 1; i >= firstDiff; --i)
893 if (m_diffList.IsDiffSignificant(i))
896 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
897 if (currentPosDst.y > pdi->dend)
899 if (pdi->blank[dstPane] >= 0)
900 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
901 else if (pdi->blank[srcPane] >= 0)
902 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
904 // Group merge with previous (merge undo data to one action)
905 bGroupWithPrevious = true;
906 if (i > firstDiff || firstWordDiff <= 0)
908 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
909 break; // sync failure
913 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
914 break; // sync failure
919 ForEachView(dstPane, [currentPosDst](auto& pView) {
920 pView->SetCursorPos(currentPosDst);
921 pView->SetNewSelection(currentPosDst, currentPosDst, false);
922 pView->SetNewAnchor(currentPosDst);
925 suppressRescan.Clear(); // done suppress Rescan
929 void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
930 int firstLineDiff, int lastLineDiff)
932 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
933 firstDiff = max(0, firstDiff);
934 if (firstDiff > lastDiff)
937 RescanSuppress suppressRescan(*this);
939 bool bGroupWithPrevious = false;
940 if (firstLineDiff <= 0 && lastLineDiff == -1)
942 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
943 return; // sync failure
947 if (!PartialListCopy(srcPane, dstPane, lastDiff,
948 (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
949 return; // sync failure
953 SetEditedAfterRescan(dstPane);
955 int nGroup = GetActiveMergeView()->m_nThisGroup;
956 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
957 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
958 CPoint currentPosSrc = pViewSrc->GetCursorPos();
960 CPoint currentPosDst = pViewDst->GetCursorPos();
964 pViewDst->SetCursorPos(pt);
965 pViewDst->SetNewSelection(pt, pt, false);
966 pViewDst->SetNewAnchor(pt);
968 // copy from bottom up is more efficient
969 for (int i = lastDiff - 1; i >= firstDiff; --i)
971 if (m_diffList.IsDiffSignificant(i))
974 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
975 if (currentPosDst.y > pdi->dend)
977 if (pdi->blank[dstPane] >= 0)
978 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
979 else if (pdi->blank[srcPane] >= 0)
980 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
982 // Group merge with previous (merge undo data to one action)
983 bGroupWithPrevious = true;
984 if (i > firstDiff || firstLineDiff <= 0)
986 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
987 break; // sync failure
991 if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
992 break; // sync failure
997 ForEachView(dstPane, [currentPosDst](auto& pView) {
998 pView->SetCursorPos(currentPosDst);
999 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1000 pView->SetNewAnchor(currentPosDst);
1003 suppressRescan.Clear(); // done suppress Rescan
1007 enum MergeResult { NoMergeNeeded, Merged, Conflict };
1009 template<class Type>
1010 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
1012 bool equal_all = middle == left && middle == right && left == right;
1014 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1015 bool conflict = middle != left && middle != right && left != right;
1017 return std::pair<MergeResult, Type>(Conflict, left);
1022 return std::pair<MergeResult, Type>(Merged, middle);
1026 return std::pair<MergeResult, Type>(Merged, right);
1028 return std::pair<MergeResult, Type>(Merged, left);
1032 return std::pair<MergeResult, Type>(Merged, middle);
1035 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1039 * @brief Do auto-merge.
1040 * @param [in] dstPane Destination side
1042 void CMergeDoc::DoAutoMerge(int dstPane)
1044 const int lastDiff = m_diffList.GetSize() - 1;
1045 const int firstDiff = 0;
1046 bool bGroupWithPrevious = false;
1047 int autoMergedCount = 0;
1048 int unresolvedConflictCount = 0;
1050 std::pair<MergeResult, FileTextEncoding> mergedEncoding =
1051 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
1052 if (mergedEncoding.first == Merged)
1054 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
1055 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
1057 else if (mergedEncoding.first == Conflict)
1058 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
1060 std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle =
1061 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
1062 if (mergedEOLStyle.first == Merged)
1064 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1065 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1067 else if (mergedEOLStyle.first == Conflict)
1068 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1070 RescanSuppress suppressRescan(*this);
1072 // Note we don't care about m_nDiffs count to become zero,
1073 // because we don't rescan() so it does not change
1075 SetCurrentDiff(lastDiff);
1077 SetEditedAfterRescan(dstPane);
1079 int nGroup = GetActiveMergeView()->m_nThisGroup;
1080 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1081 CPoint currentPosDst = pViewDst->GetCursorPos();
1082 currentPosDst.x = 0;
1085 pViewDst->SetCursorPos(pt);
1086 pViewDst->SetNewSelection(pt, pt, false);
1087 pViewDst->SetNewAnchor(pt);
1089 // copy from bottom up is more efficient
1090 for (int i = lastDiff; i >= firstDiff; --i)
1092 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1093 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1097 if (currentPosDst.y > pdi->dend)
1099 if (pdi->blank[dstPane] >= 0)
1100 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1101 else if (pdi->blank[srcPane] >= 0)
1102 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1104 // Group merge with previous (merge undo data to one action)
1105 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1106 break; // sync failure
1107 if (!bGroupWithPrevious)
1108 bGroupWithPrevious = true;
1111 if (pdi->op == OP_DIFF)
1112 ++unresolvedConflictCount;
1115 ForEachView(dstPane, [currentPosDst](auto& pView) {
1116 pView->SetCursorPos(currentPosDst);
1117 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1118 pView->SetNewAnchor(currentPosDst);
1121 suppressRescan.Clear(); // done suppress Rescan
1123 UpdateHeaderPath(dstPane);
1125 if (autoMergedCount > 0)
1126 m_bAutoMerged = true;
1128 // move to first conflict
1129 const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1131 pViewDst->SelectDiff(nDiff, true, false);
1134 strutils::format_string2(
1135 _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"),
1136 strutils::format(_T("%d"), autoMergedCount),
1137 strutils::format(_T("%d"), unresolvedConflictCount)),
1138 MB_ICONINFORMATION);
1142 * @brief Sanity check difference.
1144 * Checks that lines in difference are inside difference in both files.
1145 * If file is edited, lines added or removed diff lines get out of sync and
1146 * merging fails miserably.
1148 * @param [in] dr Difference to check.
1149 * @return true if difference lines match, false otherwise.
1151 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1153 const int cd_dbegin = dr.dbegin;
1154 const int cd_dend = dr.dend;
1156 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1158 // Must ensure line number is in range before getting line flags
1159 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1162 // Optimization - check last line first so we don't need to
1163 // check whole diff for obvious cases
1164 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1165 if (!(dwFlags & LF_WINMERGE_FLAGS))
1169 for (int line = cd_dbegin; line < cd_dend; line++)
1171 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1173 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1174 if (!(dwFlags & LF_WINMERGE_FLAGS))
1182 * @brief Copy selected (=current) difference from from side to side.
1183 * @param [in] srcPane Source side from which diff is copied
1184 * @param [in] dstPane Destination side
1185 * @param [in] nDiff Diff to copy, if -1 function determines it.
1186 * @param [in] bGroupWithPrevious Adds diff to same undo group with
1187 * @return true if ok, false if sync failure & need to abort copy
1188 * previous action (allows one undo for copy all)
1190 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1191 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1193 int nGroup = GetActiveMergeView()->m_nThisGroup;
1194 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1195 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1196 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1198 // suppress Rescan during this method
1199 // (Not only do we not want to rescan a lot of times, but
1200 // it will wreck the line status array to rescan as we merge)
1201 RescanSuppress suppressRescan(*this);
1203 // If diff-number not given, determine it from active view
1206 nDiff = GetCurrentDiff();
1208 // No current diff, but maybe cursor is in diff?
1209 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1210 pViewDst->IsCursorInDiff()))
1212 // Find out diff under cursor
1213 CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1214 nDiff = m_diffList.LineToDiff(ptCursor.y);
1221 VERIFY(m_diffList.GetDiff(nDiff, cd));
1222 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1223 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1224 bool bSrcWasMod = sbuf.IsModified();
1225 const int cd_dbegin = cd.dbegin;
1226 const int cd_dend = cd.dend;
1227 const int cd_blank = cd.blank[srcPane];
1228 bool bInSync = SanityCheckDiff(cd);
1232 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1233 return false; // abort copying
1236 // If we remove whole diff from current view, we must fix cursor
1237 // position first. Normally we would move to end of previous line,
1238 // but we want to move to begin of that line for usability.
1241 CPoint currentPos = pViewDst->GetCursorPos();
1243 if (currentPos.y > cd_dend)
1245 if (cd.blank[dstPane] >= 0)
1246 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1247 else if (cd.blank[srcPane] >= 0)
1248 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1250 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1253 // if the current diff contains missing lines, remove them from both sides
1254 int limit = cd_dend;
1256 // curView is the view which is changed, so the opposite of the source view
1257 dbuf.BeginUndoGroup(bGroupWithPrevious);
1260 // text was missing, so delete rest of lines on both sides
1261 // delete only on destination side since rescan will clear the other side
1262 if (cd_dend + 1 < dbuf.GetLineCount())
1264 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1268 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1269 ASSERT(cd_blank > 0);
1270 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1274 dbuf.FlushUndoGroup(pSource);
1275 dbuf.BeginUndoGroup(true);
1279 // copy the selected text over
1280 if (cd_dbegin <= limit)
1282 // text exists on left side, so just replace
1283 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1284 dbuf.FlushUndoGroup(pSource);
1285 dbuf.BeginUndoGroup(true);
1287 dbuf.FlushUndoGroup(pSource);
1292 // reset the mod status of the source view because we do make some
1293 // changes, but none that concern the source text
1294 sbuf.SetModified(bSrcWasMod);
1297 suppressRescan.Clear(); // done suppress Rescan
1302 bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
1303 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1305 int nGroup = GetActiveMergeView()->m_nThisGroup;
1306 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1307 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1308 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1310 // suppress Rescan during this method
1311 // (Not only do we not want to rescan a lot of times, but
1312 // it will wreck the line status array to rescan as we merge)
1313 RescanSuppress suppressRescan(*this);
1316 VERIFY(m_diffList.GetDiff(nDiff, cd));
1317 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1318 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1319 bool bSrcWasMod = sbuf.IsModified();
1320 const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
1321 const int cd_dend = cd.dend;
1322 const int cd_blank = cd.blank[srcPane];
1323 bool bInSync = SanityCheckDiff(cd);
1327 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1328 return false; // abort copying
1331 // If we remove whole diff from current view, we must fix cursor
1332 // position first. Normally we would move to end of previous line,
1333 // but we want to move to begin of that line for usability.
1336 CPoint currentPos = pViewDst->GetCursorPos();
1338 if (currentPos.y > cd_dend)
1340 if (cd.blank[dstPane] >= 0)
1341 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1342 else if (cd.blank[srcPane] >= 0)
1343 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1345 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1348 // if the current diff contains missing lines, remove them from both sides
1349 int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
1351 // curView is the view which is changed, so the opposite of the source view
1352 dbuf.BeginUndoGroup(bGroupWithPrevious);
1353 if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
1355 // text was missing, so delete rest of lines on both sides
1356 // delete only on destination side since rescan will clear the other side
1357 if (limit+1 < dbuf.GetLineCount())
1359 dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
1363 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1364 ASSERT(cd_dbegin > 0);
1365 dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
1368 limit = cd_dbegin-1;
1369 dbuf.FlushUndoGroup(pSource);
1370 dbuf.BeginUndoGroup(true);
1373 // copy the selected text over
1374 if (cd_dbegin <= limit)
1376 // text exists on left side, so just replace
1377 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1378 dbuf.FlushUndoGroup(pSource);
1379 dbuf.BeginUndoGroup(true);
1381 dbuf.FlushUndoGroup(pSource);
1386 // reset the mod status of the source view because we do make some
1387 // changes, but none that concern the source text
1388 sbuf.SetModified(bSrcWasMod);
1390 suppressRescan.Clear(); // done suppress Rescan
1395 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1396 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1398 int nGroup = GetActiveMergeView()->m_nThisGroup;
1399 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1400 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1401 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1403 // suppress Rescan during this method
1404 // (Not only do we not want to rescan a lot of times, but
1405 // it will wreck the line status array to rescan as we merge)
1406 RescanSuppress suppressRescan(*this);
1409 VERIFY(m_diffList.GetDiff(nDiff, cd));
1410 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1411 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1412 bool bSrcWasMod = sbuf.IsModified();
1413 const int cd_dbegin = cd.dbegin;
1414 const int cd_dend = cd.dend;
1415 const int cd_blank = cd.blank[srcPane];
1416 bool bInSync = SanityCheckDiff(cd);
1420 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1421 return false; // abort copying
1424 std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1426 if (worddiffs.empty())
1429 if (cd.end[srcPane] < cd.begin[srcPane])
1430 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1432 if (firstWordDiff == -1)
1434 if (lastWordDiff == -1)
1435 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1437 // If we remove whole diff from current view, we must fix cursor
1438 // position first. Normally we would move to end of previous line,
1439 // but we want to move to begin of that line for usability.
1442 CPoint currentPos = pViewDst->GetCursorPos();
1444 if (currentPos.y > cd_dend)
1446 if (cd.blank[dstPane] >= 0)
1447 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1448 else if (cd.blank[srcPane] >= 0)
1449 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1451 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1454 // if the current diff contains missing lines, remove them from both sides
1455 int limit = cd_dend;
1457 // curView is the view which is changed, so the opposite of the source view
1458 dbuf.BeginUndoGroup(bGroupWithPrevious);
1460 CString srcText, dstText;
1461 CPoint ptDstStart, ptDstEnd;
1462 CPoint ptSrcStart, ptSrcEnd;
1464 ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1465 ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1466 ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1467 ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1468 ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1469 ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1470 ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1471 ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1473 std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1474 std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1476 dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1477 sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1480 for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1481 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1483 for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1484 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1486 for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1488 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1490 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1491 int srcEnd = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1492 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1493 int dstEnd = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1494 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1495 + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1496 + dstText.Mid(dstEnd - ptDstStart.x);
1499 dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1502 dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1504 dbuf.FlushUndoGroup(pSource);
1506 // reset the mod status of the source view because we do make some
1507 // changes, but none that concern the source text
1508 sbuf.SetModified(bSrcWasMod);
1510 suppressRescan.Clear(); // done suppress Rescan
1517 * @brief Save file with new filename.
1519 * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1520 * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1521 * normal filename fails, to let user choose another filename/location.
1522 * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1523 * using this function.
1524 * @param [in, out] strPath
1525 * - [in] : Initial path shown to user
1526 * - [out] : Path to new filename if saving succeeds
1527 * @param [in, out] nSaveResult
1528 * - [in] : Statuscode telling why we ended up here. Maybe the result of
1530 * - [out] : Statuscode of this saving try
1531 * @param [in, out] sError Error string from lower level saving code
1532 * @param [in] nBuffer Buffer we are saving
1533 * @return false as long as the user is not satisfied. Calling function
1534 * should not continue until true is returned.
1535 * @sa CMergeDoc::DoSave()
1536 * @sa CMergeDoc::DoSaveAs()
1537 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1539 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1540 int nBuffer, PackingInfo& infoTempUnpacker)
1544 String strSavePath; // New path for next saving try
1547 int answer = IDOK; // Set default we use for scratchpads
1549 // We shouldn't get here if saving is succeed before
1550 ASSERT(nSaveResult != SAVE_DONE);
1552 // Select message based on reason function called
1553 if (nSaveResult == SAVE_PACK_FAILED)
1555 str = CMergeApp::GetPackingErrorMessage(nBuffer, m_nBuffers, strPath, infoTempUnpacker);
1556 // replace the unpacker with a "do nothing" unpacker
1557 infoTempUnpacker.Initialize(false);
1561 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);
1564 // SAVE_NO_FILENAME is temporarily used for scratchpad.
1565 // So don't ask about saving in that case.
1566 if (nSaveResult != SAVE_NO_FILENAME)
1567 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1573 title = _("Save Left File As");
1574 else if (nBuffer == m_nBuffers - 1)
1575 title = _("Save Right File As");
1577 title = _("Save Middle File As");
1579 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1581 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1583 nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1586 if (nSaveResult == SAVE_DONE)
1588 // We are saving scratchpad (unnamed file)
1589 if (strPath.empty())
1591 m_nBufferType[nBuffer] = BUFFERTYPE::UNNAMED_SAVED;
1592 m_strDesc[nBuffer].erase();
1595 strPath = strSavePath;
1596 UpdateHeaderPath(nBuffer);
1602 nSaveResult = SAVE_CANCELLED;
1606 nSaveResult = SAVE_CANCELLED;
1613 * @brief Save file creating backups etc.
1615 * Safe top-level file saving function. Checks validity of given path.
1616 * Creates backup file if wanted to. And if saving to given path fails,
1617 * allows user to select new location/name for file.
1618 * @param [in] szPath Path where to save including filename. Can be
1619 * empty/`nullptr` if new file is created (scratchpad) without filename.
1620 * @param [out] bSaveSuccess Will contain information about save success with
1621 * the original name (to determine if file statuses should be changed)
1622 * @param [in] nBuffer Index (0-based) of buffer to save
1623 * @return Tells if caller can continue (no errors happened)
1624 * @note Return value does not tell if SAVING succeeded. Caller must
1625 * Check value of bSaveSuccess parameter after calling this function!
1626 * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1627 * to directory it points to. If m_strSaveAsPath contains filename,
1628 * that filename is used.
1629 * @sa CMergeDoc::TrySaveAs()
1630 * @sa CMainFrame::CheckSavePath()
1631 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1633 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1635 DiffFileInfo fileInfo;
1636 String strSavePath(szPath);
1637 FileChange fileChanged;
1638 bool bApplyToAll = false;
1641 fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1642 if (fileChanged == FileChange::Changed)
1644 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1645 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1647 bSaveSuccess = true;
1652 // use a temp packer
1653 // first copy the m_infoUnpacker
1654 // if an error arises during packing, change and take a "do nothing" packer
1655 PackingInfo infoTempUnpacker = m_infoUnpacker;
1657 bSaveSuccess = false;
1659 // Check third arg possibly given from command-line
1660 if (!theApp.m_strSaveAsPath.empty())
1662 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1664 // third arg was a directory, so get append the filename
1666 paths::SplitFilename(szPath, 0, &sname, 0);
1667 strSavePath = theApp.m_strSaveAsPath;
1668 strSavePath = paths::ConcatPath(strSavePath, sname);
1671 strSavePath = theApp.m_strSaveAsPath;
1674 nRetVal = CMergeApp::HandleReadonlySave(strSavePath, false, bApplyToAll);
1675 if (nRetVal == IDCANCEL)
1678 if (!CMergeApp::CreateBackup(false, strSavePath))
1681 // false as long as the user is not satisfied
1682 // true if saving succeeds, even with another filename, or if the user cancels
1683 bool result = false;
1684 // the error code from the latest save operation,
1685 // or SAVE_DONE when the save succeeds
1686 // TODO: Shall we return this code in addition to bSaveSuccess ?
1687 int nSaveErrorCode = SAVE_DONE;
1688 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1690 // Assume empty filename means Scratchpad (unnamed file)
1691 // Todo: This is not needed? - buffer type check should be enough
1692 if (strSavePath.empty())
1693 nSaveErrorCode = SAVE_NO_FILENAME;
1695 // Handle unnamed buffers
1696 if (m_nBufferType[nBuffer] == BUFFERTYPE::UNNAMED)
1697 nSaveErrorCode = SAVE_NO_FILENAME;
1700 if (nSaveErrorCode == SAVE_DONE)
1701 // We have a filename, just try to save
1702 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, infoTempUnpacker);
1704 if (nSaveErrorCode != SAVE_DONE)
1706 // Saving failed, user may save to another location if wants to
1708 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1712 // Saving succeeded with given/selected filename
1713 if (nSaveErrorCode == SAVE_DONE)
1715 // Preserve file times if user wants to
1716 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1718 fileInfo.SetFile(strSavePath);
1721 TFile file(strSavePath);
1722 file.setLastModified(fileInfo.mtime);
1729 m_ptBuf[nBuffer]->SetModified(false);
1730 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1731 m_filePaths[nBuffer] = strSavePath;
1732 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1733 UpdateHeaderPath(nBuffer);
1734 bSaveSuccess = true;
1737 else if (nSaveErrorCode == SAVE_CANCELLED)
1739 // User cancelled current operation, lets do what user wanted to do
1746 * @brief Save file with different filename.
1748 * Safe top-level file saving function. Asks user to select filename
1749 * and path. Does not create backups.
1750 * @param [in] szPath Path where to save including filename. Can be
1751 * empty/`nullptr` if new file is created (scratchpad) without filename.
1752 * @param [out] bSaveSuccess Will contain information about save success with
1753 * the original name (to determine if file statuses should be changed)
1754 * @param [in] nBuffer Index (0-based) of buffer to save
1755 * @return Tells if caller can continue (no errors happened)
1756 * @note Return value does not tell if SAVING succeeded. Caller must
1757 * Check value of bSaveSuccess parameter after calling this function!
1758 * @sa CMergeDoc::TrySaveAs()
1759 * @sa CMainFrame::CheckSavePath()
1760 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1762 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1764 String strSavePath(szPath);
1766 // use a temp packer
1767 // first copy the m_infoUnpacker
1768 // if an error arises during packing, change and take a "do nothing" packer
1769 PackingInfo infoTempUnpacker = m_infoUnpacker;
1771 bSaveSuccess = false;
1772 // false as long as the user is not satisfied
1773 // true if saving succeeds, even with another filename, or if the user cancels
1774 bool result = false;
1775 // the error code from the latest save operation,
1776 // or SAVE_DONE when the save succeeds
1777 // TODO: Shall we return this code in addition to bSaveSuccess ?
1778 int nSaveErrorCode = SAVE_DONE;
1780 // Use SAVE_NO_FILENAME to prevent asking about error
1781 nSaveErrorCode = SAVE_NO_FILENAME;
1783 // Loop until user succeeds saving or cancels
1786 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1789 // Saving succeeded with given/selected filename
1790 if (nSaveErrorCode == SAVE_DONE)
1792 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1793 m_filePaths[nBuffer] = strSavePath;
1794 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1795 UpdateHeaderPath(nBuffer);
1796 bSaveSuccess = true;
1803 * @brief Get left->right info for a moved line (apparent line number)
1805 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1807 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1810 int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1811 int realRightLine = -1;
1812 if (m_diffWrapper.GetDetectMovedBlocks())
1814 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1815 MovedLines::SIDE::RIGHT);
1817 if (realRightLine != -1)
1818 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1824 * @brief Get right->left info for a moved line (apparent line number)
1826 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1828 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1831 int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1832 int realLeftLine = -1;
1833 if (m_diffWrapper.GetDetectMovedBlocks())
1835 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1836 MovedLines::SIDE::LEFT);
1838 if (realLeftLine != -1)
1839 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1845 * @brief Save modified documents.
1846 * This function asks if user wants to save modified documents. We also
1847 * allow user to cancel the closing.
1849 * There is a special trick avoiding showing two save-dialogs, as MFC framework
1850 * sometimes calls this function twice. We use static counter for these calls
1851 * and if we already have saving in progress (counter == 1) we skip the new
1854 * @return true if docs are closed, false if closing is cancelled.
1856 BOOL CMergeDoc::SaveModified()
1863 if (PromptAndSaveIfNeeded(true))
1876 * @brief Sets the current difference.
1877 * @param [in] nDiff Difference to set as current difference.
1879 void CMergeDoc::SetCurrentDiff(int nDiff)
1881 if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1888 * @brief Take care of rescanning document.
1890 * Update view and restore cursor and scroll position after
1891 * rescanning document.
1892 * @param [in] bForced If true rescan cannot be suppressed
1894 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1896 // Ignore suppressing when forced rescan
1898 if (!m_bEnableRescan) return;
1900 CWaitCursor waitstatus;
1902 CMergeEditView *pActiveView = GetActiveMergeView();
1904 // store cursors and hide caret
1905 ForEachView([](auto& pView) { pView->PushCursors(); });
1906 pActiveView->HideCursor();
1908 bool bBinary = false;
1909 IDENTLEVEL identical = IDENTLEVEL::NONE;
1910 int nRescanResult = Rescan(bBinary, identical, bForced);
1912 // restore cursors and caret
1913 ForEachView([](auto& pView) { pView->PopCursors(); });
1914 pActiveView->ShowCursor();
1916 ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1917 // because of ghostlines, m_nTopLine may differ just after Rescan
1918 // scroll both views to the same top line
1919 pView->UpdateSiblingScrollPos(false);
1921 // make sure we see the cursor from the curent view
1922 pActiveView->EnsureVisible(pActiveView->GetCursorPos());
1925 UpdateAllViews(nullptr);
1927 // Show possible error after updating screen
1928 if (nRescanResult != RESCAN_SUPPRESSED)
1929 ShowRescanError(nRescanResult, identical);
1930 m_LastRescan = COleDateTime::GetCurrentTime();
1934 * @brief Saves both files
1936 void CMergeDoc::OnFileSave()
1938 // We will need to know if either of the originals actually changed
1939 // so we know whether to update the diff status
1940 bool bChangedOriginal = false;
1942 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1944 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1946 // (why we don't use return value of DoSave)
1947 // DoSave will return true if it wrote to something successfully
1948 // but we have to know if it overwrote the original file
1949 bool bSaveOriginal = false;
1950 DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1952 bChangedOriginal = true;
1956 // If either of the actual source files being compared was changed
1957 // we need to update status in the dir view
1958 if (bChangedOriginal)
1960 // If DirDoc contains diffs
1961 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1963 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1965 if (m_bEditAfterRescan[nBuffer])
1967 FlushAndRescan(false);
1972 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1973 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1974 m_nTrivialDiffs, bIdentical);
1979 void CMergeDoc::DoFileSave(int nBuffer)
1981 bool bSaveSuccess = false;
1982 bool bModified = false;
1984 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1987 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
1990 // If file were modified and saving succeeded,
1991 // update status on dir view
1992 if (bModified && bSaveSuccess)
1994 // If DirDoc contains compare results
1995 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1997 for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
1999 if (m_bEditAfterRescan[nBuffer1])
2001 FlushAndRescan(false);
2006 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2007 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2008 m_nTrivialDiffs, bIdentical);
2014 * @brief Saves left-side file
2016 void CMergeDoc::OnFileSaveLeft()
2022 * @brief Saves middle-side file
2024 void CMergeDoc::OnFileSaveMiddle()
2030 * @brief Saves right-side file
2032 void CMergeDoc::OnFileSaveRight()
2034 DoFileSave(m_nBuffers - 1);
2038 * @brief Saves left-side file with name asked
2040 void CMergeDoc::OnFileSaveAsLeft()
2042 bool bSaveResult = false;
2043 DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
2047 * @brief Called when "Save middle (as...)" item is updated
2049 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
2051 pCmdUI->Enable(m_nBuffers == 3);
2055 * @brief Saves right-side file with name asked
2057 void CMergeDoc::OnFileSaveAsMiddle()
2059 bool bSaveResult = false;
2060 DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
2064 * @brief Saves right-side file with name asked
2066 void CMergeDoc::OnFileSaveAsRight()
2068 bool bSaveResult = false;
2069 DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
2073 * @brief Update diff-number pane text in file compare.
2074 * The diff number pane shows selected difference/amount of differences when
2075 * there is difference selected. If there is no difference selected, then
2076 * the panel shows amount of differences. If there are ignored differences,
2077 * those are not count into numbers.
2078 * @param [in] pCmdUI UI component to update.
2080 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
2082 TCHAR sIdx[32] = { 0 };
2083 TCHAR sCnt[32] = { 0 };
2085 const int nDiffs = m_diffList.GetSignificantDiffs();
2087 // Files are identical - show text "Identical"
2091 // There are differences, but no selected diff
2092 // - show amount of diffs
2093 else if (GetCurrentDiff() < 0)
2095 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
2096 _itot_s(nDiffs, sCnt, 10);
2097 strutils::replace(s, _T("%1"), sCnt);
2100 // There are differences and diff selected
2101 // - show diff number and amount of diffs
2104 s = _("Difference %1 of %2");
2105 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
2106 _itot_s(signInd + 1, sIdx, 10);
2107 strutils::replace(s, _T("%1"), sIdx);
2108 _itot_s(nDiffs, sCnt, 10);
2109 strutils::replace(s, _T("%2"), sCnt);
2111 pCmdUI->SetText(s.c_str());
2115 * @brief Change number of diff context lines
2117 void CMergeDoc::OnDiffContext(UINT nID)
2121 case ID_VIEW_DIFFCONTEXT_0:
2122 m_nDiffContext = 0; break;
2123 case ID_VIEW_DIFFCONTEXT_1:
2124 m_nDiffContext = 1; break;
2125 case ID_VIEW_DIFFCONTEXT_3:
2126 m_nDiffContext = 3; break;
2127 case ID_VIEW_DIFFCONTEXT_5:
2128 m_nDiffContext = 5; break;
2129 case ID_VIEW_DIFFCONTEXT_7:
2130 m_nDiffContext = 7; break;
2131 case ID_VIEW_DIFFCONTEXT_9:
2132 m_nDiffContext = 9; break;
2133 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2134 m_nDiffContext = -m_nDiffContext - 1; break;
2135 case ID_VIEW_DIFFCONTEXT_ALL:
2136 if (m_nDiffContext >= 0)
2137 m_nDiffContext = -m_nDiffContext - 1;
2139 case ID_VIEW_DIFFCONTEXT_INVERT:
2140 m_bInvertDiffContext = !m_bInvertDiffContext;
2143 GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2144 GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2145 FlushAndRescan(true);
2149 * @brief Swap context enable for 3 file compares
2151 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
2155 pCmdUI->Enable(true);
2159 pCmdUI->Enable(false);
2164 * @brief Update number of diff context lines
2166 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2169 switch (pCmdUI->m_nID)
2171 case ID_VIEW_DIFFCONTEXT_0:
2172 bCheck = (m_nDiffContext == 0); break;
2173 case ID_VIEW_DIFFCONTEXT_1:
2174 bCheck = (m_nDiffContext == 1); break;
2175 case ID_VIEW_DIFFCONTEXT_3:
2176 bCheck = (m_nDiffContext == 3); break;
2177 case ID_VIEW_DIFFCONTEXT_5:
2178 bCheck = (m_nDiffContext == 5); break;
2179 case ID_VIEW_DIFFCONTEXT_7:
2180 bCheck = (m_nDiffContext == 7); break;
2181 case ID_VIEW_DIFFCONTEXT_9:
2182 bCheck = (m_nDiffContext == 9); break;
2183 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2184 bCheck = false; break;
2185 case ID_VIEW_DIFFCONTEXT_INVERT:
2186 bCheck = m_bInvertDiffContext; break;
2188 bCheck = (m_nDiffContext < 0); break;
2190 pCmdUI->SetCheck(bCheck);
2191 pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2195 * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2197 * @note After PrimeTextBuffers(), all buffers should have the same length.
2199 void CMergeDoc::PrimeTextBuffers()
2202 m_nTrivialDiffs = 0;
2204 int nDiffCount = m_diffList.GetSize();
2207 // walk the diff list and calculate numbers of extra lines to add
2208 int extras[3] = {0, 0, 0}; // extra lines added to each view
2209 m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2211 // resize m_aLines once for each view
2212 UINT lcount[3] = {0, 0, 0};
2213 UINT lcountnew[3] = {0, 0, 0};
2216 for (file = 0; file < m_nBuffers; file++)
2218 lcount[file] = m_ptBuf[file]->GetLineCount();
2219 lcountnew[file] = lcount[file] + extras[file];
2220 lcountmax = max(lcountmax, lcountnew[file]);
2222 for (file = 0; file < m_nBuffers; file++)
2224 m_ptBuf[file]->m_aLines.resize(lcountmax);
2227 // walk the diff list backward, move existing lines to proper place,
2228 // add ghost lines, and set flags
2229 for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2232 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2234 // move matched lines after curDiff
2235 int nline[3] = { 0, 0, 0 };
2236 for (file = 0; file < m_nBuffers; file++)
2237 nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2238 // Matched lines should really match...
2239 // But matched lines after last diff may differ because of empty last line (see function's note)
2240 if (nDiff < nDiffCount - 1)
2241 ASSERT(nline[0] == nline[1]);
2244 for (file = 0; file < m_nBuffers; file++)
2246 // Move all lines after current diff down as far as needed
2247 // for any ghost lines we're about to insert
2248 m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2249 lcountnew[file] -= nline[file];
2250 lcount[file] -= nline[file];
2251 // move unmatched lines and add ghost lines
2252 nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2253 nmaxline = max(nmaxline, nline[file]);
2256 for (file = 0; file < m_nBuffers; file++)
2258 DWORD dflag = LF_GHOST;
2259 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2261 m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2262 int nextra = nmaxline - nline[file];
2265 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2266 for (int i = 1; i <= nextra; i++)
2267 m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2269 lcountnew[file] -= nmaxline;
2271 lcount[file] -= nline[file];
2274 // set dbegin, dend, blank, and line flags
2275 curDiff.dbegin = lcountnew[0];
2288 curDiff.dend = lcountnew[0]+nmaxline-1;
2289 for (file = 0; file < m_nBuffers; file++)
2291 curDiff.blank[file] = -1;
2292 int nextra = nmaxline - nline[file];
2293 if (nmaxline > nline[file])
2295 // more lines on left, ghost lines on right side
2296 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2302 for (file = 0; file < m_nBuffers; file++)
2306 for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2308 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2310 // set diff or trivial flag
2311 DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2312 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2314 m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2315 m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2319 // ghost lines are already inserted (and flagged)
2320 // ghost lines opposite to trivial lines are ghost and trivial
2321 if (curDiff.op == OP_TRIVIAL)
2322 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2328 } // switch (curDiff.op)
2329 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2330 } // for (nDiff = nDiffCount; nDiff-- > 0; )
2332 m_diffList.ConstructSignificantChain();
2335 // Note: By this point all `m_ptBuf[]` buffers must have the same
2336 // number of line entries; eventual buffer processing typically
2337 // uses the line count from `m_ptBuf[0]` for all buffer processing.
2339 for (file = 0; file < m_nBuffers; file++)
2341 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2345 for (file = 0; file < m_nBuffers; file++)
2346 m_ptBuf[file]->FinishLoading();
2350 * @brief Checks if file has changed since last update (save or rescan).
2351 * @param [in] szPath File to check
2352 * @param [in] dfi Previous fileinfo of file
2353 * @param [in] bSave If true Compare to last save-info, else to rescan-info
2354 * @param [in] nBuffer Index (0-based) of buffer
2355 * @return true if file is changed.
2357 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2358 bool bSave, int nBuffer)
2360 DiffFileInfo *fileInfo = nullptr;
2361 bool bFileChanged = false;
2362 bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2364 if (bIgnoreSmallDiff)
2365 tolerance = SmallTimeDiff; // From MainFrm.h
2368 fileInfo = m_pSaveFileInfo[nBuffer].get();
2370 fileInfo = m_pRescanFileInfo[nBuffer].get();
2372 // We assume file existed, so disappearing means removal
2373 if (!dfi.Update(szPath))
2374 return FileChange::Removed;
2376 int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2377 if (timeDiff < 0) timeDiff = -timeDiff;
2378 if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2380 bFileChanged = true;
2384 return FileChange::Changed;
2386 return FileChange::NoChange;
2389 void CMergeDoc::HideLines()
2394 if (m_nDiffContext < 0)
2396 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2400 int nLineCount = 0x7fffffff;
2401 for (file = 0; file < m_nBuffers; file++)
2403 if (nLineCount > m_ptBuf[file]->GetLineCount())
2404 nLineCount = m_ptBuf[file]->GetLineCount();
2407 for (nLine = 0; nLine < nLineCount;)
2409 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2410 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2412 for (file = 0; file < m_nBuffers; file++)
2413 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2418 int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2419 for (; nLine2 < nLine; nLine2++)
2421 for (file = 0; file < m_nBuffers; file++)
2422 m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2425 for (; nLine < nLineCount; nLine++)
2427 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2428 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2430 for (file = 0; file < m_nBuffers; file++)
2431 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2434 int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2435 for (; nLine < nLineEnd2; nLine++)
2437 for (file = 0; file < m_nBuffers; file++)
2438 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2439 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2440 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2441 nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2446 ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2450 * @brief Asks and then saves modified files.
2452 * This function saves modified files. Dialog is shown for user to select
2453 * modified file(s) one wants to save or discard changed. Cancelling of
2454 * save operation is allowed unless denied by parameter. After successfully
2455 * save operation file statuses are updated to directory compare.
2456 * @param [in] bAllowCancel If false "Cancel" button is disabled.
2457 * @return true if user selected "OK" so next operation can be
2458 * executed. If false user choosed "Cancel".
2459 * @note If filename is empty, we assume scratchpads are saved,
2460 * so instead of filename, description is shown.
2461 * @todo If we have filename and description for file, what should
2462 * we do after saving to different filename? Empty description?
2463 * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2465 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2467 bool bLModified = false, bMModified = false, bRModified = false;
2469 bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2471 if (m_nBuffers == 3)
2473 bLModified = m_ptBuf[0]->IsModified();
2474 bMModified = m_ptBuf[1]->IsModified();
2475 bRModified = m_ptBuf[2]->IsModified();
2479 bLModified = m_ptBuf[0]->IsModified();
2480 bRModified = m_ptBuf[1]->IsModified();
2482 if (!bLModified && !bMModified && !bRModified)
2486 dlg.DoAskFor(bLModified, bMModified, bRModified);
2488 dlg.m_bDisableCancel = true;
2489 if (!m_filePaths.GetLeft().empty())
2491 if (theApp.m_strSaveAsPath.empty())
2492 dlg.m_sLeftFile = m_filePaths.GetLeft();
2494 dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2497 dlg.m_sLeftFile = m_strDesc[0];
2498 if (m_nBuffers == 3)
2500 if (!m_filePaths.GetMiddle().empty())
2502 if (theApp.m_strSaveAsPath.empty())
2503 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2505 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2508 dlg.m_sMiddleFile = m_strDesc[1];
2510 if (!m_filePaths.GetRight().empty())
2512 if (theApp.m_strSaveAsPath.empty())
2513 dlg.m_sRightFile = m_filePaths.GetRight();
2515 dlg.m_sRightFile = theApp.m_strSaveAsPath;
2518 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2520 if (dlg.DoModal() == IDOK)
2522 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2524 if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2528 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2530 if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2534 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2536 if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2545 // If file were modified and saving was successfull,
2546 // update status on dir view
2547 if ((bLModified && bLSaveSuccess) ||
2548 (bMModified && bMSaveSuccess) ||
2549 (bRModified && bRSaveSuccess))
2551 // If directory compare has results
2552 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2554 if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2555 FlushAndRescan(false);
2557 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2558 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2559 m_nTrivialDiffs, bIdentical);
2566 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2567 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2569 // if we did not rescan during the request timeOut, Rescan
2570 // else we did Rescan after the request, so do nothing
2571 COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2572 if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2573 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2578 * @brief We have two child views (left & right), so we keep pointers directly
2579 * at them (the MFC view list doesn't have them both)
2581 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2584 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2585 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2589 void CMergeDoc::RemoveMergeViews(int nGroup)
2592 for (; nGroup < m_nGroups - 1; nGroup++)
2594 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2596 m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2597 m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2604 * @brief DirDoc gives us its identity just after it creates us
2606 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2608 ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2609 m_pDirDoc = pDirDoc;
2613 * @brief Return pointer to parent frame
2615 CMergeEditFrame * CMergeDoc::GetParentFrame()
2617 return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame());
2621 * @brief DirDoc is closing
2623 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2625 ASSERT(m_pDirDoc == pDirDoc);
2626 m_pDirDoc = nullptr;
2627 // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2631 * @brief DirDoc commanding us to close
2633 bool CMergeDoc::CloseNow()
2635 // Allow user to cancel closing
2636 if (!PromptAndSaveIfNeeded(true))
2639 GetParentFrame()->DestroyWindow();
2644 * @brief Loads file to buffer and shows load-errors
2645 * @param [in] sFileName File to open
2646 * @param [in] nBuffer Index (0-based) of buffer to load
2647 * @param [out] readOnly whether file is read-only
2648 * @param [in] encoding encoding used
2649 * @return Tells if files were loaded successfully
2650 * @sa CMergeDoc::OpenDocs()
2652 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2655 DWORD retVal = FileLoadResult::FRESULT_ERROR;
2657 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2658 m_filePaths[nBuffer] = sFileName;
2660 CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2662 retVal = pBuf->LoadFromFile(sFileName, m_infoUnpacker,
2663 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2665 // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2666 // it left the pBuf in a valid (but empty) state via a call to InitNew
2668 if (FileLoadResult::IsOkImpure(retVal))
2670 // File loaded, and multiple EOL types in this file
2671 FileLoadResult::SetMainOk(retVal);
2673 // If mixed EOLs are not enabled, enable them for this doc.
2674 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2676 pBuf->SetMixedEOL(true);
2680 if (FileLoadResult::IsError(retVal))
2682 // Error from Unifile/system
2683 if (!sOpenError.IsEmpty())
2684 sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2686 sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2687 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2689 else if (FileLoadResult::IsErrorUnpack(retVal))
2691 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2692 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2698 * @brief Check if specified codepage number is valid for WinMerge Editor.
2699 * @param [in] cp Codepage number to check.
2700 * @return true if codepage is valid, false otherwise.
2702 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2704 if (cp == 0) // 0 is our signal value for invalid
2706 return GetEncodingNameFromCodePage(cp) != nullptr;
2710 * @brief Sanity check file's specified codepage.
2711 * This function checks if file's specified codepage is valid for WinMerge
2712 * editor and if not resets the codepage to default.
2713 * @param [in,out] fileinfo Class containing file's codepage.
2715 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2717 if (fileinfo.encoding.m_unicoding == ucr::NONE
2718 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2720 int cp = ucr::getDefaultCodepage();
2721 if (!IsValidCodepageForMergeEditor(cp))
2723 fileinfo.encoding.SetCodepage(cp);
2728 * @brief Loads one file from disk and updates file infos.
2729 * @param [in] index Index of file in internal buffers.
2730 * @param [in] filename File's name.
2731 * @param [in] readOnly Is file read-only?
2732 * @param [in] encoding File's encoding.
2733 * @return One of FileLoadResult values.
2735 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc,
2736 const FileTextEncoding & encoding)
2738 DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2740 m_strDesc[index] = strDesc;
2741 if (!filename.empty())
2743 if (strDesc.empty())
2744 m_nBufferType[index] = BUFFERTYPE::NORMAL;
2746 m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2747 m_pSaveFileInfo[index]->Update(filename);
2748 m_pRescanFileInfo[index]->Update(filename);
2750 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2751 if (FileLoadResult::IsLossy(loadSuccess))
2753 // Determine the file encoding by looking at all the contents of the file, not just part of it
2754 FileTextEncoding encodingNew = codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1);
2755 if (encoding != encodingNew)
2757 m_ptBuf[index]->FreeAll();
2758 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encodingNew);
2764 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2765 m_ptBuf[index]->InitNew();
2766 m_ptBuf[index]->m_encoding = encoding;
2767 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer
2768 loadSuccess = FileLoadResult::FRESULT_OK;
2773 void CMergeDoc::SetTableProperties()
2775 struct TableProps { bool istable; TCHAR delimiter; TCHAR quote; bool allowNewlinesInQuotes; };
2776 auto getTablePropsByFileName = [](const String& path, const std::optional<bool>& enableTableEditing)-> TableProps
2778 const TCHAR quote = GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR).c_str()[0];
2779 FileFilterHelper filterCSV, filterTSV, filterDSV;
2780 bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2781 const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2782 if (!csvFilePattern.empty())
2784 filterCSV.UseMask(true);
2785 filterCSV.SetMask(csvFilePattern);
2786 if (filterCSV.includeFile(path))
2787 return { true, ',', quote, allowNewlineIQuotes };
2789 const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2790 if (!tsvFilePattern.empty())
2792 filterTSV.UseMask(true);
2793 filterTSV.SetMask(tsvFilePattern);
2794 if (filterTSV.includeFile(path))
2795 return { true, '\t', quote, allowNewlineIQuotes };
2797 const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2798 if (!dsvFilePattern.empty())
2800 filterDSV.UseMask(true);
2801 filterDSV.SetMask(dsvFilePattern);
2802 if (filterDSV.includeFile(path))
2803 return { true, GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR).c_str()[0], quote };
2805 if (enableTableEditing.value_or(false))
2808 if (dlg.DoModal() == IDOK)
2809 return { true, dlg.m_sDelimiterChar.c_str()[0], dlg.m_sQuoteChar.c_str()[0], dlg.m_bAllowNewlinesInQuotes };
2811 return { false, 0, 0, false };
2814 TableProps tableProps[3] = {};
2815 int nTableFileIndex = -1;
2816 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2819 paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2820 tableProps[nBuffer] = getTablePropsByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2822 tableProps[nBuffer] = tableProps[nBuffer - 1];
2823 if (tableProps[nBuffer].istable)
2824 nTableFileIndex = nBuffer;
2826 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2828 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
2830 int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
2831 m_ptBuf[nBuffer]->SetTableEditing(true);
2832 m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
2833 m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
2834 m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
2835 m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
2836 m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
2840 m_ptBuf[nBuffer]->SetTableEditing(false);
2846 * @brief Loads files and does initial rescan.
2847 * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
2848 * @param bRO [in] Is left/middle/right file read-only
2849 * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
2850 * @todo Options are still read from CMainFrame, this will change
2851 * @sa CMainFrame::ShowTextMergeDoc()
2853 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
2854 const bool bRO[], const String strDesc[])
2856 IDENTLEVEL identical = IDENTLEVEL::NONE;
2857 int nRescanResult = RESCAN_OK;
2859 FileLocation fileloc[3];
2861 std::copy_n(ifileloc, 3, fileloc);
2863 // Filter out invalid codepages, or editor will display all blank
2864 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2865 SanityCheckCodepage(fileloc[nBuffer]);
2869 curUndo = undoTgt.begin();
2871 // Prevent displaying views during LoadFile
2872 // Note : attach buffer again only if both loads succeed
2873 m_strBothFilenames.erase();
2875 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
2877 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2879 // clear undo buffers
2880 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
2883 m_ptBuf[nBuffer]->FreeAll ();
2885 // build the text being filtered, "|" separates files as it is forbidden in filenames
2886 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
2888 m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
2891 DWORD nSuccess[3] = { FileLoadResult::FRESULT_ERROR, FileLoadResult::FRESULT_ERROR, FileLoadResult::FRESULT_ERROR };
2892 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2894 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
2895 fileloc[nBuffer].encoding);
2896 if (!FileLoadResult::IsOk(nSuccess[nBuffer]))
2898 CMergeEditFrame* pFrame = GetParentFrame();
2899 if (pFrame != nullptr)
2901 // Use verify macro to trap possible error in debug.
2902 VERIFY(pFrame->DestroyWindow());
2908 SetTableProperties();
2910 const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
2912 // scratchpad : we don't call LoadFile, so
2913 // we need to initialize the unpacker as a "do nothing" one
2914 if (bFiltersEnabled)
2916 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
2918 m_infoUnpacker.Initialize(false);
2922 // Warn user if file load was lossy (bad encoding)
2924 int nLossyBuffers = 0;
2925 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2927 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
2929 // TODO: It would be nice to report how many lines were lossy
2930 // we did calculate those numbers when we loaded the files, in the text stats
2932 idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
2936 if (nLossyBuffers > 1)
2937 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
2939 if (nLossyBuffers > 0)
2941 if (m_pEncodingErrorBar == nullptr)
2943 m_pEncodingErrorBar.reset(new CEncodingErrorBar());
2944 m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
2946 m_pEncodingErrorBar->SetText(LoadResString(idres));
2947 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
2950 ForEachView([](auto& pView) {
2951 // Now buffers data are valid
2952 pView->AttachToBuffer();
2953 // Currently there is only one set of syntax colors, which all documents & views share
2954 pView->SetColorContext(theApp.GetMainSyntaxColors());
2955 // Currently there is only one set of markers, which all documents & views share
2956 pView->SetMarkersContext(theApp.GetMainMarkers());
2958 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2960 // Set read-only statuses
2961 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
2964 // Check the EOL sensitivity option (do it before Rescan)
2965 DIFFOPTIONS diffOptions = {0};
2966 m_diffWrapper.GetOptions(&diffOptions);
2967 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
2969 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2970 if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
2973 if (nBuffer < m_nBuffers)
2975 // Options and files not are not compatible :
2976 // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
2977 // All lines will differ, that is not very interesting and probably not wanted.
2978 // Propose to turn off the option 'sensitive to EOL'
2979 String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
2980 if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
2982 diffOptions.bIgnoreEol = true;
2983 m_diffWrapper.SetOptions(&diffOptions);
2985 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
2986 const int nFormerResult = dlg.GetFormerResult();
2987 if (nFormerResult != -1)
2989 // "Don't ask this question again" checkbox is checked
2990 GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
2996 // Define the prediffer
2997 PackingInfo * infoUnpacker = nullptr;
2998 PrediffingInfo * infoPrediffer = nullptr;
2999 if (bFiltersEnabled && m_pDirDoc != nullptr)
3001 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
3002 m_diffWrapper.SetPrediffer(infoPrediffer);
3003 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
3006 bool bBinary = false;
3007 nRescanResult = Rescan(bBinary, identical);
3009 // Open filed if rescan succeed and files are not binaries
3010 if (nRescanResult == RESCAN_OK)
3012 // set the document types
3013 // Warning : it is the first thing to do (must be done before UpdateView,
3014 // or any function that calls UpdateView, like SelectDiff)
3015 // Note: If option enabled, and another side type is not recognized,
3016 // we use recognized type for unrecognized side too.
3021 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3023 sext[nBuffer] = GetFileExt(m_ptBuf[nBuffer]->GetTempFileName().c_str(), m_strDesc[nBuffer].c_str());
3024 ForEachView(nBuffer, [&](auto& pView) {
3025 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
3026 if (bTyped[nBuffer])
3027 paneTyped = nBuffer;
3031 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3033 if (bTyped[0] != bTyped[nBuffer])
3037 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
3038 if (syntaxHLEnabled && nBuffer < m_nBuffers)
3040 if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
3043 m_ptBuf[0]->GetLine(0, sFirstLine);
3044 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3046 bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
3051 if (syntaxHLEnabled)
3053 CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
3054 ForEachView([&bTyped, enuType](auto& pView) {
3055 if (!bTyped[pView->m_nThisPane])
3056 pView->SetTextType(enuType);
3060 int nNormalBuffer = 0;
3061 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3063 // set the frame window header
3064 UpdateHeaderPath(nBuffer);
3066 ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
3068 if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
3069 (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
3076 // Inform user that files are identical
3077 // Don't show message if new buffers created
3078 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
3080 ShowRescanError(nRescanResult, identical);
3083 // Exit if files are identical should only work for the first
3084 // comparison and must be disabled afterward.
3085 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
3089 // CMergeDoc::Rescan fails if files do not exist on both sides
3090 // or the really arcane case that the temp files couldn't be created,
3091 // which is too obscure to bother reporting if you can't write to
3092 // your temp directory, doing nothing is graceful enough for that).
3093 ShowRescanError(nRescanResult, identical);
3094 GetParentFrame()->DestroyWindow();
3098 // Force repaint of location pane to update it in case we had some warning
3099 // dialog visible and it got painted before files were loaded
3100 if (m_pView[0][0] != nullptr)
3101 m_pView[0][0]->RepaintLocationPane();
3106 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex)
3110 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
3111 if (nPane < 0 || nPane >= m_nBuffers)
3114 if (nLineIndex == -1)
3116 // scroll to first diff
3117 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
3118 m_diffList.HasSignificantDiffs())
3120 int nDiff = m_diffList.FirstSignificantDiff();
3122 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
3123 m_pView[0][nPane]->SetActivePane();
3127 m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, false, nPane);
3130 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
3132 if (!PromptAndSaveIfNeeded(true))
3135 FileLocation fileloc[3];
3138 for (int pane = 0; pane < m_nBuffers; pane++)
3140 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3141 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3142 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3143 fileloc[pane].setPath(m_filePaths[pane]);
3145 std::copy_n(m_strDesc, m_nBuffers, strDesc);
3147 strDesc[nBuffer] = _T("");
3148 fileloc[nBuffer].setPath(path);
3149 fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3151 if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3152 MoveOnLoad(nBuffer, nLineIndex);
3156 * @brief Re-load a document.
3157 * This methods re-loads the file compare document. The re-loaded document is
3158 * one side of the file compare.
3159 * @param [in] index The document to re-load.
3160 * @return Open result code.
3162 void CMergeDoc::RefreshOptions()
3164 DIFFOPTIONS options = {0};
3166 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
3168 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3169 Options::DiffOptions::Load(GetOptionsMgr(), options);
3171 m_diffWrapper.SetOptions(&options);
3173 // Refresh view options
3174 ForEachView([](auto& pView) { pView->RefreshOptions(); });
3178 * @brief Write path and filename to headerbar
3179 * @note SetText() does not repaint unchanged text
3181 void CMergeDoc::UpdateHeaderPath(int pane)
3183 CMergeEditFrame *pf = GetParentFrame();
3184 ASSERT(pf != nullptr);
3186 bool bChanges = false;
3188 if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3189 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3191 sText = m_strDesc[pane];
3195 sText = m_filePaths[pane];
3196 if (m_pDirDoc != nullptr)
3198 m_pDirDoc->ApplyDisplayRoot(pane, sText);
3201 bChanges = m_ptBuf[pane]->IsModified();
3204 sText.insert(0, _T("* "));
3206 pf->GetHeaderInterface()->SetText(pane, sText);
3212 * @brief Paint differently the headerbar of the active view
3214 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3216 CMergeEditFrame *pf = GetParentFrame();
3217 ASSERT(pf != nullptr);
3218 pf->GetHeaderInterface()->SetActive(pane, bActivate);
3222 * @brief Set detect/not detect Moved Blocks
3224 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3226 if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3229 GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3230 m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3235 * @brief Check if given buffer has mixed EOL style.
3236 * @param [in] nBuffer Buffer to check.
3237 * @return true if buffer's EOL style is mixed, false otherwise.
3239 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3241 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3242 return pBuf->IsMixedEOL();
3245 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3247 m_bEditAfterRescan[nBuffer] = true;
3250 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3252 if (nBuffer >= 0 && nBuffer < m_nBuffers)
3253 return m_bEditAfterRescan[nBuffer];
3255 for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3257 if (m_bEditAfterRescan[nBuffer])
3265 * @brief Update document filenames to title
3267 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3269 PrediffingInfo infoPrediffer;
3270 GetPrediffer(&infoPrediffer);
3271 String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer);
3272 CDocument::SetTitle(sTitle.c_str());
3276 * @brief Update any resources necessary after a GUI language change
3278 void CMergeDoc::UpdateResources()
3280 if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3281 m_strDesc[0] = _("Untitled left");
3282 if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3283 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3284 if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3285 m_strDesc[1] = _("Untitled middle");
3286 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3287 UpdateHeaderPath(nBuffer);
3289 GetParentFrame()->UpdateResources();
3290 ForEachView([](auto& pView) { pView->UpdateResources(); });
3293 // Return current word breaking break type setting (whitespace only or include punctuation)
3294 bool CMergeDoc::GetBreakType() const
3296 bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3300 // Return true to do line diff colors at the byte level (false to do them at word level)
3301 bool CMergeDoc::GetByteColoringOption() const
3303 // color at byte level if 'break_on_words' option not set
3304 bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3308 /// Swap files and update views
3309 void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
3311 if ((nFromIndex >= 0 && nFromIndex < m_nBuffers) && (nToIndex >= 0 && nToIndex < m_nBuffers))
3314 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3316 int nLeftViewId = m_pView[nGroup][nFromIndex]->GetDlgCtrlID();
3317 int nRightViewId = m_pView[nGroup][nToIndex]->GetDlgCtrlID();
3318 m_pView[nGroup][nFromIndex]->SetDlgCtrlID(nRightViewId);
3319 m_pView[nGroup][nToIndex]->SetDlgCtrlID(nLeftViewId);
3323 // Swap buffers and so on
3324 std::swap(m_ptBuf[nFromIndex], m_ptBuf[nToIndex]);
3325 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3326 std::swap(m_pView[nGroup][nFromIndex], m_pView[nGroup][nToIndex]);
3327 std::swap(m_pSaveFileInfo[nFromIndex], m_pSaveFileInfo[nToIndex]);
3328 std::swap(m_pRescanFileInfo[nFromIndex], m_pRescanFileInfo[nToIndex]);
3329 std::swap(m_nBufferType[nFromIndex], m_nBufferType[nToIndex]);
3330 std::swap(m_bEditAfterRescan[nFromIndex], m_bEditAfterRescan[nToIndex]);
3331 std::swap(m_strDesc[nFromIndex], m_strDesc[nToIndex]);
3333 m_filePaths.Swap(nFromIndex, nToIndex);
3334 m_diffList.Swap(nFromIndex, nToIndex);
3335 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3336 swap(m_pView[nGroup][nFromIndex]->m_piMergeEditStatus, m_pView[nGroup][nToIndex]->m_piMergeEditStatus);
3338 ClearWordDiffCache();
3340 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3342 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3343 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3344 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3347 UpdateHeaderPath(nBuffer);
3349 GetParentFrame()->UpdateSplitter();
3350 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3352 UpdateAllViews(nullptr);
3357 * @brief Reloads the opened files
3359 void CMergeDoc::OnFileReload()
3361 if (!PromptAndSaveIfNeeded(true))
3364 FileLocation fileloc[3];
3366 for (int pane = 0; pane < m_nBuffers; pane++)
3368 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3369 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3370 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3371 fileloc[pane].setPath(m_filePaths[pane]);
3373 CPoint pt = GetActiveMergeView()->GetCursorPos();
3374 if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3375 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3379 * @brief Display encodings to user
3381 void CMergeDoc::OnFileEncoding()
3383 DoFileEncodingDialog();
3386 void CMergeDoc::OnOpenWithUnpacker()
3388 CSelectPluginDlg dlg(m_infoUnpacker.GetPluginPipeline(),
3389 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")), true, false);
3390 if (dlg.DoModal() != IDOK)
3393 if (!PromptAndSaveIfNeeded(true))
3396 PackingInfo infoUnpacker(dlg.GetPluginPipeline());
3397 PathContext paths = m_filePaths;
3398 DWORD dwFlags[3] = { FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU };
3399 String strDesc[3] = { m_strDesc[0], m_strDesc[1], m_strDesc[2] };
3400 int nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3402 if (GetMainFrame()->DoFileOpen(nID, &paths, dwFlags, strDesc, _T(""), &infoUnpacker))
3403 GetParentFrame()->DestroyWindow();
3406 void CMergeDoc::OnApplyPrediffer()
3408 PrediffingInfo prediffer;
3409 GetPrediffer(&prediffer);
3410 // let the user choose a handler
3411 CSelectPluginDlg dlg(prediffer.GetPluginPipeline(),
3412 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")), false, false);
3413 if (dlg.DoModal() != IDOK)
3415 prediffer.SetPluginPipeline(dlg.GetPluginPipeline());
3416 SetPrediffer(&prediffer);
3417 FlushAndRescan(true);
3421 void CMergeDoc::OnBnClickedFileEncoding()
3423 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3425 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3426 DoFileEncodingDialog();
3429 void CMergeDoc::OnBnClickedPlugin()
3431 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3433 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3434 OnOpenWithUnpacker();
3437 void CMergeDoc::OnBnClickedHexView()
3439 OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3442 void CMergeDoc::OnOK()
3444 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3446 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3449 void CMergeDoc::OnFileRecompareAsText()
3451 m_bEnableTableEditing = false;
3455 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3457 pCmdUI->Enable(m_ptBuf[0]->GetTableEditing());
3460 void CMergeDoc::OnFileRecompareAsTable()
3462 m_bEnableTableEditing = true;
3466 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3468 pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3471 void CMergeDoc::OnFileRecompareAs(UINT nID)
3473 if (!PromptAndSaveIfNeeded(true))
3476 DWORD dwFlags[3] = { 0 };
3477 FileLocation fileloc[3];
3479 int nBuffers = m_nBuffers;
3480 CDirDoc *pDirDoc = m_pDirDoc->GetMainView() ? m_pDirDoc :
3481 static_cast<CDirDoc*>(theApp.m_pDirTemplate->CreateNewDocument());
3482 PackingInfo infoUnpacker(m_infoUnpacker.GetPluginPipeline());
3484 for (int pane = 0; pane < m_nBuffers; pane++)
3486 fileloc[pane].setPath(m_filePaths[pane]);
3487 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3488 strDesc[pane] = m_strDesc[pane];
3490 if (ID_UNPACKERS_FIRST <= nID && nID <= ID_UNPACKERS_LAST)
3492 infoUnpacker.SetPluginPipeline(CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST));
3493 nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3496 if (GetMainFrame()->ShowMergeDoc(nID, pDirDoc, nBuffers, fileloc, dwFlags, strDesc, _T(""), &infoUnpacker))
3497 GetParentFrame()->DestroyWindow();
3500 // Return file extension either from file name
3501 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3504 paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3509 * @brief Generate report from file compare results.
3511 bool CMergeDoc::GenerateReport(const String& sFileName) const
3513 // calculate HTML font size
3516 dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3517 m_pView[0][0]->GetFont(lf);
3518 int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3520 // create HTML report
3522 if (!file.Open(sFileName, _T("wt")))
3524 String errMsg = GetSysError(GetLastError());
3525 String msg = strutils::format_string1(
3526 _("Error creating the report:\n%1"), errMsg);
3527 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3531 file.SetCodepage(ucr::CP_UTF_8);
3533 CString headerText =
3534 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3535 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3538 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3539 _T("<title>WinMerge File Compare Report</title>\n")
3540 _T("<style type=\"text/css\">\n")
3542 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3543 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3544 _T("tr { vertical-align: top; }\n")
3545 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3551 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3555 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3556 file.WriteString(header);
3559 // If archive, use archive path + folder + filename inside archive
3560 // If desc text given, use it
3561 PathContext paths = m_filePaths;
3562 if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3564 for (int i = 0; i < paths.GetSize(); i++)
3565 m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3569 for (int i = 0; i < paths.GetSize(); i++)
3571 if (!m_strDesc[i].empty())
3572 paths[i] = m_strDesc[i];
3578 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3580 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3581 (double)100 / m_nBuffers);
3582 file.WriteString(data);
3583 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3584 file.WriteString(_T("</th>\n"));
3591 // write the body of the report
3593 int nLineCount[3] = {0};
3595 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3596 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3600 file.WriteString(_T("<tr>\n"));
3601 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3603 for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3605 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3609 if (idx[nBuffer] < nLineCount[nBuffer])
3612 int iVisibleLineNumber = 0;
3613 String tdtag = _T("<td class=\"ln\">");
3614 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3615 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3617 iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3620 (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3621 (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3624 if (iVisibleLineNumber > 0)
3626 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3627 iVisibleLineNumber = 0;
3630 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3632 if (iVisibleLineNumber > 0)
3633 tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3635 tdtag += _T("</td>");
3636 file.WriteString(tdtag);
3638 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3642 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3643 file.WriteString(_T("\n"));
3645 file.WriteString(_T("</tr>\n"));
3647 bool bBorderLine = false;
3648 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3650 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3656 file.WriteString(_T("<tr height=1>"));
3657 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3659 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3660 file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3662 file.WriteString(_T("<td></td><td></td>"));
3664 file.WriteString(_T("</tr>\n"));
3667 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3682 * @brief Generate report from file compare results.
3684 void CMergeDoc::OnToolsGenerateReport()
3689 if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3692 if (GenerateReport(s.c_str()))
3693 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3697 * @brief Generate patch from files selected.
3699 * Creates a patch from selected files in active directory compare, or
3700 * active file compare. Files in file compare must be saved before
3703 void CMergeDoc::OnToolsGeneratePatch()
3705 // If there are changes in files, tell user to save them first
3708 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3713 patcher.AddFiles(m_filePaths.GetLeft(),
3714 m_filePaths.GetRight());
3715 patcher.CreatePatch();
3719 * @brief Add synchronization point
3721 void CMergeDoc::AddSyncPoint()
3724 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3726 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3727 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3730 // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
3731 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3732 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
3734 LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
3738 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3739 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3740 DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3742 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3743 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3745 m_bHasSyncPoints = true;
3747 ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3749 FlushAndRescan(true);
3753 * @brief Delete a synchronization point
3755 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3757 const auto syncpoints = GetSyncPointList();
3758 for (auto syncpnt : syncpoints)
3760 if (syncpnt[pane] == nLine)
3762 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3763 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3767 if (syncpoints.size() == 1)
3768 m_bHasSyncPoints = false;
3771 FlushAndRescan(true);
3776 * @brief Clear Synchronization points
3778 void CMergeDoc::ClearSyncPoints()
3780 if (!m_bHasSyncPoints)
3783 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3785 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3786 for (int nLine = 0; nLine < nLineCount; ++nLine)
3788 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3789 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3793 m_bHasSyncPoints = false;
3795 FlushAndRescan(true);
3798 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3800 std::vector<std::vector<int> > list;
3801 if (!m_bHasSyncPoints)
3803 int idx[3] = {-1, -1, -1};
3804 std::vector<int> points(m_nBuffers);
3805 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3806 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3807 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3809 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3810 for (int nLine = 0; nLine < nLineCount; ++nLine)
3812 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3815 if (static_cast<int>(list.size()) <= idx[nBuffer])
3816 list.push_back(points);
3817 list[idx[nBuffer]][nBuffer] = nLine;