OSDN Git Service

Improve plugin system (#797)
[winmerge-jp/winmerge-jp.git] / Src / DiffTextBuffer.cpp
1 /** 
2  * @file  DiffTextBuffer.cpp
3  *
4  * @brief Implementation file for CDiffTextBuffer
5  *
6  */
7
8 #include "StdAfx.h"
9 #include "DiffTextBuffer.h"
10 #include <Poco/Exception.h>
11 #include "UniFile.h"
12 #include "files.h"
13 #include "locality.h"
14 #include "paths.h"
15 #include "OptionsDef.h"
16 #include "OptionsMgr.h"
17 #include "Environment.h"
18 #include "MergeLineFlags.h"
19 #include "MergeDoc.h"
20 #include "FileTransform.h"
21 #include "FileTextEncoding.h"
22 #include "codepage_detect.h"
23 #include "TFile.h"
24
25 using Poco::Exception;
26
27 #ifdef _DEBUG
28 #define new DEBUG_NEW
29 #endif
30
31 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats);
32 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats);
33
34 /**
35  * @brief Check if file has only one EOL type.
36  * @param [in] stats File's text stats.
37  * @return true if only one EOL type is found, false otherwise.
38  */
39 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats)
40 {
41         int nType = 0;
42         if (stats.ncrlfs > 0)
43                 nType++;
44         if ( stats.ncrs > 0)
45                 nType++;
46         if (stats.nlfs > 0)
47                 nType++;
48         return (nType <= 1);
49 }
50
51 /**
52  * @brief Get file's EOL type.
53  * @param [in] stats File's text stats.
54  * @return EOL type.
55  */
56 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats)
57 {
58         // Check if file has more than one EOL type.
59         if (!IsTextFileStylePure(stats))
60                         return CRLFSTYLE::MIXED;
61         else if (stats.ncrlfs >= stats.nlfs)
62         {
63                 if (stats.ncrlfs >= stats.ncrs)
64                         return CRLFSTYLE::DOS;
65                 else
66                         return CRLFSTYLE::MAC;
67         }
68         else
69         {
70                 if (stats.nlfs >= stats.ncrs)
71                         return CRLFSTYLE::UNIX;
72                 else
73                         return CRLFSTYLE::MAC;
74         }
75 }
76
77 /**
78  * @brief Constructor.
79  * @param [in] pDoc Owning CMergeDoc.
80  * @param [in] pane Pane number this buffer is associated with.
81  */
82 CDiffTextBuffer::CDiffTextBuffer(CMergeDoc * pDoc, int pane)
83 : m_pOwnerDoc(pDoc)
84 , m_nThisPane(pane)
85 , m_bMixedEOL(false)
86 {
87 }
88
89 /**
90  * @brief Get a line from the buffer.
91  * @param [in] nLineIndex Index of the line to get.
92  * @param [out] strLine Returns line text in the index.
93  */
94 bool CDiffTextBuffer::GetLine(int nLineIndex, CString &strLine) const
95 {
96         int nLineLength = CCrystalTextBuffer::GetLineLength(nLineIndex);
97         if (nLineLength < 0)
98                 return false;
99         else if (nLineLength == 0)
100                 strLine.Empty();
101         else
102         {
103                 _tcsncpy_s(strLine.GetBuffer(nLineLength + 1), nLineLength + 1,
104                         CCrystalTextBuffer::GetLineChars(nLineIndex), nLineLength);
105                 strLine.ReleaseBuffer(nLineLength);
106         }
107         return true;
108 }
109
110 /**
111  * @brief Set the buffer modified status.
112  * @param [in] bModified New modified status, true if buffer has been
113  *   modified since last saving.
114  */
115 void CDiffTextBuffer::                  /* virtual override */
116 SetModified(bool bModified /*= true*/)  
117 {
118         CCrystalTextBuffer::SetModified (bModified);
119         m_pOwnerDoc->SetModifiedFlag (bModified);
120 }
121
122 /**
123  * @brief Get a line (with EOL bytes) from the buffer.
124  * This function is like GetLine() but it also includes line's EOL to the
125  * returned string.
126  * @param [in] nLineIndex Index of the line to get.
127  * @param [out] strLine Returns line text in the index. Existing content
128  * of this string is overwritten.
129  */
130 bool CDiffTextBuffer::GetFullLine(int nLineIndex, CString &strLine) const
131 {
132         int cchText = GetFullLineLength(nLineIndex);
133         if (cchText == 0)
134         {
135                 strLine.Empty();
136                 return false;
137         }
138         LPTSTR pchText = strLine.GetBufferSetLength(cchText);
139         memcpy(pchText, GetLineChars(nLineIndex), cchText * sizeof(TCHAR));
140         return true;
141 }
142
143 void CDiffTextBuffer::                  /* virtual override */
144 AddUndoRecord(bool bInsert, const CPoint & ptStartPos,
145                 const CPoint & ptEndPos, LPCTSTR pszText, size_t cchText,
146                 int nActionType /*= CE_ACTION_UNKNOWN*/,
147                 CDWordArray *paSavedRevisionNumbers /*= nullptr*/)
148 {
149         CGhostTextBuffer::AddUndoRecord(bInsert, ptStartPos, ptEndPos, pszText,
150                 cchText, nActionType, paSavedRevisionNumbers);
151         if (m_aUndoBuf[m_nUndoPosition - 1].m_dwFlags & UNDO_BEGINGROUP)
152         {
153                 m_pOwnerDoc->undoTgt.erase(m_pOwnerDoc->curUndo, m_pOwnerDoc->undoTgt.end());
154                 m_pOwnerDoc->undoTgt.push_back(m_nThisPane);
155                 m_pOwnerDoc->curUndo = m_pOwnerDoc->undoTgt.end();
156         }
157 }
158
159 /**
160  * @brief Checks if a flag is set for line.
161  * @param [in] line Index (0-based) for line.
162  * @param [in] flag Flag to check.
163  * @return true if flag is set, false otherwise.
164  */
165 bool CDiffTextBuffer::FlagIsSet(UINT line, DWORD flag) const
166 {
167         return ((m_aLines[line].m_dwFlags & flag) == flag);
168 }
169
170 /**
171 Remove blank lines and clear winmerge flags
172 (2003-06-21, Perry: I don't understand why this is necessary, but if this isn't 
173 done, more and more gray lines appear in the file)
174 (2003-07-31, Laoran I don't understand either why it is necessary, but it works
175 fine, so let's go on with it)
176 */
177 void CDiffTextBuffer::prepareForRescan()
178 {
179         RemoveAllGhostLines();
180         for (int ct = GetLineCount() - 1; ct >= 0; --ct)
181         {
182                 SetLineFlag(ct, 
183                         LF_INVISIBLE | LF_DIFF | LF_TRIVIAL | LF_MOVED | LF_SNP,
184                         false, false, false);
185         }
186 }
187
188 /** 
189  * @brief Called when line has been edited.
190  * After editing a line, we don't know if there is a diff or not.
191  * So we clear the LF_DIFF flag (and it is more easy to read during editing).
192  * Rescan will set the proper color.
193  * @param [in] nLine Line that has been edited.
194  */
195
196 void CDiffTextBuffer::                  /* virtual override */
197 OnNotifyLineHasBeenEdited(int nLine)
198 {
199         SetLineFlag(nLine, LF_DIFF, false, false, false);
200         SetLineFlag(nLine, LF_TRIVIAL, false, false, false);
201         SetLineFlag(nLine, LF_MOVED, false, false, false);
202         SetLineFlag(nLine, LF_SNP, false, false, false);
203         CGhostTextBuffer::OnNotifyLineHasBeenEdited(nLine);
204 }
205
206 /**
207  * @brief Load file from disk into buffer
208  *
209  * @param [in] pszFileNameInit File to load
210  * @param [in] infoUnpacker Unpacker plugin
211  * @param [in] sToFindUnpacker String for finding unpacker plugin
212  * @param [out] readOnly Loading was lossy so file should be read-only
213  * @param [in] nCrlfStyle EOL style used
214  * @param [in] encoding Encoding used
215  * @param [out] sError Error message returned
216  * @return FRESULT_OK when loading succeed or (list in files.h):
217  * - FRESULT_OK_IMPURE : load OK, but the EOL are of different types
218  * - FRESULT_ERROR_UNPACK : plugin failed to unpack
219  * - FRESULT_ERROR : loading failed, sError contains error message
220  * - FRESULT_BINARY : file is binary file
221  * @note If this method fails, it calls InitNew so the CDiffTextBuffer is in a valid state
222  */
223 int CDiffTextBuffer::LoadFromFile(LPCTSTR pszFileNameInit,
224                 PackingInfo& infoUnpacker, LPCTSTR sToFindUnpacker, bool & readOnly,
225                 CRLFSTYLE nCrlfStyle, const FileTextEncoding & encoding, CString &sError)
226 {
227         ASSERT(!m_bInit);
228         ASSERT(m_aLines.size() == 0);
229
230         // Unpacking the file here, save the result in a temporary file
231         m_strTempFileName = pszFileNameInit;
232         if (!infoUnpacker.Unpacking(&m_unpackerSubcodes, m_strTempFileName, sToFindUnpacker, { m_strTempFileName }))
233         {
234                 InitNew(); // leave crystal editor in valid, empty state
235                 return FileLoadResult::FRESULT_ERROR_UNPACK;
236         }
237
238         // we will load the transformed file
239         LPCTSTR pszFileName = m_strTempFileName.c_str();
240
241         String sExt;
242         DWORD nRetVal = FileLoadResult::FRESULT_OK;
243
244         // Set encoding based on extension, if we know one
245         paths::SplitFilename(pszFileName, nullptr, nullptr, &sExt);
246         CrystalLineParser::TextDefinition *def = 
247                 CrystalLineParser::GetTextType(sExt.c_str());
248         if (def && def->encoding != -1)
249                 m_nSourceEncoding = def->encoding;
250         
251         UniFile *pufile = new UniMemFile;
252
253         // Now we only use the UniFile interface
254         // which is something we could implement for HTTP and/or FTP files
255
256         if (!pufile->OpenReadOnly(pszFileName))
257         {
258                 nRetVal = FileLoadResult::FRESULT_ERROR;
259                 UniFile::UniError uniErr = pufile->GetLastUniError();
260                 if (uniErr.HasError())
261                 {
262                         sError = uniErr.GetError().c_str();
263                 }
264                 InitNew(); // leave crystal editor in valid, empty state
265         }
266         else
267         {
268                 if (!infoUnpacker.GetPluginPipeline().empty())
269                 {
270                         // re-detect codepage
271                         int iGuessEncodingType = GetOptionsMgr()->GetInt(OPT_CP_DETECT);
272                         FileTextEncoding encoding2 = codepage_detect::Guess(pszFileName, iGuessEncodingType);
273                         pufile->SetUnicoding(encoding2.m_unicoding);
274                         pufile->SetCodepage(encoding2.m_codepage);
275                         pufile->SetBom(encoding2.m_bom);
276                         if (encoding2.m_bom)
277                                 pufile->ReadBom();
278                 }
279                 else
280                 {
281                         // If the file is not unicode file, use the codepage we were given to
282                         // interpret the 8-bit characters. If the file is unicode file,
283                         // determine its type (IsUnicode() does that).
284                         if (encoding.m_unicoding == ucr::NONE  || !pufile->IsUnicode())
285                                 pufile->SetCodepage(encoding.m_codepage);
286                 }
287                 UINT lineno = 0;
288                 String eol, preveol;
289                 String sline;
290                 bool done = false;
291                 COleDateTime start = COleDateTime::GetCurrentTime(); // for trace messages
292
293                 // Manually grow line array exponentially
294                 UINT arraysize = 500;
295                 m_aLines.resize(arraysize);
296                 
297                 // preveol must be initialized for empty files
298                 preveol = _T("\n");
299                 
300                 do {
301                         bool lossy = false;
302                         done = !pufile->ReadString(sline, eol, &lossy);
303
304                         // if last line had no eol, we can quit
305                         if (done && preveol.empty())
306                                 break;
307                         // but if last line had eol, we add an extra (empty) line to buffer
308
309                         // Grow line array
310                         if (lineno == arraysize)
311                         {
312                                 // For smaller sizes use exponential growth, but for larger
313                                 // sizes grow by constant ratio. Unlimited exponential growth
314                                 // easily runs out of memory.
315                                 if (arraysize < 100 * 1024)
316                                         arraysize *= 2;
317                                 else
318                                         arraysize += 100 * 1024;
319                                 m_aLines.resize(arraysize);
320                         }
321
322                         sline += eol; // TODO: opportunity for optimization, as CString append is terrible
323                         if (lossy)
324                         {
325                                 // TODO: Should record lossy status of line
326                         }
327                         AppendLine(lineno, sline.c_str(), static_cast<int>(sline.length()));
328                         ++lineno;
329                         preveol = eol;
330
331                 } while (!done);
332
333                 // fix array size (due to our manual exponential growth
334                 m_aLines.resize(lineno);
335         
336                 
337                 //Try to determine current CRLF mode (most frequent)
338                 if (nCrlfStyle == CRLFSTYLE::AUTOMATIC)
339                 {
340                         nCrlfStyle = GetTextFileStyle(pufile->GetTxtStats());
341                 }
342                 ASSERT (nCrlfStyle != CRLFSTYLE::AUTOMATIC);
343                 SetCRLFMode(nCrlfStyle);
344                 
345                 //  At least one empty line must present
346                 // (view does not work for empty buffers)
347                 ASSERT(m_aLines.size() > 0);
348                 
349                 m_bInit = true;
350                 m_bModified = false;
351                 m_bUndoGroup = m_bUndoBeginGroup = false;
352                 m_nSyncPosition = m_nUndoPosition = 0;
353                 ASSERT(m_aUndoBuf.size() == 0);
354                 m_ptLastChange.x = m_ptLastChange.y = -1;
355                 
356                 FinishLoading();
357                 // flags don't need initialization because 0 is the default value
358
359                 // Set the return value : OK + info if the file is impure
360                 // A pure file is a file where EOL are consistent (all DOS, or all UNIX, or all MAC)
361                 // An impure file is a file with several EOL types
362                 // WinMerge may display impure files, but the default option is to unify the EOL
363                 // We return this info to the caller, so it may display a confirmation box
364                 if (IsTextFileStylePure(pufile->GetTxtStats()))
365                         nRetVal = FileLoadResult::FRESULT_OK;
366                 else
367                         nRetVal = FileLoadResult::FRESULT_OK_IMPURE;
368
369                 // stash original encoding away
370                 m_encoding.m_unicoding = pufile->GetUnicoding();
371                 m_encoding.m_bom = pufile->HasBom();
372                 m_encoding.m_codepage = pufile->GetCodepage();
373
374                 if (pufile->GetTxtStats().nlosses)
375                 {
376                         FileLoadResult::AddModifier(nRetVal, FileLoadResult::FRESULT_LOSSY);
377                         readOnly = true;
378                 }
379         }
380         
381         // close the file now to free the handle
382         pufile->Close();
383         delete pufile;
384
385         // delete the file that unpacking may have created
386         if (_tcscmp(pszFileNameInit, pszFileName) != 0)
387         {
388                 try
389                 {
390                         TFile(pszFileName).remove();
391                 }
392                 catch (Exception& e)
393                 {
394                         LogErrorStringUTF8(e.displayText());
395                 }
396         }
397         return nRetVal;
398 }
399
400 /**
401  * @brief Saves file from buffer to disk
402  *
403  * @param bTempFile : false if we are saving user files and
404  * true if we are saving working-temp-files for diff-engine
405  *
406  * @return SAVE_DONE or an error code (list in MergeDoc.h)
407  */
408 int CDiffTextBuffer::SaveToFile (const String& pszFileName,
409                 bool bTempFile, String & sError, PackingInfo& infoUnpacker /*= nullptr*/,
410                 CRLFSTYLE nCrlfStyle /*= CRLFSTYLE::AUTOMATIC*/,
411                 bool bClearModifiedFlag /*= true*/,
412                 int nStartLine /*= 0*/, int nLines /*= -1*/)
413 {
414         ASSERT (nCrlfStyle == CRLFSTYLE::AUTOMATIC || nCrlfStyle == CRLFSTYLE::DOS ||
415                 nCrlfStyle == CRLFSTYLE::UNIX || nCrlfStyle == CRLFSTYLE::MAC);
416         ASSERT (m_bInit);
417
418         if (nLines == -1)
419                 nLines = static_cast<int>(m_aLines.size() - nStartLine);
420
421         if (pszFileName.empty())
422                 return SAVE_FAILED;     // No filename, cannot save...
423
424         if (nCrlfStyle == CRLFSTYLE::AUTOMATIC &&
425                 !GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
426         {
427                         // get the default nCrlfStyle of the CDiffTextBuffer
428                 nCrlfStyle = GetCRLFMode();
429                 ASSERT(nCrlfStyle != CRLFSTYLE::AUTOMATIC);
430         }
431
432         bool bOpenSuccess = true;
433         bool bSaveSuccess = false;
434
435         UniStdioFile file;
436
437         String sIntermediateFilename; // used when !bTempFile
438
439         if (bTempFile)
440         {
441                 file.SetUnicoding(ucr::UTF8);
442                 file.SetBom(GetOptionsMgr()->GetInt(OPT_CMP_DIFF_ALGORITHM) == 0);
443                 bOpenSuccess = !!file.OpenCreate(pszFileName);
444         }
445         else
446         {
447                 file.SetUnicoding(m_encoding.m_unicoding);
448                 file.SetBom(m_encoding.m_bom);
449                 file.SetCodepage(m_encoding.m_codepage);
450                 sIntermediateFilename = env::GetTemporaryFileName(m_strTempPath,
451                         _T("MRG_"), nullptr);
452                 if (sIntermediateFilename.empty())
453                         return SAVE_FAILED;  //Nothing to do if even tempfile name fails
454                 bOpenSuccess = !!file.OpenCreate(sIntermediateFilename);
455         }
456
457         if (!bOpenSuccess)
458         {       
459                 UniFile::UniError uniErr = file.GetLastUniError();
460                 if (uniErr.HasError())
461                 {
462                         sError = uniErr.GetError();
463                         if (bTempFile)
464                                 LogErrorString(strutils::format(_T("Opening file %s failed: %s"),
465                                         pszFileName, sError));
466                         else
467                                 LogErrorString(strutils::format(_T("Opening file %s failed: %s"),
468                                         sIntermediateFilename, sError));
469                 }
470                 return SAVE_FAILED;
471         }
472
473         file.WriteBom();
474
475         // line loop : get each real line and write it in the file
476         String sLine;
477         String sEol = GetStringEol(nCrlfStyle);
478         int lastRealLine = ApparentLastRealLine();
479         for (int line = nStartLine; line < nStartLine + nLines; ++line)
480         {
481                 if (GetLineFlags(line) & LF_GHOST)
482                         continue;
483
484                 // get the characters of the line (excluding EOL)
485                 if (GetLineLength(line) > 0)
486                 {
487                         int nLineLength = GetLineLength(line);
488                         sLine.resize(0);
489                         sLine.reserve(nLineLength + 4);
490                         sLine.append(GetLineChars(line), nLineLength);
491                 }
492                 else
493                         sLine = _T("");
494
495                 if (bTempFile && m_bTableEditing && m_bAllowNewlinesInQuotes)
496                 {
497                         strutils::replace(sLine, _T("\x1b"), _T("\x1b\x1b"));
498                         strutils::replace(sLine, _T("\r"), _T("\x1br"));
499                         strutils::replace(sLine, _T("\n"), _T("\x1bn"));
500                 }
501
502                 // last real line ?
503                 if (line == lastRealLine || lastRealLine == -1 )
504                 {
505                         // If original last line had no EOL, then we are done
506                         if( !m_aLines[line].HasEol() )
507                         {
508                                 file.WriteString(sLine);
509                                 break;
510                         }
511                         // Otherwise, add the appropriate EOL to the last line ...
512                 }
513
514                 // normal line : append an EOL
515                 if (nCrlfStyle == CRLFSTYLE::AUTOMATIC || nCrlfStyle == CRLFSTYLE::MIXED)
516                 {
517                         // either the EOL of the line (when preserve original EOL chars is on)
518                         sLine += GetLineEol(line);
519                 }
520                 else
521                 {
522                         // or the default EOL for this file
523                         sLine += sEol;
524                 }
525
526                 // write this line to the file (codeset or unicode conversions are done there
527                 file.WriteString(sLine);
528
529                 if (line == lastRealLine || lastRealLine == -1)
530                 {
531                         // Last line, so now done
532                         break;
533                 }
534         }
535         file.Close();
536
537         if (!bTempFile)
538         {
539                 // If we are saving user files
540                 // we need an unpacker/packer, at least a "do nothing" one
541                 // repack the file here, overwrite the temporary file we did save in
542                 bSaveSuccess = infoUnpacker.Packing(sIntermediateFilename, pszFileName, m_unpackerSubcodes, { pszFileName });
543                 try
544                 {
545                         TFile(sIntermediateFilename).remove();
546                 }
547                 catch (Exception& e)
548                 {
549                         LogErrorStringUTF8(e.displayText());
550                 }
551                 if (!bSaveSuccess)
552                 {
553                         // returns now, don't overwrite the original file
554                         return SAVE_PACK_FAILED;
555                 }
556
557                 if (bClearModifiedFlag)
558                 {
559                         SetModified(false);
560                         m_nSyncPosition = m_nUndoPosition;
561                 }
562
563                 // remember revision number on save
564                 m_dwRevisionNumberOnSave = m_dwCurrentRevisionNumber;
565
566                 // redraw line revision marks
567                 UpdateViews (nullptr, nullptr, UPDATE_FLAGSONLY);       
568         }
569         else
570         {
571                 if (bClearModifiedFlag)
572                 {
573                         SetModified(false);
574                         m_nSyncPosition = m_nUndoPosition;
575                 }
576                 bSaveSuccess = true;
577         }
578
579         if (bSaveSuccess)
580                 return SAVE_DONE;
581         else
582                 return SAVE_FAILED;
583 }
584
585 /// Replace line (removing any eol, and only including one if in strText)
586 void CDiffTextBuffer::ReplaceFullLines(CDiffTextBuffer& dbuf, CDiffTextBuffer& sbuf, CCrystalTextView * pSource, int nLineBegin, int nLineEnd, int nAction /*=CE_ACTION_UNKNOWN*/)
587 {
588         CString strText;
589         if (nLineBegin != nLineEnd || sbuf.GetLineLength(nLineEnd) > 0)
590                 sbuf.GetTextWithoutEmptys(nLineBegin, 0, nLineEnd, sbuf.GetLineLength(nLineEnd), strText);
591         strText += sbuf.GetLineEol(nLineEnd);
592
593         if (nLineBegin != nLineEnd || dbuf.GetFullLineLength(nLineEnd) > 0)
594         {
595                 int nLineEndSource = nLineEnd < dbuf.GetLineCount() ? nLineEnd : dbuf.GetLineCount();
596                 if (nLineEnd+1 < GetLineCount())
597                         dbuf.DeleteText(pSource, nLineBegin, 0, nLineEndSource + 1, 0, nAction);
598                 else
599                         dbuf.DeleteText(pSource, nLineBegin, 0, nLineEndSource, dbuf.GetLineLength(nLineEndSource), nAction); 
600         }
601
602         if (int cchText = strText.GetLength())
603         {
604                 int endl,endc;
605                 dbuf.InsertText(pSource, nLineBegin, 0, strText, cchText, endl,endc, nAction);
606         }
607 }
608
609 bool CDiffTextBuffer::curUndoGroup()
610 {
611         return (m_aUndoBuf.size() != 0 && m_aUndoBuf[0].m_dwFlags&UNDO_BEGINGROUP);
612 }
613
614 bool CDiffTextBuffer::                  /* virtual override */
615 DeleteText2(CCrystalTextView * pSource, int nStartLine, int nStartChar,
616         int nEndLine, int nEndChar, int nAction /*= CE_ACTION_UNKNOWN*/, bool bHistory /*= true*/)
617 {
618         for (auto syncpnt : m_pOwnerDoc->GetSyncPointList())
619         {
620                 const int nLineSyncPoint = syncpnt[m_nThisPane];
621                 if (((nStartChar == 0 && nStartLine == nLineSyncPoint) || nStartLine < nLineSyncPoint) &&
622                         nLineSyncPoint < nEndLine)
623                         m_pOwnerDoc->DeleteSyncPoint(m_nThisPane, nLineSyncPoint, false);
624         }
625         return CGhostTextBuffer::DeleteText2(pSource, nStartLine, nStartChar, nEndLine, nEndChar, nAction, bHistory);
626 }