2 * @file DiffTextBuffer.cpp
4 * @brief Implementation file for CDiffTextBuffer
7 // ID line follows -- this is updated by SVN
15 #include "coretools.h"
17 #include "OptionsDef.h"
18 #include "Environment.h"
19 #include "MergeLineFlags.h"
21 #include "FileTransform.h"
22 #include "FileTextEncoding.h"
23 #include "DiffTextBuffer.h"
28 static char THIS_FILE[] = __FILE__;
31 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats);
32 static CString GetLineByteTimeReport(UINT lines, __int64 bytes,
33 const COleDateTime & start);
34 static void EscapeControlChars(CString &s);
35 static LPCTSTR GetEol(const CString &str);
36 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats);
39 * @brief Check if file has only one EOL type.
40 * @param [in] stats File's text stats.
41 * @return true if only one EOL type is found, false otherwise.
43 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats)
46 nType += (stats.ncrlfs > 0);
47 nType += (stats.ncrs > 0);
48 nType += (stats.nlfs > 0);
53 * @brief Return a string giving #lines and #bytes and how much time elapsed.
54 * @param [in] lines Count of lines.
55 * @param [in] bytes Count of bytes.
56 * @param [in] start Time used.
57 * @return Formatted string.
59 static CString GetLineByteTimeReport(UINT lines, __int64 bytes,
60 const COleDateTime & start)
62 String sLines = locality::NumToLocaleStr((int)lines);
63 String sBytes = locality::NumToLocaleStr(bytes);
64 COleDateTimeSpan duration = COleDateTime::GetCurrentTime() - start;
65 String sMinutes = locality::NumToLocaleStr((int)duration.GetTotalMinutes());
67 str.Format(_T("%s lines (%s byte) saved in %sm%02ds")
68 , sLines.c_str(), sBytes.c_str(), sMinutes.c_str()
69 , duration.GetSeconds()
75 * @brief Escape control characters.
76 * @param [in,out] s Line of text excluding eol chars.
78 * @note Escape sequences follow the pattern
79 * (leadin character, high nibble, low nibble, leadout character).
80 * The leadin character is '\x0F'. The leadout character is a backslash.
82 static void EscapeControlChars(CString &s)
84 // Compute buffer length required for escaping
85 int n = s.GetLength();
91 // Is it a control character in the range 0..31 except TAB?
92 if (!(c & ~_T('\x1F')) && c != _T('\t'))
94 n += 3; // Need 3 extra characters to escape
97 // Reallocate accordingly
99 LPTSTR p = s.GetBufferSetLength(n);
100 // Copy/translate characters starting at end of string
104 // Is it a control character in the range 0..31 except TAB?
105 if (!(c & ~_T('\x1F')) && c != _T('\t'))
107 // Bitwise OR with 0x100 so _itot() will output 3 hex digits
108 _itot(0x100 | c, p + n - 4, 16);
109 // Replace terminating zero with leadout character
111 // Prepare to replace 1st hex digit with leadin character
120 * @brief Get EOL of the string.
121 * This function returns a pointer to the EOL chars in the given string.
122 * Behavior is similar to CCrystalTextBuffer::GetLineEol().
123 * @param [in] str String whose EOL chars are returned.
124 * @return Pointer to string's EOL chars, or empty string if no EOL found.
126 static LPCTSTR GetEol(const CString &str)
128 if (str.GetLength()>1 && str[str.GetLength()-2]=='\r' && str[str.GetLength()-1]=='\n')
129 return (LPCTSTR)str + str.GetLength()-2;
130 if (str.GetLength()>0 && (str[str.GetLength()-1]=='\r' || str[str.GetLength()-1]=='\n'))
131 return (LPCTSTR)str + str.GetLength()-1;
136 * @brief Get file's EOL type.
137 * @param [in] stats File's text stats.
140 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats)
142 if (stats.ncrlfs >= stats.nlfs)
144 if (stats.ncrlfs >= stats.ncrs)
145 return CRLF_STYLE_DOS;
147 return CRLF_STYLE_MAC;
151 if (stats.nlfs >= stats.ncrs)
152 return CRLF_STYLE_UNIX;
154 return CRLF_STYLE_MAC;
159 * @brief Constructor.
160 * @param [in] pDoc Owning CMergeDoc.
161 * @param [in] pane Pane number this buffer is associated with.
163 CDiffTextBuffer::CDiffTextBuffer(CMergeDoc * pDoc, int pane)
166 , m_unpackerSubcode(0)
172 * @brief Get a line from the buffer.
173 * @param [in] nLineIndex Index of the line to get.
174 * @param [out] strLine Returns line text in the index.
176 BOOL CDiffTextBuffer::GetLine(int nLineIndex, CString &strLine)
178 int nLineLength = CCrystalTextBuffer::GetLineLength(nLineIndex);
181 else if (nLineLength == 0)
185 _tcsncpy(strLine.GetBuffer(nLineLength + 1),
186 CCrystalTextBuffer::GetLineChars(nLineIndex), nLineLength);
187 strLine.ReleaseBuffer(nLineLength);
193 * @brief Set the buffer modified status.
194 * @param [in] bModified New modified status, TRUE if buffer has been
195 * modified since last saving.
197 void CDiffTextBuffer::SetModified(BOOL bModified /*= TRUE*/)
199 CCrystalTextBuffer::SetModified (bModified);
200 m_pOwnerDoc->SetModifiedFlag (bModified);
204 * @brief Get a line (with EOL bytes) from the buffer.
205 * This function is like GetLine() but it also includes line's EOL to the
207 * @param [in] nLineIndex Index of the line to get.
208 * @param [out] strLine Returns line text in the index. Existing content
209 * of this string is overwritten.
211 BOOL CDiffTextBuffer::GetFullLine(int nLineIndex, CString &strLine)
213 int cchText = GetFullLineLength(nLineIndex);
219 LPTSTR pchText = strLine.GetBufferSetLength(cchText);
220 memcpy(pchText, GetLineChars(nLineIndex), cchText * sizeof(TCHAR));
224 void CDiffTextBuffer::AddUndoRecord(BOOL bInsert, const CPoint & ptStartPos,
225 const CPoint & ptEndPos, LPCTSTR pszText, int cchText,
226 int nLinesToValidate, int nActionType /*= CE_ACTION_UNKNOWN*/,
227 CDWordArray *paSavedRevisonNumbers)
229 CGhostTextBuffer::AddUndoRecord(bInsert, ptStartPos, ptEndPos, pszText,
230 cchText, nLinesToValidate, nActionType, paSavedRevisonNumbers);
231 if (m_aUndoBuf[m_nUndoPosition - 1].m_dwFlags & UNDO_BEGINGROUP)
233 m_pOwnerDoc->undoTgt.erase(m_pOwnerDoc->curUndo, m_pOwnerDoc->undoTgt.end());
234 m_pOwnerDoc->undoTgt.push_back(m_pOwnerDoc->GetView(m_nThisPane));
235 m_pOwnerDoc->curUndo = m_pOwnerDoc->undoTgt.end();
239 * @brief Checks if a flag is set for line.
240 * @param [in] line Index (0-based) for line.
241 * @param [in] flag Flag to check.
242 * @return TRUE if flag is set, FALSE otherwise.
244 BOOL CDiffTextBuffer::FlagIsSet(UINT line, DWORD flag)
246 return ((m_aLines[line].m_dwFlags & flag) == flag);
250 Remove blank lines and clear winmerge flags
251 (2003-06-21, Perry: I don't understand why this is necessary, but if this isn't
252 done, more and more gray lines appear in the file)
253 (2003-07-31, Laoran I don't understand either why it is necessary, but it works
254 fine, so let's go on with it)
256 void CDiffTextBuffer::prepareForRescan()
258 RemoveAllGhostLines();
259 for (int ct = GetLineCount() - 1; ct >= 0; --ct)
261 SetLineFlag(ct, LF_DIFF, FALSE, FALSE, FALSE);
262 SetLineFlag(ct, LF_TRIVIAL, FALSE, FALSE, FALSE);
263 SetLineFlag(ct, LF_MOVED, FALSE, FALSE, FALSE);
268 * @brief Called when line has been edited.
269 * After editing a line, we don't know if there is a diff or not.
270 * So we clear the LF_DIFF flag (and it is more easy to read during edition).
271 * Rescan will set the proper color.
272 * @param [in] nLine Line that has been edited.
275 void CDiffTextBuffer::OnNotifyLineHasBeenEdited(int nLine)
277 SetLineFlag(nLine, LF_DIFF, FALSE, FALSE, FALSE);
278 SetLineFlag(nLine, LF_TRIVIAL, FALSE, FALSE, FALSE);
279 SetLineFlag(nLine, LF_MOVED, FALSE, FALSE, FALSE);
280 CGhostTextBuffer::OnNotifyLineHasBeenEdited(nLine);
284 * @brief Set the folder for temp files.
285 * @param [in] path Temp files folder.
287 void CDiffTextBuffer::SetTempPath(const String &path)
289 m_strTempPath = path;
293 * @brief Is the buffer initialized?
294 * @return TRUE if the buffer is initialized, FALSE otherwise.
296 bool CDiffTextBuffer::IsInitialized() const
302 * @brief Load file from disk into buffer
304 * @param [in] pszFileNameInit File to load
305 * @param [in] infoUnpacker Unpacker plugin
306 * @param [in] sToFindUnpacker String for finding unpacker plugin
307 * @param [out] readOnly Loading was lossy so file should be read-only
308 * @param [in] nCrlfStyle EOL style used
309 * @param [in] encoding Encoding used
310 * @param [out] sError Error message returned
311 * @return FRESULT_OK when loading succeed or (list in files.h):
312 * - FRESULT_OK_IMPURE : load OK, but the EOL are of different types
313 * - FRESULT_ERROR_UNPACK : plugin failed to unpack
314 * - FRESULT_ERROR : loading failed, sError contains error message
315 * - FRESULT_BINARY : file is binary file
316 * @note If this method fails, it calls InitNew so the CDiffTextBuffer is in a valid state
318 int CDiffTextBuffer::LoadFromFile(LPCTSTR pszFileNameInit,
319 PackingInfo * infoUnpacker, LPCTSTR sToFindUnpacker, BOOL & readOnly,
320 CRLFSTYLE nCrlfStyle, const FileTextEncoding & encoding, CString &sError)
323 ASSERT(m_aLines.size() == 0);
325 // Unpacking the file here, save the result in a temporary file
326 String sFileName(pszFileNameInit);
327 if (infoUnpacker->bToBeScanned)
329 if (!FileTransform_Unpacking(sFileName, sToFindUnpacker, infoUnpacker,
332 InitNew(); // leave crystal editor in valid, empty state
333 return FileLoadResult::FRESULT_ERROR_UNPACK;
338 if (!FileTransform_Unpacking(sFileName, infoUnpacker, &m_unpackerSubcode))
340 InitNew(); // leave crystal editor in valid, empty state
341 return FileLoadResult::FRESULT_ERROR_UNPACK;
344 // we use the same unpacker for both files, so it must be defined after first file
345 ASSERT(infoUnpacker->bToBeScanned != PLUGIN_AUTO);
346 // we will load the transformed file
347 LPCTSTR pszFileName = sFileName.c_str();
350 DWORD nRetVal = FileLoadResult::FRESULT_OK;
352 // Set encoding based on extension, if we know one
353 SplitFilename(pszFileName, NULL, NULL, &sExt);
354 CCrystalTextView::TextDefinition *def =
355 CCrystalTextView::GetTextType(sExt.c_str());
356 if (def && def->encoding != -1)
357 m_nSourceEncoding = def->encoding;
359 UniFile *pufile = infoUnpacker->pufile;
361 pufile = new UniMemFile;
363 // Now we only use the UniFile interface
364 // which is something we could implement for HTTP and/or FTP files
366 if (!pufile->OpenReadOnly(pszFileName))
368 nRetVal = FileLoadResult::FRESULT_ERROR;
369 UniFile::UniError uniErr = pufile->GetLastUniError();
370 if (uniErr.HasError())
372 sError = uniErr.GetError().c_str();
374 InitNew(); // leave crystal editor in valid, empty state
375 goto LoadFromFileExit;
379 // If the file is not unicode file, use the codepage we were given to
380 // interpret the 8-bit characters. If the file is unicode file,
381 // determine its type (IsUnicode() does that).
382 if (encoding.m_unicoding == ucr::NONE || !pufile->IsUnicode())
383 pufile->SetCodepage(encoding.m_codepage);
388 UINT next_line_report = 100; // for trace messages
389 UINT next_line_multiple = 5; // for trace messages
390 COleDateTime start = COleDateTime::GetCurrentTime(); // for trace messages
392 // Manually grow line array exponentially
393 UINT arraysize = 500;
394 m_aLines.resize(arraysize);
396 // preveol must be initialized for empty files
401 done = !pufile->ReadString(sline, eol, &lossy);
403 // if last line had no eol, we can quit
404 if (done && preveol.empty())
406 // but if last line had eol, we add an extra (empty) line to buffer
409 if (lineno == arraysize)
411 // For smaller sizes use exponential growth, but for larger
412 // sizes grow by constant ratio. Unlimited exponential growth
413 // easily runs out of memory.
414 if (arraysize < 100 * 1024)
417 arraysize += 100 * 1024;
418 m_aLines.resize(arraysize);
421 sline += eol; // TODO: opportunity for optimization, as CString append is terrible
424 // TODO: Should record lossy status of line
426 AppendLine(lineno, sline.c_str(), sline.length());
431 // send occasional line counts to trace
432 // (at 100, 500, 1000, 5000, etc)
433 if (lineno == next_line_report)
435 __int64 dwBytesRead = pufile->GetPosition();
436 COleDateTimeSpan duration = COleDateTime::GetCurrentTime() - start;
437 if (duration.GetTotalMinutes() > 0)
439 CString strace = GetLineByteTimeReport(lineno, dwBytesRead, start);
440 TRACE(_T("%s\n"), (LPCTSTR)strace);
442 next_line_report = next_line_multiple * next_line_report;
443 next_line_multiple = (next_line_multiple == 5) ? 2 : 5;
449 // Send report of duration to trace (if it took a while)
450 COleDateTime end = COleDateTime::GetCurrentTime();
451 COleDateTimeSpan duration = end - start;
452 if (duration.GetTotalMinutes() > 0)
454 __int64 dwBytesRead = pufile->GetPosition();
455 CString strace = GetLineByteTimeReport(lineno, dwBytesRead, start);
456 TRACE(_T("%s\n"), (LPCTSTR)strace);
460 // fix array size (due to our manual exponential growth
461 m_aLines.resize(lineno);
464 //Try to determine current CRLF mode (most frequent)
465 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC)
467 nCrlfStyle = GetTextFileStyle(pufile->GetTxtStats());
469 ASSERT(nCrlfStyle >= 0 && nCrlfStyle <= 2);
470 SetCRLFMode(nCrlfStyle);
472 // At least one empty line must present
473 // (view does not work for empty buffers)
474 ASSERT(m_aLines.size() > 0);
478 m_bUndoGroup = m_bUndoBeginGroup = FALSE;
479 m_nSyncPosition = m_nUndoPosition = 0;
480 ASSERT(m_aUndoBuf.size() == 0);
481 m_ptLastChange.x = m_ptLastChange.y = -1;
484 // flags don't need initialization because 0 is the default value
486 // Set the return value : OK + info if the file is impure
487 // A pure file is a file where EOL are consistent (all DOS, or all UNIX, or all MAC)
488 // An impure file is a file with several EOL types
489 // WinMerge may display impure files, but the default option is to unify the EOL
490 // We return this info to the caller, so it may display a confirmation box
491 if (IsTextFileStylePure(pufile->GetTxtStats()))
492 nRetVal = FileLoadResult::FRESULT_OK;
494 nRetVal = FileLoadResult::FRESULT_OK_IMPURE;
496 // stash original encoding away
497 m_encoding.m_unicoding = pufile->GetUnicoding();
498 m_encoding.m_bom = pufile->HasBom();
499 m_encoding.m_codepage = pufile->GetCodepage();
501 if (pufile->GetTxtStats().nlosses)
503 FileLoadResult::AddModifier(nRetVal, FileLoadResult::FRESULT_LOSSY);
509 // close the file now to free the handle
513 // delete the file that unpacking may have created
514 if (_tcscmp(pszFileNameInit, pszFileName) != 0)
515 if (!::DeleteFile(pszFileName))
517 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
518 pszFileName, GetSysError(GetLastError())));
525 * @brief Saves file from buffer to disk
527 * @param bTempFile : FALSE if we are saving user files and
528 * TRUE if we are saving workin-temp-files for diff-engine
530 * @return SAVE_DONE or an error code (list in MergeDoc.h)
532 int CDiffTextBuffer::SaveToFile (LPCTSTR pszFileName,
533 BOOL bTempFile, String & sError, PackingInfo * infoUnpacker /*= NULL*/,
534 CRLFSTYLE nCrlfStyle /*= CRLF_STYLE_AUTOMATIC*/,
535 BOOL bClearModifiedFlag /*= TRUE*/ )
539 if (!pszFileName || _tcslen(pszFileName) == 0)
540 return SAVE_FAILED; // No filename, cannot save...
542 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC &&
543 !GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) ||
544 infoUnpacker && infoUnpacker->disallowMixedEOL)
546 // get the default nCrlfStyle of the CDiffTextBuffer
547 nCrlfStyle = GetCRLFMode();
548 ASSERT(nCrlfStyle >= 0 && nCrlfStyle <= 2);
551 BOOL bOpenSuccess = TRUE;
552 BOOL bSaveSuccess = FALSE;
555 file.SetUnicoding(m_encoding.m_unicoding);
556 file.SetBom(m_encoding.m_bom);
557 file.SetCodepage(m_encoding.m_codepage);
559 String sIntermediateFilename; // used when !bTempFile
563 bOpenSuccess = !!file.OpenCreate(pszFileName);
567 sIntermediateFilename = env_GetTempFileName(m_strTempPath.c_str(),
569 if (sIntermediateFilename.empty())
570 return SAVE_FAILED; //Nothing to do if even tempfile name fails
571 bOpenSuccess = !!file.OpenCreate(sIntermediateFilename.c_str());
576 UniFile::UniError uniErr = file.GetLastUniError();
577 if (uniErr.HasError())
579 sError = uniErr.GetError().c_str();
581 LogErrorString(Fmt(_T("Opening file %s failed: %s"),
582 pszFileName, sError));
584 LogErrorString(Fmt(_T("Opening file %s failed: %s"),
585 sIntermediateFilename, sError));
592 // line loop : get each real line and write it in the file
594 CString sEol = GetStringEol(nCrlfStyle);
595 int nLineCount = m_aLines.size();
596 for (int line = 0; line < nLineCount; ++line)
598 if (GetLineFlags(line) & LF_GHOST)
601 // get the characters of the line (excluding EOL)
602 if (GetLineLength(line) > 0)
603 GetText(line, 0, line, GetLineLength(line), sLine, 0);
608 EscapeControlChars(sLine);
610 if (line == ApparentLastRealLine())
612 // last real line is never EOL terminated
613 ASSERT (_tcslen(GetLineEol(line)) == 0);
614 // write the line and exit loop
615 String tmpLine(sLine);
616 file.WriteString(tmpLine);
620 // normal real line : append an EOL
621 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC)
623 // either the EOL of the line (when preserve original EOL chars is on)
624 sLine += GetLineEol(line);
628 // or the default EOL for this file
632 // write this line to the file (codeset or unicode conversions are done there)
633 String tmpLine(sLine);
634 file.WriteString(tmpLine);
640 // If we are saving user files
641 // we need an unpacker/packer, at least a "do nothing" one
642 ASSERT(infoUnpacker != NULL);
643 // repack the file here, overwrite the temporary file we did save in
644 String csTempFileName = sIntermediateFilename;
645 infoUnpacker->subcode = m_unpackerSubcode;
646 if (!FileTransform_Packing(csTempFileName, *infoUnpacker))
648 if (!::DeleteFile(sIntermediateFilename.c_str()))
650 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
651 sIntermediateFilename.c_str(), GetSysError(GetLastError())));
653 // returns now, don't overwrite the original file
654 return SAVE_PACK_FAILED;
656 // the temp filename may have changed during packing
657 if (csTempFileName != sIntermediateFilename)
659 if (!::DeleteFile(sIntermediateFilename.c_str()))
661 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
662 sIntermediateFilename.c_str(), GetSysError(GetLastError())));
664 sIntermediateFilename = csTempFileName;
667 // Write tempfile over original file
668 if (::CopyFile(sIntermediateFilename.c_str(), pszFileName, FALSE))
670 if (!::DeleteFile(sIntermediateFilename.c_str()))
672 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
673 sIntermediateFilename.c_str(), GetSysError(GetLastError())));
675 if (bClearModifiedFlag)
678 m_nSyncPosition = m_nUndoPosition;
682 // remember revision number on save
683 m_dwRevisionNumberOnSave = m_dwCurrentRevisionNumber;
685 // redraw line revision marks
686 UpdateViews (NULL, NULL, UPDATE_FLAGSONLY);
690 sError = GetSysError(GetLastError());
691 LogErrorString(Fmt(_T("CopyFile(%s, %s) failed: %s"),
692 sIntermediateFilename.c_str(), pszFileName, sError));
697 if (bClearModifiedFlag)
700 m_nSyncPosition = m_nUndoPosition;
712 * @brief Replace a line with new text.
713 * This function replaces line's text without changing the EOL style/bytes
715 * @param [in] pSource Editor view where text is changed.
716 * @param [in] nLine Index of the line to change.
717 * @param [in] pchText New text of the line.
718 * @param [in] cchText New length of the line (not inc. EOL bytes).
719 * @param [in] nAction Edit action to use.
721 void CDiffTextBuffer::ReplaceLine(CCrystalTextView * pSource, int nLine,
722 LPCTSTR pchText, int cchText, int nAction /*=CE_ACTION_UNKNOWN*/)
724 if (GetLineLength(nLine)>0)
725 DeleteText(pSource, nLine, 0, nLine, GetLineLength(nLine), nAction);
728 InsertText(pSource, nLine, 0, pchText, cchText, endl, endc, nAction);
731 /// Replace line (removing any eol, and only including one if in strText)
733 * @brief Replace a line with new text.
734 * This function replaces line's text including EOL bytes. If the @p strText
735 * does not include EOL bytes, the "line" does not get EOL bytes.
736 * @param [in] pSource Editor view where text is changed.
737 * @param [in] nLine Index of the line to change.
738 * @param [in] pchText New text of the line.
739 * @param [in] cchText New length of the line (not inc. EOL bytes).
740 * @param [in] nAction Edit action to use.
742 void CDiffTextBuffer::ReplaceFullLine(CCrystalTextView * pSource, int nLine,
743 const CString &strText, int nAction /*=CE_ACTION_UNKNOWN*/)
745 LPCTSTR eol = GetEol(strText);
746 if (_tcscmp(GetLineEol(nLine), eol) == 0)
748 // (optimization) eols are the same, so just replace text inside line
749 // we must clean strText from its eol...
750 int eolLength = _tcslen(eol);
751 ReplaceLine(pSource, nLine, strText, strText.GetLength() - eolLength, nAction);
755 // we may need a last line as the DeleteText end is (x=0,y=line+1)
756 if (nLine + 1 == GetLineCount())
757 InsertGhostLine (pSource, GetLineCount());
759 if (GetFullLineLength(nLine))
760 DeleteText(pSource, nLine, 0, nLine + 1, 0, nAction);
762 const int cchText = strText.GetLength();
764 InsertText(pSource, nLine, 0, strText, cchText, endl, endc, nAction);
767 bool CDiffTextBuffer::curUndoGroup()
769 return (m_aUndoBuf.size() != 0 && m_aUndoBuf[0].m_dwFlags&UNDO_BEGINGROUP);