2 * @file DiffTextBuffer.cpp
4 * @brief Implementation file for CDiffTextBuffer
9 #include "DiffTextBuffer.h"
11 #include <Poco/Exception.h>
17 #include "OptionsDef.h"
18 #include "OptionsMgr.h"
19 #include "Environment.h"
20 #include "MergeLineFlags.h"
22 #include "FileTransform.h"
23 #include "FileTextEncoding.h"
24 #include "codepage_detect.h"
27 using Poco::Exception;
33 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats);
34 static void EscapeControlChars(String &s);
35 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats);
38 * @brief Check if file has only one EOL type.
39 * @param [in] stats File's text stats.
40 * @return true if only one EOL type is found, false otherwise.
42 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats)
55 * @brief Escape control characters.
56 * @param [in,out] s Line of text excluding eol chars.
58 * @note Escape sequences follow the pattern
59 * (leadin character, high nibble, low nibble, leadout character).
60 * The leadin character is '\x0F'. The leadout character is a backslash.
62 static void EscapeControlChars(String &s)
64 // Compute buffer length required for escaping
65 size_t n = s.length();
66 LPCTSTR q = s.c_str();
71 // Is it a control character in the range 0..31 except TAB?
72 if (!(c & ~_T('\x1F')) && c != _T('\t'))
74 n += 3; // Need 3 extra characters to escape
77 // Reallocate accordingly
82 // Copy/translate characters starting at end of string
86 // Is it a control character in the range 0..31 except TAB?
87 if (!(c & ~_T('\x1F')) && c != _T('\t'))
89 // Bitwise OR with 0x100 so _itot() will output 3 hex digits
90 _itot(0x100 | c, p + n - 4, 16);
91 // Replace terminating zero with leadout character
93 // Prepare to replace 1st hex digit with leadin character
99 s.resize(s.length() - 1);
103 * @brief Get file's EOL type.
104 * @param [in] stats File's text stats.
107 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats)
109 // Check if file has more than one EOL type.
110 if (!IsTextFileStylePure(stats))
111 return CRLF_STYLE_MIXED;
112 else if (stats.ncrlfs >= stats.nlfs)
114 if (stats.ncrlfs >= stats.ncrs)
115 return CRLF_STYLE_DOS;
117 return CRLF_STYLE_MAC;
121 if (stats.nlfs >= stats.ncrs)
122 return CRLF_STYLE_UNIX;
124 return CRLF_STYLE_MAC;
129 * @brief Constructor.
130 * @param [in] pDoc Owning CMergeDoc.
131 * @param [in] pane Pane number this buffer is associated with.
133 CDiffTextBuffer::CDiffTextBuffer(CMergeDoc * pDoc, int pane)
136 , m_unpackerSubcode(0)
142 * @brief Get a line from the buffer.
143 * @param [in] nLineIndex Index of the line to get.
144 * @param [out] strLine Returns line text in the index.
146 bool CDiffTextBuffer::GetLine(int nLineIndex, CString &strLine) const
148 int nLineLength = CCrystalTextBuffer::GetLineLength(nLineIndex);
151 else if (nLineLength == 0)
155 _tcsncpy(strLine.GetBuffer(nLineLength + 1),
156 CCrystalTextBuffer::GetLineChars(nLineIndex), nLineLength);
157 strLine.ReleaseBuffer(nLineLength);
163 * @brief Set the buffer modified status.
164 * @param [in] bModified New modified status, true if buffer has been
165 * modified since last saving.
167 void CDiffTextBuffer::SetModified(bool bModified /*= true*/)
169 CCrystalTextBuffer::SetModified (bModified);
170 m_pOwnerDoc->SetModifiedFlag (bModified);
174 * @brief Get a line (with EOL bytes) from the buffer.
175 * This function is like GetLine() but it also includes line's EOL to the
177 * @param [in] nLineIndex Index of the line to get.
178 * @param [out] strLine Returns line text in the index. Existing content
179 * of this string is overwritten.
181 bool CDiffTextBuffer::GetFullLine(int nLineIndex, CString &strLine) const
183 int cchText = GetFullLineLength(nLineIndex);
189 LPTSTR pchText = strLine.GetBufferSetLength(cchText);
190 memcpy(pchText, GetLineChars(nLineIndex), cchText * sizeof(TCHAR));
194 void CDiffTextBuffer::AddUndoRecord(bool bInsert, const CPoint & ptStartPos,
195 const CPoint & ptEndPos, LPCTSTR pszText, int cchText,
196 int nActionType /*= CE_ACTION_UNKNOWN*/,
197 CDWordArray *paSavedRevisionNumbers)
199 CGhostTextBuffer::AddUndoRecord(bInsert, ptStartPos, ptEndPos, pszText,
200 cchText, nActionType, paSavedRevisionNumbers);
201 if (m_aUndoBuf[m_nUndoPosition - 1].m_dwFlags & UNDO_BEGINGROUP)
203 m_pOwnerDoc->undoTgt.erase(m_pOwnerDoc->curUndo, m_pOwnerDoc->undoTgt.end());
204 m_pOwnerDoc->undoTgt.push_back(m_pOwnerDoc->GetView(m_nThisPane));
205 m_pOwnerDoc->curUndo = m_pOwnerDoc->undoTgt.end();
209 * @brief Checks if a flag is set for line.
210 * @param [in] line Index (0-based) for line.
211 * @param [in] flag Flag to check.
212 * @return true if flag is set, false otherwise.
214 bool CDiffTextBuffer::FlagIsSet(UINT line, DWORD flag) const
216 return ((m_aLines[line].m_dwFlags & flag) == flag);
220 Remove blank lines and clear winmerge flags
221 (2003-06-21, Perry: I don't understand why this is necessary, but if this isn't
222 done, more and more gray lines appear in the file)
223 (2003-07-31, Laoran I don't understand either why it is necessary, but it works
224 fine, so let's go on with it)
226 void CDiffTextBuffer::prepareForRescan()
228 RemoveAllGhostLines();
229 for (int ct = GetLineCount() - 1; ct >= 0; --ct)
232 LF_INVISIBLE | LF_DIFF | LF_TRIVIAL | LF_MOVED | LF_SNP,
233 false, false, false);
238 * @brief Called when line has been edited.
239 * After editing a line, we don't know if there is a diff or not.
240 * So we clear the LF_DIFF flag (and it is more easy to read during edition).
241 * Rescan will set the proper color.
242 * @param [in] nLine Line that has been edited.
245 void CDiffTextBuffer::OnNotifyLineHasBeenEdited(int nLine)
247 SetLineFlag(nLine, LF_DIFF, false, false, false);
248 SetLineFlag(nLine, LF_TRIVIAL, false, false, false);
249 SetLineFlag(nLine, LF_MOVED, false, false, false);
250 SetLineFlag(nLine, LF_SNP, false, false, false);
251 CGhostTextBuffer::OnNotifyLineHasBeenEdited(nLine);
255 * @brief Set the folder for temp files.
256 * @param [in] path Temp files folder.
258 void CDiffTextBuffer::SetTempPath(const String &path)
260 m_strTempPath = path;
264 * @brief Is the buffer initialized?
265 * @return true if the buffer is initialized, false otherwise.
267 bool CDiffTextBuffer::IsInitialized() const
273 * @brief Load file from disk into buffer
275 * @param [in] pszFileNameInit File to load
276 * @param [in] infoUnpacker Unpacker plugin
277 * @param [in] sToFindUnpacker String for finding unpacker plugin
278 * @param [out] readOnly Loading was lossy so file should be read-only
279 * @param [in] nCrlfStyle EOL style used
280 * @param [in] encoding Encoding used
281 * @param [out] sError Error message returned
282 * @return FRESULT_OK when loading succeed or (list in files.h):
283 * - FRESULT_OK_IMPURE : load OK, but the EOL are of different types
284 * - FRESULT_ERROR_UNPACK : plugin failed to unpack
285 * - FRESULT_ERROR : loading failed, sError contains error message
286 * - FRESULT_BINARY : file is binary file
287 * @note If this method fails, it calls InitNew so the CDiffTextBuffer is in a valid state
289 int CDiffTextBuffer::LoadFromFile(LPCTSTR pszFileNameInit,
290 PackingInfo * infoUnpacker, LPCTSTR sToFindUnpacker, bool & readOnly,
291 CRLFSTYLE nCrlfStyle, const FileTextEncoding & encoding, CString &sError)
294 ASSERT(m_aLines.size() == 0);
296 // Unpacking the file here, save the result in a temporary file
297 String sFileName(pszFileNameInit);
298 if (!FileTransform_Unpacking(infoUnpacker, sFileName, sToFindUnpacker))
300 InitNew(); // leave crystal editor in valid, empty state
301 return FileLoadResult::FRESULT_ERROR_UNPACK;
303 m_unpackerSubcode = infoUnpacker->subcode;
305 // we use the same unpacker for both files, so it must be defined after first file
306 ASSERT(infoUnpacker->bToBeScanned != PLUGIN_AUTO);
307 // we will load the transformed file
308 LPCTSTR pszFileName = sFileName.c_str();
311 DWORD nRetVal = FileLoadResult::FRESULT_OK;
313 // Set encoding based on extension, if we know one
314 paths_SplitFilename(pszFileName, NULL, NULL, &sExt);
315 CCrystalTextView::TextDefinition *def =
316 CCrystalTextView::GetTextType(sExt.c_str());
317 if (def && def->encoding != -1)
318 m_nSourceEncoding = def->encoding;
320 UniFile *pufile = infoUnpacker->pufile;
322 pufile = new UniMemFile;
324 // Now we only use the UniFile interface
325 // which is something we could implement for HTTP and/or FTP files
327 if (!pufile->OpenReadOnly(pszFileName))
329 nRetVal = FileLoadResult::FRESULT_ERROR;
330 UniFile::UniError uniErr = pufile->GetLastUniError();
331 if (uniErr.HasError())
333 sError = uniErr.GetError().c_str();
335 InitNew(); // leave crystal editor in valid, empty state
336 goto LoadFromFileExit;
340 if (infoUnpacker->pluginName.length() > 0)
342 // re-detect codepage
343 int iGuessEncodingType = GetOptionsMgr()->GetInt(OPT_CP_DETECT);
344 FileTextEncoding encoding2 = GuessCodepageEncoding(pszFileName, iGuessEncodingType);
345 pufile->SetUnicoding(encoding2.m_unicoding);
346 pufile->SetCodepage(encoding2.m_codepage);
347 pufile->SetBom(encoding2.m_bom);
353 // If the file is not unicode file, use the codepage we were given to
354 // interpret the 8-bit characters. If the file is unicode file,
355 // determine its type (IsUnicode() does that).
356 if (encoding.m_unicoding == ucr::NONE || !pufile->IsUnicode())
357 pufile->SetCodepage(encoding.m_codepage);
363 COleDateTime start = COleDateTime::GetCurrentTime(); // for trace messages
365 // Manually grow line array exponentially
366 UINT arraysize = 500;
367 m_aLines.resize(arraysize);
369 // preveol must be initialized for empty files
374 done = !pufile->ReadString(sline, eol, &lossy);
376 // if last line had no eol, we can quit
377 if (done && preveol.empty())
379 // but if last line had eol, we add an extra (empty) line to buffer
382 if (lineno == arraysize)
384 // For smaller sizes use exponential growth, but for larger
385 // sizes grow by constant ratio. Unlimited exponential growth
386 // easily runs out of memory.
387 if (arraysize < 100 * 1024)
390 arraysize += 100 * 1024;
391 m_aLines.resize(arraysize);
394 sline += eol; // TODO: opportunity for optimization, as CString append is terrible
397 // TODO: Should record lossy status of line
399 AppendLine(lineno, sline.c_str(), static_cast<int>(sline.length()));
404 // fix array size (due to our manual exponential growth
405 m_aLines.resize(lineno);
408 //Try to determine current CRLF mode (most frequent)
409 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC)
411 nCrlfStyle = GetTextFileStyle(pufile->GetTxtStats());
413 ASSERT(nCrlfStyle >= 0 && nCrlfStyle <= 3);
414 SetCRLFMode(nCrlfStyle);
416 // At least one empty line must present
417 // (view does not work for empty buffers)
418 ASSERT(m_aLines.size() > 0);
422 m_bUndoGroup = m_bUndoBeginGroup = false;
423 m_nSyncPosition = m_nUndoPosition = 0;
424 ASSERT(m_aUndoBuf.size() == 0);
425 m_ptLastChange.x = m_ptLastChange.y = -1;
428 // flags don't need initialization because 0 is the default value
430 // Set the return value : OK + info if the file is impure
431 // A pure file is a file where EOL are consistent (all DOS, or all UNIX, or all MAC)
432 // An impure file is a file with several EOL types
433 // WinMerge may display impure files, but the default option is to unify the EOL
434 // We return this info to the caller, so it may display a confirmation box
435 if (IsTextFileStylePure(pufile->GetTxtStats()))
436 nRetVal = FileLoadResult::FRESULT_OK;
438 nRetVal = FileLoadResult::FRESULT_OK_IMPURE;
440 // stash original encoding away
441 m_encoding.m_unicoding = pufile->GetUnicoding();
442 m_encoding.m_bom = pufile->HasBom();
443 m_encoding.m_codepage = pufile->GetCodepage();
445 if (pufile->GetTxtStats().nlosses)
447 FileLoadResult::AddModifier(nRetVal, FileLoadResult::FRESULT_LOSSY);
453 // close the file now to free the handle
457 // delete the file that unpacking may have created
458 if (_tcscmp(pszFileNameInit, pszFileName) != 0)
462 TFile(pszFileName).remove();
466 LogErrorStringUTF8(e.displayText());
473 * @brief Saves file from buffer to disk
475 * @param bTempFile : false if we are saving user files and
476 * true if we are saving workin-temp-files for diff-engine
478 * @return SAVE_DONE or an error code (list in MergeDoc.h)
480 int CDiffTextBuffer::SaveToFile (const String& pszFileName,
481 bool bTempFile, String & sError, PackingInfo * infoUnpacker /*= NULL*/,
482 CRLFSTYLE nCrlfStyle /*= CRLF_STYLE_AUTOMATIC*/,
483 bool bClearModifiedFlag /*= true*/,
484 int nStartLine /*= 0*/, int nLines /*= -1*/)
486 ASSERT (nCrlfStyle == CRLF_STYLE_AUTOMATIC || nCrlfStyle == CRLF_STYLE_DOS ||
487 nCrlfStyle == CRLF_STYLE_UNIX || nCrlfStyle == CRLF_STYLE_MAC);
491 nLines = static_cast<int>(m_aLines.size() - nStartLine);
493 if (pszFileName.empty())
494 return SAVE_FAILED; // No filename, cannot save...
496 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC &&
497 !GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) ||
498 infoUnpacker && infoUnpacker->disallowMixedEOL)
500 // get the default nCrlfStyle of the CDiffTextBuffer
501 nCrlfStyle = GetCRLFMode();
502 ASSERT(nCrlfStyle >= 0 && nCrlfStyle <= 3);
505 bool bOpenSuccess = true;
506 bool bSaveSuccess = false;
509 file.SetUnicoding(m_encoding.m_unicoding);
510 file.SetBom(m_encoding.m_bom);
511 file.SetCodepage(m_encoding.m_codepage);
513 String sIntermediateFilename; // used when !bTempFile
517 bOpenSuccess = !!file.OpenCreate(pszFileName);
521 sIntermediateFilename = env_GetTempFileName(m_strTempPath,
523 if (sIntermediateFilename.empty())
524 return SAVE_FAILED; //Nothing to do if even tempfile name fails
525 bOpenSuccess = !!file.OpenCreate(sIntermediateFilename);
530 UniFile::UniError uniErr = file.GetLastUniError();
531 if (uniErr.HasError())
533 sError = uniErr.GetError();
535 LogErrorString(string_format(_T("Opening file %s failed: %s"),
536 pszFileName.c_str(), sError.c_str()));
538 LogErrorString(string_format(_T("Opening file %s failed: %s"),
539 sIntermediateFilename.c_str(), sError.c_str()));
546 // line loop : get each real line and write it in the file
548 String sEol = GetStringEol(nCrlfStyle);
549 for (int line = nStartLine; line < nStartLine + nLines; ++line)
551 if (GetLineFlags(line) & LF_GHOST)
554 // get the characters of the line (excluding EOL)
555 if (GetLineLength(line) > 0)
557 int nLineLength = GetLineLength(line);
559 sLine.reserve(nLineLength + 4);
560 sLine.append(GetLineChars(line), nLineLength);
566 EscapeControlChars(sLine);
568 int lastRealLine = ApparentLastRealLine();
569 if (line == lastRealLine || lastRealLine == -1 )
571 // last real line is never EOL terminated
572 ASSERT (_tcslen(GetLineEol(line)) == 0);
573 // write the line and exit loop
574 file.WriteString(sLine);
578 // normal real line : append an EOL
579 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC || nCrlfStyle == CRLF_STYLE_MIXED)
581 // either the EOL of the line (when preserve original EOL chars is on)
582 sLine += GetLineEol(line);
586 // or the default EOL for this file
590 // write this line to the file (codeset or unicode conversions are done there
591 file.WriteString(sLine);
597 // If we are saving user files
598 // we need an unpacker/packer, at least a "do nothing" one
599 ASSERT(infoUnpacker != NULL);
600 // repack the file here, overwrite the temporary file we did save in
601 String csTempFileName = sIntermediateFilename;
602 infoUnpacker->subcode = m_unpackerSubcode;
603 if (!FileTransform_Packing(csTempFileName, *infoUnpacker))
607 TFile(sIntermediateFilename).remove();
611 LogErrorStringUTF8(e.displayText());
613 // returns now, don't overwrite the original file
614 return SAVE_PACK_FAILED;
616 // the temp filename may have changed during packing
617 if (csTempFileName != sIntermediateFilename)
621 TFile(sIntermediateFilename).remove();
625 LogErrorStringUTF8(e.displayText());
627 sIntermediateFilename = csTempFileName;
630 // Write tempfile over original file
633 TFile file(sIntermediateFilename);
634 file.copyTo(pszFileName);
636 if (bClearModifiedFlag)
639 m_nSyncPosition = m_nUndoPosition;
643 // remember revision number on save
644 m_dwRevisionNumberOnSave = m_dwCurrentRevisionNumber;
646 // redraw line revision marks
647 UpdateViews (NULL, NULL, UPDATE_FLAGSONLY);
651 LogErrorStringUTF8(e.displayText());
656 if (bClearModifiedFlag)
659 m_nSyncPosition = m_nUndoPosition;
670 /// Replace line (removing any eol, and only including one if in strText)
671 void CDiffTextBuffer::ReplaceFullLines(CDiffTextBuffer& dbuf, CDiffTextBuffer& sbuf, CCrystalTextView * pSource, int nLineBegin, int nLineEnd, int nAction /*=CE_ACTION_UNKNOWN*/)
674 if (nLineBegin != nLineEnd || sbuf.GetLineLength(nLineEnd) > 0)
675 sbuf.GetTextWithoutEmptys(nLineBegin, 0, nLineEnd, sbuf.GetLineLength(nLineEnd), strText);
676 strText += sbuf.GetLineEol(nLineEnd);
678 if (nLineBegin != nLineEnd || dbuf.GetFullLineLength(nLineEnd) > 0)
680 int nLineEndSource = nLineEnd < dbuf.GetLineCount() ? nLineEnd : dbuf.GetLineCount();
681 if (nLineEnd+1 < GetLineCount())
682 dbuf.DeleteText(pSource, nLineBegin, 0, nLineEndSource + 1, 0, nAction);
684 dbuf.DeleteText(pSource, nLineBegin, 0, nLineEndSource, dbuf.GetLineLength(nLineEndSource), nAction);
687 if (int cchText = strText.GetLength())
690 dbuf.InsertText(pSource, nLineBegin, 0, strText, cchText, endl,endc, nAction);
694 bool CDiffTextBuffer::curUndoGroup()
696 return (m_aUndoBuf.size() != 0 && m_aUndoBuf[0].m_dwFlags&UNDO_BEGINGROUP);
699 bool CDiffTextBuffer::
700 DeleteText2(CCrystalTextView * pSource, int nStartLine, int nStartChar,
701 int nEndLine, int nEndChar, int nAction, bool bHistory /*=true*/)
703 for (auto syncpnt : m_pOwnerDoc->GetSyncPointList())
705 const int nLineSyncPoint = syncpnt[m_nThisPane];
706 if (((nStartChar == 0 && nStartLine == nLineSyncPoint) || nStartLine < nLineSyncPoint) &&
707 nLineSyncPoint < nEndLine)
708 m_pOwnerDoc->DeleteSyncPoint(m_nThisPane, nLineSyncPoint, false);
710 return CGhostTextBuffer::DeleteText2(pSource, nStartLine, nStartChar, nEndLine, nEndChar, nAction, bHistory);