diff --git a/sakura_core/_main/CCommandLine.cpp b/sakura_core/_main/CCommandLine.cpp index 801ea9e5cf..d5b4a172c2 100644 --- a/sakura_core/_main/CCommandLine.cpp +++ b/sakura_core/_main/CCommandLine.cpp @@ -12,7 +12,7 @@ Copyright (C) 2005, D.S.Koba, genta, susu Copyright (C) 2006, ryoji Copyright (C) 2007, ryoji - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holder to use this code for other purpose. @@ -484,6 +484,8 @@ void CCommandLine::ParseCommandLine( LPCWSTR pszCmdLineSrc, bool bResponse ) m_gi.bGrepPaste = true; break; case 'O': m_gi.bGrepBackup = true; break; + case 'E': + m_gi.bGrepExcludeFileRegexp = true; break; default: break; } diff --git a/sakura_core/_main/CControlTray.cpp b/sakura_core/_main/CControlTray.cpp index 83a8117f0c..811365e396 100644 --- a/sakura_core/_main/CControlTray.cpp +++ b/sakura_core/_main/CControlTray.cpp @@ -138,18 +138,19 @@ void CControlTray::DoGrepCreateWindow(HINSTANCE hinst, HWND msgParent, CDlgGrep& //GOPTオプション WCHAR pOpt[64] = L""; - if( cDlgGrep.m_bSubFolder )wcscat( pOpt, L"S" ); // サブフォルダーからも検索する - if( cDlgGrep.m_sSearchOption.bLoHiCase )wcscat( pOpt, L"L" ); // 英大文字と英小文字を区別する - if( cDlgGrep.m_sSearchOption.bRegularExp )wcscat( pOpt, L"R" ); // 正規表現 - if( cDlgGrep.m_nGrepOutputLineType == 1 )wcscat( pOpt, L"P" ); // 行を出力する - if( cDlgGrep.m_nGrepOutputLineType == 2 )wcscat( pOpt, L"N" ); // 否ヒット行を出力する 2014.09.23 - if( cDlgGrep.m_sSearchOption.bWordOnly )wcscat( pOpt, L"W" ); // 単語単位で探す - if( 1 == cDlgGrep.m_nGrepOutputStyle )wcscat( pOpt, L"1" ); // Grep: 出力形式 - if( 2 == cDlgGrep.m_nGrepOutputStyle )wcscat( pOpt, L"2" ); // Grep: 出力形式 - if( 3 == cDlgGrep.m_nGrepOutputStyle )wcscat( pOpt, L"3" ); - if( cDlgGrep.m_bGrepOutputFileOnly )wcscat( pOpt, L"F" ); - if( cDlgGrep.m_bGrepOutputBaseFolder )wcscat( pOpt, L"B" ); - if( cDlgGrep.m_bGrepSeparateFolder )wcscat( pOpt, L"D" ); + if( cDlgGrep.m_bSubFolder )wcscat_s( pOpt, L"S" ); // サブフォルダーからも検索する + if( cDlgGrep.m_sSearchOption.bLoHiCase )wcscat_s( pOpt, L"L" ); // 英大文字と英小文字を区別する + if( cDlgGrep.m_sSearchOption.bRegularExp )wcscat_s( pOpt, L"R" ); // 正規表現 + if( cDlgGrep.m_nGrepOutputLineType == 1 )wcscat_s( pOpt, L"P" ); // 行を出力する + if( cDlgGrep.m_nGrepOutputLineType == 2 )wcscat_s( pOpt, L"N" ); // 否ヒット行を出力する 2014.09.23 + if( cDlgGrep.m_sSearchOption.bWordOnly )wcscat_s( pOpt, L"W" ); // 単語単位で探す + if( 1 == cDlgGrep.m_nGrepOutputStyle )wcscat_s( pOpt, L"1" ); // Grep: 出力形式 + if( 2 == cDlgGrep.m_nGrepOutputStyle )wcscat_s( pOpt, L"2" ); // Grep: 出力形式 + if( 3 == cDlgGrep.m_nGrepOutputStyle )wcscat_s( pOpt, L"3" ); + if( cDlgGrep.m_bGrepOutputFileOnly )wcscat_s( pOpt, L"F" ); + if( cDlgGrep.m_bGrepOutputBaseFolder )wcscat_s( pOpt, L"B" ); + if( cDlgGrep.m_bGrepSeparateFolder )wcscat_s( pOpt, L"D" ); + if( cDlgGrep.m_bExcludeFileRegularExp )wcscat_s( pOpt, L"E" ); // 除外ファイルを正規表現として扱う if( pOpt[0] != L'\0' ){ cCmdLine.AppendString( L" -GOPT=" ); cCmdLine.AppendString( pOpt ); diff --git a/sakura_core/_main/CNormalProcess.cpp b/sakura_core/_main/CNormalProcess.cpp index a2287972cf..b0457b9a26 100644 --- a/sakura_core/_main/CNormalProcess.cpp +++ b/sakura_core/_main/CNormalProcess.cpp @@ -14,7 +14,7 @@ Copyright (C) 2007, ryoji Copyright (C) 2008, Uchi Copyright (C) 2009, syat, ryoji - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holder to use this code for other purpose. @@ -211,27 +211,30 @@ bool CNormalProcess::InitializeProcess() SetMainWindow( pEditWnd->GetHwnd() ); ::ReleaseMutex( hMutex ); ::CloseHandle( hMutex ); - this->m_pcEditApp->m_pcGrepAgent->DoGrep( - &pEditWnd->GetActiveView(), - gi.bGrepReplace, - &gi.cmGrepKey, - &gi.cmGrepRep, - &gi.cmGrepFile, - &gi.cmGrepFolder, - gi.bGrepCurFolder, - gi.bGrepSubFolder, - gi.bGrepStdout, - gi.bGrepHeader, - gi.sGrepSearchOption, - gi.nGrepCharSet, // 2002/09/21 Moca - gi.nGrepOutputLineType, - gi.nGrepOutputStyle, - gi.bGrepOutputFileOnly, - gi.bGrepOutputBaseFolder, - gi.bGrepSeparateFolder, - gi.bGrepPaste, - gi.bGrepBackup - ); + { + const SGrepInput grepInput{ &gi.cmGrepKey, &gi.cmGrepRep, &gi.cmGrepFile, &gi.cmGrepFolder }; + SGrepOption sGrepOption; + sGrepOption.bGrepReplace = gi.bGrepReplace; + sGrepOption.bGrepSubFolder = gi.bGrepSubFolder != FALSE; + sGrepOption.bGrepStdout = gi.bGrepStdout; + sGrepOption.bGrepHeader = gi.bGrepHeader; + sGrepOption.nGrepCharSet = gi.nGrepCharSet; + sGrepOption.nGrepOutputLineType = gi.nGrepOutputLineType; + sGrepOption.nGrepOutputStyle = gi.nGrepOutputStyle; + sGrepOption.bGrepOutputFileOnly = gi.bGrepOutputFileOnly; + sGrepOption.bGrepOutputBaseFolder = gi.bGrepOutputBaseFolder; + sGrepOption.bGrepSeparateFolder = gi.bGrepSeparateFolder; + sGrepOption.bGrepPaste = gi.bGrepPaste; + sGrepOption.bGrepBackup = gi.bGrepBackup; + this->m_pcEditApp->m_pcGrepAgent->DoGrep( + &pEditWnd->GetActiveView(), + grepInput, + gi.sGrepSearchOption, + sGrepOption, + gi.bGrepCurFolder, + gi.bGrepExcludeFileRegexp + ); + } pEditWnd->m_cDlgFuncList.Refresh(); // アウトラインを再解析する } else{ diff --git a/sakura_core/agent/CGrepAgent.cpp b/sakura_core/agent/CGrepAgent.cpp index 366ad3b9d2..62dadb5b67 100644 --- a/sakura_core/agent/CGrepAgent.cpp +++ b/sakura_core/agent/CGrepAgent.cpp @@ -1,6 +1,9 @@ -/*! @file */ +/*! @file + @brief Grep検索エージェント + @note マルチスレッド対応・除外ファイル機能拡張 +*/ /* - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -9,8 +12,10 @@ #include "grep/CGrepEnumKeys.h" #include "grep/CGrepEnumFilterFiles.h" #include "grep/CGrepEnumFilterFolders.h" +#include "grep/GrepPathFormat.h" #include "agent/CSearchAgent.h" #include "dlg/CDlgCancel.h" +#include "dlg/CDlgGrep.h" #include "_main/CAppMode.h" #include "_main/CMutex.h" #include "env/CShareData.h" @@ -33,15 +38,28 @@ #include "CSelectLang.h" #include "sakura_rc.h" #include "config/system_constants.h" - -#define UICHECK_INTERVAL_MILLISEC 100 // UI確認の時間間隔 -#define ADDTAIL_INTERVAL_MILLISEC 50 // 結果出力の時間間隔 -#define UIFILENAME_INTERVAL_MILLISEC 15 // Cancelダイアログのファイル名表示更新間隔 +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr DWORD UICHECK_INTERVAL_MILLISEC = 100; // UI確認の時間間隔 +constexpr DWORD ADDTAIL_INTERVAL_MILLISEC = 50; // 結果出力の時間間隔 +constexpr DWORD UIFILENAME_INTERVAL_MILLISEC = 100; // Cancelダイアログのファイル名表示更新間隔(P8: SendMessage コスト削減のため UICHECK_INTERVAL_MILLISEC と統一) + +//! Grep 結果フッタおよびタイマー表示のバッファサイズ +//! "該当 %d 件\r\n" や "%d ミリ秒\r\n" 等のフォーマット結果が収まる十分なサイズ。 +//! int の最大桁数 (11桁) + フォーマット文字列の最大長 (≦ 64文字) + 終端 0 を考慮。 +constexpr size_t kGrepFooterBufSize = 128; /*! * 指定された文字列をタイプ別設定に従ってエスケープする */ -inline CNativeW EscapeStringLiteral( const STypeConfig& type, const CNativeW& cmemString ) +static CNativeW EscapeStringLiteral( const STypeConfig& type, const CNativeW& cmemString ) { CNativeW cmemWork2( cmemString ); if( FALSE == type.m_ColorInfoArr[COLORIDX_WSTRING].m_bDisp ){ @@ -62,31 +80,6 @@ inline CNativeW EscapeStringLiteral( const STypeConfig& type, const CNativeW& cm /*! * パスリストを文字列化する */ -template -std::wstring FormatPathList( const ContainerType& containter ) -{ - std::wstring strPatterns; - bool firstItem = true; - for( const auto& pattern : containter ){ - // パスリストは ':' で区切る(2つ目以降の前に付加する) - if( firstItem ){ - firstItem = false; - }else { - strPatterns += L';'; - } - - // ';' を含むパス名は引用符で囲む - if( std::wstring::npos != std::wstring_view( pattern ).find( L';' ) ){ - strPatterns += L'"'; - strPatterns += pattern; - strPatterns += L'"'; - }else{ - strPatterns += pattern; - } - } - return strPatterns; -} - class CFileLoadOrWnd{ CFileLoad m_cfl; HWND m_hWnd; @@ -160,6 +153,9 @@ class CFileLoadOrWnd{ } int GetPercent(){ if( m_hWnd ){ + if( m_nLineNum <= 0 ){ + return 0; // 行数 0 のウインドウでのゼロ除算を防ぐ + } return (int)(m_nLineCurrent * 100.0 / m_nLineNum); } return m_cfl.GetPercent(); @@ -173,9 +169,7 @@ class CFileLoadOrWnd{ } }; -CGrepAgent::CGrepAgent() -{ -} +CGrepAgent::CGrepAgent() = default; ECallbackResult CGrepAgent::OnBeforeClose() { @@ -264,18 +258,21 @@ void CGrepAgent::AddTail( CEditView* pcEditView, const CNativeW& cmem, bool bAdd } } -int GetHwndTitle(HWND& hWndTarget, CNativeW* pmemTitle, WCHAR* pszWindowName, WCHAR* pszWindowPath, const WCHAR* pszFile) +static int GetHwndTitle(HWND& hWndTarget, CNativeW* pmemTitle, WCHAR* pszWindowName, WCHAR* pszWindowPath, const WCHAR* pszFile) { hWndTarget = nullptr; //out引数をクリアする - constexpr auto& szTargetPrefix = L":HWND:"; - constexpr auto cchTargetPrefix = int(std::size(szTargetPrefix)) - 1; - if( 0 != wcsncmp(pszFile, szTargetPrefix, cchTargetPrefix) ){ + // :HWND: トークンの接頭辞定義と判定は CDlgGrep に共通化(2-E) + constexpr auto cchTargetPrefix = int(std::size(CDlgGrep::HWND_FILE_TOKEN_PREFIX)) - 1; + if( !CDlgGrep::IsHwndFileToken(pszFile) ){ return 0; // ハンドルGrepではない } - if( 0 >= ::swscanf_s(pszFile + cchTargetPrefix, L"%zx", (size_t*)&hWndTarget) || !IsSakuraMainWindow(hWndTarget) ){ + // HWND* を size_t* として読み書きする型パンニングを避けるため、一旦 size_t で受けてから変換する + size_t nHwndValue = 0; + if( 0 >= ::swscanf_s(pszFile + cchTargetPrefix, L"%zx", &nHwndValue) || !IsSakuraMainWindow((HWND)nHwndValue) ){ return -1; // ハンドルを読み取れなかった、または、対象ウインドウハンドルが存在しない } + hWndTarget = (HWND)nHwndValue; if( pmemTitle ){ const wchar_t* p = L"Window:["; pmemTitle->SetStringHoldBuffer(p, 8); @@ -288,6 +285,9 @@ int GetHwndTitle(HWND& hWndTarget, CNativeW* pmemTitle, WCHAR* pszWindowName, WC WCHAR szGrep[100]; editInfo->m_bIsModified = false; const EditNode* node = CAppNodeManager::getInstance()->GetEditNode(hWndTarget); + if( !node ){ + return -1; // 管理リストに見つからない(対象ウインドウが閉じられた直後など) + } WCHAR* pszTagName = szTitle; if( editInfo->m_bIsGrep ){ // Grepは検索キーとタグがぶつかることがあるので単に(Grep)と表示 @@ -317,36 +317,39 @@ int GetHwndTitle(HWND& hWndTarget, CNativeW* pmemTitle, WCHAR* pszWindowName, WC } +//! @brief CGrepAgent::DoGrep のエラー・正常終了いずれの経路でも +//! m_bGrepRunning / pcViewDst->m_bDoing_UndoRedo を確実にリセットする RAII ガード(A-3) +//! @note 本ガード導入前は、HWNDエラー経路(GetHwndTitle が -1 を返す場合)のみ +//! このリセットが漏れており、以後 Grep が永続的に実行不能になる不具合があった。 +struct SGrepRunningGuard { + CGrepAgent* pAgent; + CEditView* pView; + SGrepRunningGuard( CGrepAgent* agent, CEditView* view ) noexcept : pAgent(agent), pView(view) {} + SGrepRunningGuard(const SGrepRunningGuard&) = delete; + SGrepRunningGuard& operator=(const SGrepRunningGuard&) = delete; + ~SGrepRunningGuard(){ + pAgent->m_bGrepRunning = false; + pView->m_bDoing_UndoRedo = false; + } +}; + /*! Grep実行 - @param[in] pcmGrepKey 検索パターン - @param[in] pcmGrepFile 検索対象ファイルパターン(!で除外指定)) - @param[in] pcmGrepFolder 検索対象フォルダー + @param[in] grepInput 検索入力(キー/置換/ファイル/フォルダー) + @param[in] sSearchOption 検索オプション + @param[in] sGrepOption Grepオプション(値渡し・内部で正規化) @date 2008.12.07 nasukoji ファイル名パターンのバッファオーバラン対策 @date 2008.12.13 genta 検索パターンのバッファオーバラン対策 @date 2012.10.13 novice 検索オプションをクラスごと代入 */ DWORD CGrepAgent::DoGrep( - CEditView* pcViewDst, - bool bGrepReplace, - const CNativeW* pcmGrepKey, - const CNativeW* pcmGrepReplace, - const CNativeW* pcmGrepFile, - const CNativeW* pcmGrepFolder, - bool bGrepCurFolder, - BOOL bGrepSubFolder, - bool bGrepStdout, - bool bGrepHeader, - const SSearchOption& sSearchOption, - ECodeType nGrepCharSet, // 2002/09/21 Moca 文字コードセット選択 - int nGrepOutputLineType, - int nGrepOutputStyle, - bool bGrepOutputFileOnly, - bool bGrepOutputBaseFolder, - bool bGrepSeparateFolder, - bool bGrepPaste, - bool bGrepBackup + CEditView* pcViewDst, //!< [in] 出力先ビュー + const SGrepInput& grepInput, //!< [in] 検索入力(キー/置換/ファイル/フォルダー) + const SSearchOption& sSearchOption, //!< [in] 検索オプション + SGrepOption sGrepOption, //!< [in] Grep オプション(内部で正規化するため値渡し) + bool bGrepCurFolder, //!< [in] カレントフォルダーを変更しない + bool bGrepExcludeFileRegexp //!< [in] 除外ファイルを正規表現として扱う ) { MY_RUNNINGTIMER( cRunningTimer, L"CEditView::DoGrep" ); @@ -367,7 +370,6 @@ DWORD CGrepAgent::DoGrep( CNativeW cmemMessage; CNativeW cUnicodeBuffer; int nWork; - SGrepOption sGrepOption; /* || バッファサイズの調整 @@ -376,6 +378,8 @@ DWORD CGrepAgent::DoGrep( cUnicodeBuffer.AllocStringBuffer( 4000 ); pcViewDst->m_bDoing_UndoRedo = true; + // A-3: 以降のどの return 経路でも m_bGrepRunning / m_bDoing_UndoRedo を確実にリセットする + SGrepRunningGuard grepRunningGuard( this, pcViewDst ); /* アンドゥバッファの処理 */ if( nullptr != pcViewDst->GetDocument()->m_cDocEditor.m_pcOpeBlk ){ /* 操作ブロック */ @@ -391,38 +395,36 @@ DWORD CGrepAgent::DoGrep( pcViewDst->GetDocument()->m_cDocEditor.m_pcOpeBlk->AddRef(); pcViewDst->m_bCurSrchKeyMark = true; /* 検索文字列のマーク */ - pcViewDst->m_strCurSearchKey = pcmGrepKey->GetStringPtr(); /* 検索文字列 */ + pcViewDst->m_strCurSearchKey = grepInput.pcmGrepKey->GetStringPtr(); /* 検索文字列 */ pcViewDst->m_sCurSearchOption = sSearchOption; // 検索オプション pcViewDst->m_nCurSearchKeySequence = GetDllShareData().m_Common.m_sSearch.m_nSearchKeySequence; // 置換後文字列の準備 CNativeW cmemReplace; - if( bGrepReplace ){ - if( bGrepPaste ){ + if( sGrepOption.bGrepReplace ){ + if( sGrepOption.bGrepPaste ){ // 矩形・ラインモード貼り付けは未サポート bool bColmnSelect; bool bLineSelect = false; if( !pcViewDst->MyGetClipboardData( cmemReplace, &bColmnSelect, GetDllShareData().m_Common.m_sEdit.m_bEnableLineModePaste? &bLineSelect: nullptr ) ){ - this->m_bGrepRunning = false; - pcViewDst->m_bDoing_UndoRedo = false; ErrorMessage( pcViewDst->m_hwndParent, LS(STR_DLGREPLC_CLIPBOARD) ); return 0; } if( bLineSelect ){ int len = cmemReplace.GetStringLength(); - if( cmemReplace[len - 1] != WCODE::CR && cmemReplace[len - 1] != WCODE::LF ){ + // 空クリップボード時に cmemReplace[len - 1] が範囲外参照になるのを防ぐ + if( len == 0 || (cmemReplace[len - 1] != WCODE::CR && cmemReplace[len - 1] != WCODE::LF) ){ cmemReplace.AppendString(pcViewDst->GetDocument()->m_cDocEditor.GetNewLineCode().GetValue2()); } } if( GetDllShareData().m_Common.m_sEdit.m_bConvertEOLPaste ){ CLogicInt len = cmemReplace.GetStringLength(); - wchar_t *pszConvertedText = new wchar_t[len * 2]; // 全文字\n→\r\n変換で最大の2倍になる - auto nConvertedTextLen = pcViewDst->m_cCommander.ConvertEol(cmemReplace.GetStringPtr(), len, pszConvertedText); - cmemReplace.SetString(pszConvertedText, nConvertedTextLen); - delete [] pszConvertedText; + std::vector convertedText(std::max(1, len * 2)); // 全文字\n→\r\n変換で最大の2倍になる(len==0 でも data() が有効になるよう最低 1 確保) + auto nConvertedTextLen = pcViewDst->m_cCommander.ConvertEol(cmemReplace.GetStringPtr(), len, convertedText.data()); + cmemReplace.SetString(convertedText.data(), nConvertedTextLen); } }else{ - cmemReplace = *pcmGrepReplace; + cmemReplace = *grepInput.pcmGrepReplace; } } /* 正規表現 */ @@ -437,18 +439,16 @@ DWORD CGrepAgent::DoGrep( */ if( !pcViewDst->m_sSearchPattern.SetPattern(pcViewDst->GetHwnd(), pcViewDst->m_strCurSearchKey.c_str(), pcViewDst->m_strCurSearchKey.size(), pcViewDst->m_sCurSearchOption, &pcViewDst->m_CurRegexp) ){ - this->m_bGrepRunning = false; - pcViewDst->m_bDoing_UndoRedo = false; pcViewDst->SetUndoBuffer(); return 0; } //2014.06.13 別ウィンドウで検索したとき用にGrepダイアログの検索キーを設定 - GetEditWnd().m_cDlgGrep.m_strText = pcmGrepKey->GetStringPtr(); + GetEditWnd().m_cDlgGrep.m_strText = grepInput.pcmGrepKey->GetStringPtr(); GetEditWnd().m_cDlgGrep.m_bSetText = true; - GetEditWnd().m_cDlgGrepReplace.m_strText = pcmGrepKey->GetStringPtr(); - if( bGrepReplace ){ - GetEditWnd().m_cDlgGrepReplace.m_strText2 = pcmGrepReplace->GetStringPtr(); + GetEditWnd().m_cDlgGrepReplace.m_strText = grepInput.pcmGrepKey->GetStringPtr(); + if( sGrepOption.bGrepReplace ){ + GetEditWnd().m_cDlgGrepReplace.m_strText2 = grepInput.pcmGrepReplace->GetStringPtr(); } GetEditWnd().m_cDlgGrepReplace.m_bSetText = true; hwndCancel = cDlgCancel.DoModeless( G_AppInstance(), pcViewDst->m_hwndParent, IDD_GREPRUNNING ); @@ -460,7 +460,7 @@ DWORD CGrepAgent::DoGrep( // 2008.12.13 genta パターンが長すぎる場合は登録しない // (正規表現が途中で途切れると困るので) // 2011.12.10 Moca 表示の際に...に切り捨てられるので登録するように - wcsncpy_s( CAppMode::getInstance()->m_szGrepKey, int(std::size(CAppMode::getInstance()->m_szGrepKey)), pcmGrepKey->GetStringPtr(), _TRUNCATE ); + wcsncpy_s( CAppMode::getInstance()->m_szGrepKey, int(std::size(CAppMode::getInstance()->m_szGrepKey)), grepInput.pcmGrepKey->GetStringPtr(), _TRUNCATE ); this->m_bGrepMode = true; // 2007.07.22 genta @@ -469,38 +469,23 @@ DWORD CGrepAgent::DoGrep( { /* 検索パターンのコンパイル */ bool bError; - if( bGrepReplace && !bGrepPaste ){ + if( sGrepOption.bGrepReplace && !sGrepOption.bGrepPaste ){ // Grep置換 // 2015.03.03 Grep置換がoptGlobalじゃないバグを修正 - bError = !pattern.SetPattern(pcViewDst->GetHwnd(), pcmGrepKey->GetStringPtr(), pcmGrepKey->GetStringLength(), + bError = !pattern.SetPattern(pcViewDst->GetHwnd(), grepInput.pcmGrepKey->GetStringPtr(), grepInput.pcmGrepKey->GetStringLength(), cmemReplace.GetStringPtr(), sSearchOption, &cRegexp, true); }else{ - bError = !pattern.SetPattern(pcViewDst->GetHwnd(), pcmGrepKey->GetStringPtr(), pcmGrepKey->GetStringLength(), + bError = !pattern.SetPattern(pcViewDst->GetHwnd(), grepInput.pcmGrepKey->GetStringPtr(), grepInput.pcmGrepKey->GetStringLength(), sSearchOption, &cRegexp); } if( bError ){ - this->m_bGrepRunning = false; - pcViewDst->m_bDoing_UndoRedo = false; pcViewDst->SetUndoBuffer(); return 0; } } - // Grepオプションまとめ - sGrepOption.bGrepSubFolder = FALSE != bGrepSubFolder; - sGrepOption.bGrepStdout = bGrepStdout; - sGrepOption.bGrepHeader = bGrepHeader; - sGrepOption.nGrepCharSet = nGrepCharSet; - sGrepOption.nGrepOutputLineType = nGrepOutputLineType; - sGrepOption.nGrepOutputStyle = nGrepOutputStyle; - sGrepOption.bGrepOutputFileOnly = bGrepOutputFileOnly; - sGrepOption.bGrepOutputBaseFolder = bGrepOutputBaseFolder; - sGrepOption.bGrepSeparateFolder = bGrepSeparateFolder; - sGrepOption.bGrepReplace = bGrepReplace; - sGrepOption.bGrepPaste = bGrepPaste; - sGrepOption.bGrepBackup = bGrepBackup; + // Grep否定行はGrep置換では無効 if( sGrepOption.bGrepReplace ){ - // Grep否定行はGrep置換では無効 if( sGrepOption.nGrepOutputLineType == 2 ){ sGrepOption.nGrepOutputLineType = 1; // 行単位 } @@ -520,10 +505,8 @@ DWORD CGrepAgent::DoGrep( CGrepEnumKeys cGrepEnumKeys; { - int nErrorNo = cGrepEnumKeys.SetFileKeys( pcmGrepFile->GetStringPtr() ); + int nErrorNo = cGrepEnumKeys.SetFileKeys( grepInput.pcmGrepFile->GetStringPtr(), bGrepExcludeFileRegexp ); if( nErrorNo != 0 ){ - this->m_bGrepRunning = false; - pcViewDst->m_bDoing_UndoRedo = false; pcViewDst->SetUndoBuffer(); const WCHAR* pszErrorMessage = LS(STR_GREP_ERR_ENUMKEYS0); @@ -538,41 +521,41 @@ DWORD CGrepAgent::DoGrep( } } + // 除外正規表現パターンの事前検証(UI初期化前に入力バリデーション) + // `!` 付きの除外条件は検索開始前に失敗させて、ワーカー起動後の中断を避ける。 + for( const auto& pat : cGrepEnumKeys.m_vecExceptFileRegexPatterns ){ + CBregexp testRegexp; + if( !InitRegexp( pcViewDst->m_hwndParent, testRegexp, true ) ){ + pcViewDst->SetUndoBuffer(); + return 0; + } + if( !testRegexp.Compile( pat.c_str(), CBregexp::optCaseSensitive ) ){ + ErrorMessage( pcViewDst->m_hwndParent, L"無効な除外正規表現パターン: %s", pat.c_str() ); + pcViewDst->SetUndoBuffer(); + return 0; + } + } + // 出力対象ビューのタイプ別設定(grepout固定) const STypeConfig& type = pcViewDst->m_pcEditDoc->m_cDocType.GetDocumentAttribute(); std::vector vPaths; - CreateFolders( pcmGrepFolder->GetStringPtr(), vPaths ); + CreateFolders( grepInput.pcmGrepFolder->GetStringPtr(), vPaths ); - nWork = pcmGrepKey->GetStringLength(); // 2003.06.10 Moca あらかじめ長さを計算しておく + nWork = grepInput.pcmGrepKey->GetStringLength(); // 2003.06.10 Moca あらかじめ長さを計算しておく - /* 最後にテキストを追加 */ - CNativeW cmemWork; - cmemMessage.AppendString( LS( STR_GREP_SEARCH_CONDITION ) ); //L"\r\n□検索条件 " + CNativeW formattedKey; if( 0 < nWork ){ - cmemMessage.AppendString( L"\"" ); - cmemMessage += EscapeStringLiteral(type, *pcmGrepKey); - cmemMessage.AppendString( L"\"\r\n" ); - }else{ - cmemMessage.AppendString( LS( STR_GREP_SEARCH_FILE ) ); //L"「ファイル検索」\r\n" - } - - if( bGrepReplace ){ - cmemMessage.AppendString( LS(STR_GREP_REPLACE_TO) ); - if( bGrepPaste ){ - cmemMessage.AppendString( LS(STR_GREP_PASTE_CLIPBOAD) ); - }else{ - cmemMessage.AppendString( L"\"" ); - cmemMessage += EscapeStringLiteral(type, cmemReplace); - cmemMessage.AppendString( L"\"\r\n" ); - } + // ヘッダ文字列は個別に整形してから共通関数へ渡し、既存の表示順を保つ。 + formattedKey += EscapeStringLiteral(type, *grepInput.pcmGrepKey); } + CNativeW formattedTarget; HWND hWndTarget = nullptr; WCHAR szWindowName[_MAX_PATH]; WCHAR szWindowPath[_MAX_PATH]; { - int nHwndRet = GetHwndTitle(hWndTarget, &cmemWork, szWindowName, szWindowPath, pcmGrepFile->GetStringPtr()); + int nHwndRet = GetHwndTitle(hWndTarget, &formattedTarget, szWindowName, szWindowPath, grepInput.pcmGrepFile->GetStringPtr()); if( -1 == nHwndRet ){ cmemMessage.AppendString(L"HWND handle error.\n"); if( sGrepOption.bGrepHeader ){ @@ -581,116 +564,68 @@ DWORD CGrepAgent::DoGrep( return 0; }else if( 0 == nHwndRet ){ { - // 解析済みのファイルパターン配列を取得する const auto& vecSearchFileKeys = cGrepEnumKeys.m_vecSearchFileKeys; std::wstring strPatterns = FormatPathList( vecSearchFileKeys ); - cmemWork.SetString( strPatterns.c_str(), strPatterns.length() ); + formattedTarget.SetString( strPatterns.c_str(), strPatterns.length() ); } } } - cmemMessage.AppendString( LS( STR_GREP_SEARCH_TARGET ) ); //L"検索対象 " - cmemMessage += cmemWork; - cmemMessage.AppendString( L"\r\n" ); - cmemMessage.AppendString( LS( STR_GREP_SEARCH_FOLDER ) ); //L"フォルダー " + CNativeW formattedFolder; { - // フォルダーリストから末尾のバックスラッシュを削ったパスリストを作る std::list folders; std::transform( vPaths.cbegin(), vPaths.cend(), std::back_inserter( folders ), []( const auto& path ) { return ChopYen( path ); } ); std::wstring strPatterns = FormatPathList( folders ); - cmemMessage.AppendString( strPatterns.c_str(), strPatterns.length() ); + formattedFolder.AppendString( strPatterns.c_str(), strPatterns.length() ); } - cmemMessage.AppendString( L"\r\n" ); - cmemMessage.AppendString(LS(STR_GREP_EXCLUDE_FILE)); //L"除外ファイル " + CNativeW formattedReplace; + if( sGrepOption.bGrepReplace && !sGrepOption.bGrepPaste ){ + formattedReplace += EscapeStringLiteral(type, cmemReplace); + } + CNativeW header = BuildGrepHeader( + formattedKey.GetStringPtr(), + formattedTarget.GetStringPtr(), + formattedFolder.GetStringPtr(), + sSearchOption, + sGrepOption, + sGrepOption.bGrepReplace ? formattedReplace.GetStringPtr() : nullptr + ); + + CNativeW excludeString; + excludeString.AppendString(LS(STR_GREP_EXCLUDE_FILE)); { - // 除外ファイルの解析済みリストを取得る auto excludeFiles = cGrepEnumKeys.GetExcludeFiles(); std::wstring strPatterns = FormatPathList( excludeFiles ); - cmemMessage.AppendString( strPatterns.c_str(), strPatterns.length() ); + excludeString.AppendString( strPatterns.c_str(), strPatterns.length() ); } - cmemMessage.AppendString(L"\r\n"); + excludeString.AppendString(L"\r\n"); - cmemMessage.AppendString(LS(STR_GREP_EXCLUDE_FOLDER)); //L"除外フォルダー " + excludeString.AppendString(LS(STR_GREP_EXCLUDE_FOLDER)); { - // 除外フォルダーの解析済みリストを取得する auto excludeFolders = cGrepEnumKeys.GetExcludeFolders(); std::wstring strPatterns = FormatPathList( excludeFolders ); - cmemMessage.AppendString( strPatterns.c_str(), strPatterns.length() ); + excludeString.AppendString( strPatterns.c_str(), strPatterns.length() ); } - cmemMessage.AppendString(L"\r\n"); - - const wchar_t* pszWork; + excludeString.AppendString(L"\r\n"); + if( sGrepOption.bGrepSubFolder ){ - pszWork = LS( STR_GREP_SUBFOLDER_YES ); //L" (サブフォルダーも検索)\r\n" + excludeString.AppendString( LS(STR_GREP_SUBFOLDER_YES) ); + header.Replace( LS(STR_GREP_SUBFOLDER_YES), excludeString.GetStringPtr() ); }else{ - pszWork = LS( STR_GREP_SUBFOLDER_NO ); //L" (サブフォルダーを検索しない)\r\n" - } - cmemMessage.AppendString( pszWork ); - - if( 0 < nWork ){ // 2003.06.10 Moca ファイル検索の場合は表示しない // 2004.09.26 条件誤り修正 - if( sSearchOption.bWordOnly ){ - /* 単語単位で探す */ - cmemMessage.AppendString( LS( STR_GREP_COMPLETE_WORD ) ); //L" (単語単位で探す)\r\n" - } - - if( sSearchOption.bLoHiCase ){ - pszWork = LS( STR_GREP_CASE_SENSITIVE ); //L" (英大文字小文字を区別する)\r\n" - }else{ - pszWork = LS( STR_GREP_IGNORE_CASE ); //L" (英大文字小文字を区別しない)\r\n" - } - cmemMessage.AppendString( pszWork ); - - if( sSearchOption.bRegularExp ){ - // 2007.07.22 genta : 正規表現ライブラリのバージョンも出力する - cmemMessage.AppendString( LS( STR_GREP_REGEX_DLL ) ); //L" (正規表現:" - cmemMessage.AppendString( cRegexp.GetVersionW() ); - cmemMessage.AppendString( L")\r\n" ); - } - } - - if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ - cmemMessage.AppendString( LS( STR_GREP_CHARSET_AUTODETECT ) ); //L" (文字コードセットの自動判別)\r\n" - }else if(IsValidCodeOrCPType(sGrepOption.nGrepCharSet)){ - cmemMessage.AppendString( LS( STR_GREP_CHARSET ) ); //L" (文字コードセット:" - WCHAR szCpName[100]; - CCodePage::GetNameNormal(szCpName, sGrepOption.nGrepCharSet); - cmemMessage.AppendString( szCpName ); - cmemMessage.AppendString( L")\r\n" ); + excludeString.AppendString( LS(STR_GREP_SUBFOLDER_NO) ); + header.Replace( LS(STR_GREP_SUBFOLDER_NO), excludeString.GetStringPtr() ); } - if( 0 < nWork ){ // 2003.06.10 Moca ファイル検索の場合は表示しない // 2004.09.26 条件誤り修正 - if( sGrepOption.nGrepOutputLineType == 1 ){ - /* 該当行 */ - pszWork = LS( STR_GREP_SHOW_MATCH_LINE ); //L" (一致した行を出力)\r\n" - }else if( sGrepOption.nGrepOutputLineType == 2 ){ - // 否該当行 - pszWork = LS( STR_GREP_SHOW_MATCH_NOHITLINE ); //L" (一致しなかった行を出力)\r\n" - }else{ - if( bGrepReplace && sSearchOption.bRegularExp && !bGrepPaste ){ - pszWork = LS(STR_GREP_SHOW_FIRST_LINE); - }else{ - pszWork = LS( STR_GREP_SHOW_MATCH_AREA ); - } - } - cmemMessage.AppendString( pszWork ); - - if( sGrepOption.bGrepOutputFileOnly ){ - pszWork = LS( STR_GREP_SHOW_FIRST_MATCH ); //L" (ファイル毎最初のみ検索)\r\n" - cmemMessage.AppendString( pszWork ); - } - } + cmemMessage.AppendNativeData( header ); - cmemMessage.AppendString( L"\r\n\r\n" ); nWork = cmemMessage.GetStringLength(); - pszWork = cmemMessage.GetStringPtr(); //@@@ 2002.01.03 YAZAKI Grep直後はカーソルをGrep直前の位置に動かす CLayoutInt tmp_PosY_Layout = pcViewDst->m_pcEditDoc->m_cLayoutMgr.GetLineCount(); if( 0 < nWork && sGrepOption.bGrepHeader ){ AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); } cmemMessage._SetStringLength(0); - pszWork = nullptr; // 2007.07.22 genta バージョンを取得するために, // 正規表現の初期化を上へ移動 @@ -728,7 +663,7 @@ DWORD CGrepAgent::DoGrep( pcViewDst, &cDlgCancel, hwnd, - pcmGrepKey->GetStringPtr(), + grepInput.pcmGrepKey->GetStringPtr(), szWindowName, sSearchOption, sGrepOption, @@ -758,14 +693,15 @@ DWORD CGrepAgent::DoGrep( cmemMessage.Clear(); } nHitCount = nGrepTreeResult; - }else{ + }else if( sGrepOption.bGrepReplace ){ + // Grep置換は副作用(ファイル書き換え)を伴うため既存の直列処理を維持する for( int nPath = 0; nPath < (int)vPaths.size(); nPath++ ){ bool bOutputBaseFolder = false; std::wstring sPath = ChopYen( vPaths[nPath] ); int nTreeRet = DoGrepTree( pcViewDst, &cDlgCancel, - pcmGrepKey->GetStringPtr(), + grepInput.pcmGrepKey->GetStringPtr(), cmemReplace, cGrepEnumKeys, cGrepExceptAbsFiles, @@ -776,7 +712,6 @@ DWORD CGrepAgent::DoGrep( sGrepOption, pattern, &cRegexp, - 0, bOutputBaseFolder, &nHitCount, cmemMessage, @@ -792,6 +727,15 @@ DWORD CGrepAgent::DoGrep( AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); cmemMessage._SetStringLength(0); } + }else{ + int parallelHitCount = 0; + const SGrepSearchParams searchParams{ grepInput.pcmGrepKey->GetStringPtr(), sSearchOption, sGrepOption }; + SGrepEnumContext enumCtx{ cGrepEnumKeys, cGrepExceptAbsFiles, cGrepExceptAbsFolders }; + nGrepTreeResult = RunParallelGrep( + pcViewDst, &cDlgCancel, + searchParams, enumCtx, vPaths, + cmemMessage, parallelHitCount ); + nHitCount = parallelHitCount; } if( -1 == nGrepTreeResult && sGrepOption.bGrepHeader ){ const wchar_t* p = LS( STR_GREP_SUSPENDED ); //L"中断しました。\r\n" @@ -800,18 +744,12 @@ DWORD CGrepAgent::DoGrep( AddTail( pcViewDst, cmemSuspend, sGrepOption.bGrepStdout ); } if( sGrepOption.bGrepHeader ){ - WCHAR szBuffer[128]; - if( bGrepReplace ){ - auto_sprintf( szBuffer, LS(STR_GREP_REPLACE_COUNT), nHitCount ); - }else{ - auto_sprintf( szBuffer, LS( STR_GREP_MATCH_COUNT ), nHitCount ); - } - CNativeW cmemOutput; - cmemOutput.SetString( szBuffer ); + CNativeW cmemOutput = BuildGrepFooter(nHitCount, sGrepOption.bGrepReplace); AddTail( pcViewDst, cmemOutput, sGrepOption.bGrepStdout ); #if defined(_DEBUG) && defined(TIME_MEASURE) - auto_sprintf( szBuffer, LS(STR_GREP_TIMER), cRunningTimer.Read() ); - cmemOutput.SetString( szBuffer ); + WCHAR szTimerBuffer[kGrepFooterBufSize]; + auto_sprintf( szTimerBuffer, LS(STR_GREP_TIMER), cRunningTimer.Read() ); + cmemOutput.SetString( szTimerBuffer ); AddTail( pcViewDst, cmemOutput, sGrepOption.bGrepStdout ); #endif } @@ -849,6 +787,56 @@ DWORD CGrepAgent::DoGrep( return nHitCount; } +/*! @brief 除外正規表現パターンを "(?:p1)|(?:p2)|..." の1本に結合する(P4) + @note ファイルあたりの Match 呼び出しを 1 回にするための前処理。 + 結合パターンのコンパイル失敗時は呼び出し側で個別パターン方式にフォールバックする。 +*/ +static std::wstring BuildCombinedExcludePattern( const std::vector& patterns ) +{ + std::wstring combined; + for( const auto& pat : patterns ){ + if( !combined.empty() ){ + combined += L"|"; + } + combined += L"(?:"; + combined += pat; + combined += L")"; + } + return combined; +} + +/*! @brief 除外正規表現パターン群が P4 結合可能か判定する + @note "(?:p1)|(?:p2)" 結合はキャプチャグループ番号が全パターン通しで振り直される。 + 後方参照(\1~\9、\k、\g)や名前付きグループを含むパターンは結合すると + 参照先が変わり意味が変化する(コンパイルは成功するためコンパイル失敗 + フォールバックも働かない)。該当パターンを含む場合は結合せず個別コンパイル + 方式を使う(安全側の誤判定は性能低下のみで動作は変わらない)。 +*/ +static bool CanCombineExcludePatterns( const std::vector& patterns ) +{ + for( const auto& pat : patterns ){ + for( size_t i = 0; i + 1 < pat.size(); i++ ){ + if( pat[i] == L'\\' ){ + const wchar_t c = pat[i + 1]; + if( (L'1' <= c && c <= L'9') || c == L'k' || c == L'g' ){ + return false; // \1~\9 / \k / \g + } + i++; // エスケープ対象文字を読み飛ばす("\\\\1" の誤検知防止) + continue; + } + if( pat[i] == L'(' && pat[i + 1] == L'?' && i + 2 < pat.size() ){ + const wchar_t c2 = pat[i + 2]; + const bool bLookBehind = ( c2 == L'<' && i + 3 < pat.size() + && (pat[i + 3] == L'=' || pat[i + 3] == L'!') ); + if( (c2 == L'<' && !bLookBehind) || c2 == L'\'' ){ + return false; // (?...) / (?'name'...) 名前付きグループ + } + } + } + } + return true; +} + /*! @brief Grep実行 @date 2001.06.27 genta 正規表現ライブラリの差し替え @@ -871,11 +859,11 @@ int CGrepAgent::DoGrepTree( const SGrepOption& sGrepOption, //!< [in] Grepオプション const CSearchStringPattern& pattern, //!< [in] 検索パターン CBregexp* pRegexp, //!< [in] 正規表現コンパイルデータ。既にコンパイルされている必要がある - int nNest, //!< [in] ネストレベル bool& bOutputBaseFolder, //!< [i/o] ベースフォルダー名出力 int* pnHitCount, //!< [i/o] ヒット数の合計 CNativeW& cmemMessage, //!< [i/o] Grep結果文字列 - CNativeW& cUnicodeBuffer + CNativeW& cUnicodeBuffer, + std::vector* pExclRegexps //!< [in] コンパイル済み除外正規表現(nullptr: 内部でコンパイル) ) { int i; @@ -884,11 +872,54 @@ int CGrepAgent::DoGrepTree( int nWork = 0; int nHitCountOld = -100; bool bOutputFolderName = false; - auto nBasePathLen = int(wcslen(pszBasePath)); + auto nBasePathLen = int(std::wstring_view(pszBasePath).length()); + + // B-1: goto cancel_return の代替。残存メッセージをフラッシュしてキャンセル値(-1)を返す。 + auto FlushAndCancel = [&]() -> int { + if( 0 < cmemMessage.GetStringLength() ){ + AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); + cmemMessage._SetStringLength(0); + } + return -1; + }; + CGrepEnumOptions cGrepEnumOptions; CGrepEnumFilterFiles cGrepEnumFilterFiles; cGrepEnumFilterFiles.Enumerates( pszPath, cGrepEnumKeys, cGrepEnumOptions, cGrepExceptAbsFiles ); + // P7: ループ内での文字列再構築を避けるため、パス部分を先に構築してファイル名部分だけ差し替える + std::wstring currentFile = pszPath; + currentFile += L"\\"; + const size_t nCurrentDirLen = currentFile.size(); + + // 正規表現除外パターン: 呼び出し元からコンパイル済みが渡された場合は使い回す(再帰コスト削減) + // 渡されなかった場合(最上位呼び出し)のみここでコンパイルする + std::vector localExclRegexps; + std::vector* pVecExclRegexps = pExclRegexps; + + if( pVecExclRegexps == nullptr ){ + // 再帰呼び出しでは同じ除外正規表現を共有し、無駄な再コンパイルを防ぐ。 + // resize() は MoveInsertable を要求するが CBregexp はムーブ不可(CDllImp が= delete) + // vector(n) はデフォルト構築のみで済むため、ベクターのムーブ代入で代替する + const auto& vecPatterns = cGrepEnumKeys.m_vecExceptFileRegexPatterns; + if( !vecPatterns.empty() ){ + // P4: 全除外パターンを1本に結合し、ファイルあたりのマッチ回数を1回にする + localExclRegexps = std::vector( 1 ); + InitRegexp( nullptr, localExclRegexps[0], false ); + // 後方参照・名前付きグループを含む場合は結合で意味が変わるため個別方式へ(P4 安全化) + if( !CanCombineExcludePatterns( vecPatterns ) + || !localExclRegexps[0].Compile( BuildCombinedExcludePattern( vecPatterns ).c_str(), CBregexp::optCaseSensitive ) ){ + // 結合パターンのコンパイルに失敗した場合は従来の個別パターン方式にフォールバック + localExclRegexps = std::vector( vecPatterns.size() ); + for( size_t ri = 0; ri < localExclRegexps.size(); ri++ ){ + InitRegexp( nullptr, localExclRegexps[ri], false ); + localExclRegexps[ri].Compile( vecPatterns[ri].c_str(), CBregexp::optCaseSensitive ); + } + } + } + pVecExclRegexps = &localExclRegexps; + } + /* * カレントフォルダーのファイルを探索する。 */ @@ -901,11 +932,11 @@ int CGrepAgent::DoGrepTree( m_dwTickUICheck = dwNow; /* 処理中のユーザー操作を可能にする */ if( !::BlockingHook( pcDlgCancel->GetHwnd() ) ){ - goto cancel_return; + return FlushAndCancel(); } /* 中断ボタン押下チェック */ if( pcDlgCancel->IsCanceled() ){ - goto cancel_return; + return FlushAndCancel(); } /* 表示設定をチェック */ @@ -920,14 +951,25 @@ int CGrepAgent::DoGrepTree( ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURFILE, lpFileName ); } - std::wstring currentFile = pszPath; - currentFile += L"\\"; + currentFile.resize( nCurrentDirLen ); currentFile += lpFileName; int nBasePathLen2 = nBasePathLen + 1; - if( (int)wcslen(pszPath) < nBasePathLen2 ){ + if( (int)std::wstring_view(pszPath).length() < nBasePathLen2 ){ nBasePathLen2 = nBasePathLen; } + // 正規表現除外パターンによるフィルタリング(!プレフィックス指定) + { + bool bExcluded = false; + for( auto& reExcl : *pVecExclRegexps ){ + if( reExcl.Match( currentFile.c_str(), (int)currentFile.size(), 0 ) ){ + bExcluded = true; + break; + } + } + if( bExcluded ) continue; + } + /* ファイル内の検索 */ int nRet; if( sGrepOption.bGrepReplace ){ @@ -995,7 +1037,7 @@ int CGrepAgent::DoGrepTree( nHitCountOld = *pnHitCount; } if( -1 == nRet ){ - goto cancel_return; + return FlushAndCancel(); } } @@ -1017,11 +1059,11 @@ int CGrepAgent::DoGrepTree( //サブフォルダーの探索を再帰呼び出し。 /* 処理中のユーザー操作を可能にする */ if( !::BlockingHook( pcDlgCancel->GetHwnd() ) ){ - goto cancel_return; + return FlushAndCancel(); } /* 中断ボタン押下チェック */ if( pcDlgCancel->IsCanceled() ){ - goto cancel_return; + return FlushAndCancel(); } /* 表示設定をチェック */ CEditWnd::getInstance()->SetDrawSwitchOfAllViews( @@ -1049,14 +1091,14 @@ int CGrepAgent::DoGrepTree( sGrepOption, pattern, pRegexp, - nNest + 1, bOutputBaseFolder, pnHitCount, cmemMessage, - cUnicodeBuffer + cUnicodeBuffer, + pVecExclRegexps // コンパイル済みパターンを再帰に引き継ぐ(再コンパイル不要) ); if( -1 == nGrepTreeResult ){ - goto cancel_return; + return FlushAndCancel(); } ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURPATH, pszPath ); //@@@ 2002.01.10 add サブフォルダーから戻ってきたら... } @@ -1065,15 +1107,125 @@ int CGrepAgent::DoGrepTree( ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURFILE, L" " ); // 2002/09/09 Moca add return 0; +} -cancel_return:; - /* 結果出力 */ - if( 0 < cmemMessage.GetStringLength() ){ - AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); - cmemMessage._SetStringLength(0); +/*! @brief UICHECK 間隔ごとのキャンセル確認・表示設定更新 + @param[in] pcDlgCancel Cancelダイアログ(nullptr 時は何もしない) + @param[in] dwNow 現在の GetTickCount() 値 + @retval 0 続行 + @retval -1 キャンセル(BlockingHook 失敗 or 中断ボタン) +*/ +int CGrepAgent::CheckGrepCancelUI( CDlgCancel* pcDlgCancel, DWORD dwNow ) +{ + if( pcDlgCancel == nullptr || dwNow - m_dwTickUICheck <= UICHECK_INTERVAL_MILLISEC ){ + return 0; + } + m_dwTickUICheck = dwNow; + if( !::BlockingHook( pcDlgCancel->GetHwnd() ) ){ + return -1; + } + if( pcDlgCancel->IsCanceled() ){ + return -1; + } + CEditWnd::getInstance()->SetDrawSwitchOfAllViews( + 0 != ::IsDlgButtonChecked( pcDlgCancel->GetHwnd(), IDC_CHECK_REALTIMEVIEW ) + ); + return 0; +} + +/*! @brief フォルダー走査のみ行い、SGrepFileTask をベクターに積む(メインスレッド用) + @note ファイルの検索は行わない。DoGrep() からの並列化用エントリポイント。 +*/ +void CGrepAgent::DoGrepTreeEnumerate( + CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログ(nullptr 可) + const SGrepOption& sGrepOption, //!< [in] Grep オプション(パス整形フラグ用) + const SGrepEnumContext& enumCtx, //!< [in] 列挙・除外オブジェクト群 + const WCHAR* pszPath, //!< [in] 列挙対象パス + const WCHAR* pszBasePath, //!< [in] ベースパス + std::vector& vecTasks, //!< [out] 生成されたファイルタスク + bool& bCancelled //!< [out] キャンセルされたら true +) +{ + int i; + int count; + LPCWSTR lpFileName; + auto nBasePathLen = int(std::wstring_view(pszBasePath).length()); + CGrepEnumOptions cGrepEnumOptions; + CGrepEnumFilterFiles cGrepEnumFilterFiles; + cGrepEnumFilterFiles.Enumerates( pszPath, enumCtx.keys, cGrepEnumOptions, enumCtx.exceptFiles ); + + // P7: ループ内での文字列再構築を避けるため、パス部分を先に構築してファイル名部分だけ差し替える + std::wstring currentFile = pszPath; + currentFile += L"\\"; + const size_t nCurrentDirLen = currentFile.size(); + + count = cGrepEnumFilterFiles.GetCount(); + for( i = 0; i < count; i++ ){ + lpFileName = cGrepEnumFilterFiles.GetFileName( i ); + + DWORD dwNow = ::GetTickCount(); + if( CheckGrepCancelUI( pcDlgCancel, dwNow ) != 0 ){ + bCancelled = true; + return; + } + if( pcDlgCancel != nullptr && dwNow - m_dwTickUIFileName > UIFILENAME_INTERVAL_MILLISEC ){ + m_dwTickUIFileName = dwNow; + ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURFILE, lpFileName ); + } + + currentFile.resize( nCurrentDirLen ); + currentFile += lpFileName; + int nBasePathLen2 = nBasePathLen + 1; + if( (int)std::wstring_view(pszPath).length() < nBasePathLen2 ){ + nBasePathLen2 = nBasePathLen; + } + + SGrepFileTask task; + task.fullPath = currentFile; + task.fileName = lpFileName; + task.baseFolder = pszBasePath; + task.folder = ( sGrepOption.bGrepSeparateFolder && sGrepOption.bGrepOutputBaseFolder ) + ? std::wstring( pszPath + nBasePathLen2 ) : pszPath; + task.relPath = sGrepOption.bGrepSeparateFolder + ? lpFileName : currentFile.c_str() + nBasePathLen + 1; + vecTasks.push_back( std::move(task) ); + } + + if( sGrepOption.bGrepSubFolder ){ + CGrepEnumOptions cGrepEnumOptionsDir; + CGrepEnumFilterFolders cGrepEnumFilterFolders; + cGrepEnumFilterFolders.Enumerates( pszPath, enumCtx.keys, cGrepEnumOptionsDir, enumCtx.exceptFolders ); + + count = cGrepEnumFilterFolders.GetCount(); + for( i = 0; i < count; i++ ){ + lpFileName = cGrepEnumFilterFolders.GetFileName( i ); + + DWORD dwNow = ::GetTickCount(); + if( CheckGrepCancelUI( pcDlgCancel, dwNow ) != 0 ){ + bCancelled = true; + return; + } + + std::wstring currentPath = pszPath; + currentPath += L"\\"; + currentPath += lpFileName; + + DoGrepTreeEnumerate( + pcDlgCancel, + sGrepOption, + enumCtx, + currentPath.c_str(), + pszBasePath, + vecTasks, + bCancelled + ); + if( bCancelled ) return; + + if( pcDlgCancel != nullptr ) ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURPATH, pszPath ); + } } - return -1; + if( pcDlgCancel != nullptr ) ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURFILE, L" " ); } /*! @brief マッチした行番号と桁番号をGrep結果に出力する為に文字列化 @@ -1106,7 +1258,7 @@ wchar_t* lineColumnToString( *p++ = L','; p += int2dec(nColumn, p); *p++ = L')'; - *p = '\0'; + *p = L'\0'; #ifdef _DEBUG // Debug 版に限って両方実行して、両者が一致することを確認 wchar_t strWork2[requiredMinimumCapacity]; @@ -1123,25 +1275,29 @@ wchar_t* lineColumnToString( @date 2013.11.05 Moca cmemMessageに直接追加するように */ void CGrepAgent::SetGrepResult( - /* データ格納先 */ - CNativeW& cmemMessage, - /* マッチしたファイルの情報 */ - const WCHAR* pszFilePath, /*!< [in] フルパス or 相対パス*/ - const WCHAR* pszCodeName, /*!< [in] 文字コード情報." [SJIS]"とか */ - /* マッチした行の情報 */ - LONGLONG nLine, /*!< [in] マッチした行番号(1~) */ - int nColumn, /*!< [in] マッチした桁番号(1~) */ - const wchar_t* pCompareData, /*!< [in] 行の文字列 */ - int nLineLen, /*!< [in] 行の文字列の長さ */ - int nEolCodeLen, /*!< [in] EOLの長さ */ - /* マッチした文字列の情報 */ - const wchar_t* pMatchData, /*!< [in] マッチした文字列 */ - int nMatchLen, /*!< [in] マッチした文字列の長さ */ - /* オプション */ - const SGrepOption& sGrepOption + CNativeW& cmemMessage, //!< [out] 整形結果の追記先 + const WCHAR* pszFilePath, //!< [in] フルパス or 相対パス + const WCHAR* pszCodeName, //!< [in] 文字コード情報"[SJIS]"とか + const SGrepMatchInfo& mi, //!< [in] ヒット位置情報 + const SGrepOption& sGrepOption //!< [in] Grep オプション +) +{ + FormatGrepResultLine(cmemMessage, pszFilePath, pszCodeName, mi, sGrepOption); +} + +/*! @brief 検索結果 1 件分のフォーマット生成(HWND 非依存) +*/ +void CGrepAgent::FormatGrepResultLine( + CNativeW& cmemMessage, //!< [out] 整形結果の追記先 + const WCHAR* pszFilePath, //!< [in] 表示用ファイルパス + const WCHAR* pszCodeName, //!< [in] 文字コード表記("[SJIS]" 等) + const SGrepMatchInfo& mi, //!< [in] ヒット位置情報 + const SGrepOption& sGrepOption //!< [in] Grep オプション ) { - CNativeW cmemBuf(L""); + // P7: 呼び出しごとの CNativeW 生成を避け、スレッドローカルのバッファを再利用する + static thread_local CNativeW cmemBuf; + cmemBuf.SetStringHoldBuffer( L"", 0 ); wchar_t strWork[64]; const wchar_t * pDispData; int k; @@ -1154,14 +1310,14 @@ void CGrepAgent::SetGrepResult( cmemBuf.AppendString( L"・" ); } cmemBuf.AppendString( pszFilePath ); - cmemBuf.AppendString( lineColumnToString(strWork, nLine, nColumn) ); + cmemBuf.AppendString( lineColumnToString(strWork, mi.nLine, mi.nColumn) ); cmemBuf.AppendString( pszCodeName ); cmemBuf.AppendString( L": " ); nMaxOutStr = 2000; // 2003.06.10 Moca 最大長変更 } /* WZ風 */ else if( 2 == sGrepOption.nGrepOutputStyle ){ - ::auto_sprintf( strWork, L"・(%6I64d,%-5d): ", nLine, nColumn ); + ::auto_sprintf( strWork, L"・(%6I64d,%-5d): ", mi.nLine, mi.nColumn ); cmemBuf.AppendString( strWork ); nMaxOutStr = 2500; // 2003.06.10 Moca 最大長変更 } @@ -1172,22 +1328,22 @@ void CGrepAgent::SetGrepResult( /* 該当行 */ if( sGrepOption.nGrepOutputLineType != 0 ){ - pDispData = pCompareData; - k = nLineLen - nEolCodeLen; + pDispData = mi.pCompareData; + k = mi.nLineLen - mi.nEolCodeLen; if( nMaxOutStr < k ){ k = nMaxOutStr; // 2003.06.10 Moca 最大長変更 } } /* 該当部分 */ else{ - pDispData = pMatchData; - k = nMatchLen; + pDispData = mi.pMatchData; + k = mi.nMatchLen; if( nMaxOutStr < k ){ k = nMaxOutStr; // 2003.06.10 Moca 最大長変更 } // 該当部分に改行を含む場合はその改行コードをそのまま利用する(次の行に空行を作らない) // 2003.06.10 Moca k==0のときにバッファアンダーランしないように - if( 0 < k && WCODE::IsLineDelimiter(pMatchData[ k - 1 ], GetDllShareData().m_Common.m_sEdit.m_bEnableExtEol) ){ + if( 0 < k && WCODE::IsLineDelimiter(mi.pMatchData[ k - 1 ], GetDllShareData().m_Common.m_sEdit.m_bEnableExtEol) ){ bEOL = false; } } @@ -1202,7 +1358,7 @@ void CGrepAgent::SetGrepResult( static void OutputPathInfo( CNativeW& cmemMessage, - SGrepOption sGrepOption, + const SGrepOption& sGrepOption, const WCHAR* pszFullPath, const WCHAR* pszBaseFolder, const WCHAR* pszFolder, @@ -1263,80 +1419,397 @@ static void OutputPathInfo( } } -/*! - Grep実行 (CFileLoadを使ったテスト版) - - @retval -1 GREPのキャンセル - @retval それ以外 ヒット数(ファイル検索時はファイル数) - - @date 2001/06/27 genta 正規表現ライブラリの差し替え - @date 2002/08/30 Moca CFileLoadを使ったテスト版 - @date 2004/03/28 genta 不要な引数nNest, bGrepSubFolder, pszPathを削除 +/*! @brief 1 行分の検索一致を判定して結果を出力する(DoGrepFile / DoGrepFileWorker 共通化・C-1) + @note 正規表現/単語のみ/文字列の 3 分割と否ヒット行(nGrepOutputLineType==2)処理をまとめる。 + pnGlobalHitCount が非 null のときのみ *pnGlobalHitCount にも加算する(直列版用)。 + @return この行で追加されたヒット数 */ -int CGrepAgent::DoGrepFile( - CEditView* pcViewDst, //!< - CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログへのポインタ - HWND hWndTarget, //!< [in] 対象Windows(NULLでファイル) - const wchar_t* pszKey, //!< [in] 検索パターン - const WCHAR* pszFile, //!< [in] 処理対象ファイル名(表示用) - const SSearchOption& sSearchOption, //!< [in] 検索オプション - const SGrepOption& sGrepOption, //!< [in] Grepオプション - const CSearchStringPattern& pattern, //!< [in] 検索パターン - CBregexp* pRegexp, //!< [in] 正規表現コンパイルデータ。既にコンパイルされている必要がある - int* pnHitCount, //!< [i/o] ヒット数の合計.元々の値に見つかった数を加算して返す. - const WCHAR* pszFullPath, //!< [in] 処理対象ファイルパス C:\Folder\SubFolder\File.ext - const WCHAR* pszBaseFolder, //!< [in] 検索フォルダー C:\Folder - const WCHAR* pszFolder, //!< [in] サブフォルダー SubFolder (!bGrepSeparateFolder) または C:\Folder\SubFolder (!bGrepSeparateFolder) - const WCHAR* pszRelPath, //!< [in] 相対パス File.ext(bGrepSeparateFolder) または SubFolder\File.ext(!bGrepSeparateFolder) - bool& bOutputBaseFolder, //!< - bool& bOutputFolderName, //!< - CNativeW& cmemMessage, //!< [i/o] Grep結果文字列 - CNativeW& cUnicodeBuffer +int CGrepAgent::MatchAndEmitGrepLine( + const wchar_t* pLine, + int nLineLen, + LONGLONG nLine, + int nEolCodeLen, + int nKeyLen, + const std::vector>& searchWords, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption, + const CSearchStringPattern& pattern, + CBregexp* pRegexp, + const SGrepOutputPaths& paths, + bool& bOutputBaseFolder, + bool& bOutputFolderName, + BOOL& bOutFileName, + int* pnGlobalHitCount, + CNativeW& cmemMessage ) { - int nHitCount; - LONGLONG nLine; - const wchar_t* pszRes; // 2002/08/29 const付加 - ECodeType nCharCode; - const wchar_t* pCompareData; // 2002/08/29 const付加 - int nColumn; - BOOL bOutFileName; - bOutFileName = FALSE; - CEol cEol; - int nEolCodeLen; - const STypeConfigMini* type = nullptr; - if( !CDocTypeManager().GetTypeConfigMini( CDocTypeManager().GetDocumentTypeOfPath( pszFile ), &type ) ){ - return -1; - } - CFileLoadOrWnd cfl( type->m_encoding, hWndTarget ); // 2012/12/18 Uchi 検査するファイルのデフォルトの文字コードを取得する様に - int nOldPercent = 0; - - auto nKeyLen = int(wcslen(pszKey)); - // ファイル名表示 - const WCHAR* pszDispFilePath = ( sGrepOption.bGrepSeparateFolder || sGrepOption.bGrepOutputBaseFolder ) ? pszRelPath : pszFullPath; - - // ここでは正規表現コンパイルデータの初期化は不要 - - const WCHAR* pszCodeName; // 2002/08/29 const付加 - pszCodeName = L""; - nHitCount = 0; - nLine = 0; + int nHitCount = 0; + // 否ヒット行(type==2)処理で *pnGlobalHitCount を行開始値に巻き戻すための退避(直列版のみ使用) + const int nGlobalOld = ( pnGlobalHitCount != nullptr ) ? *pnGlobalHitCount : 0; - /* 検索条件が長さゼロの場合はファイル名だけ返す */ - // 2002/08/29 行ループの前からここに移動 - if( 0 == nKeyLen ){ - WCHAR szCpName[100]; - if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ - // 2003.06.10 Moca コード判別処理をここに移動. - // 判別エラーでもファイル数にカウントするため - // ファイルの日本語コードセット判別 - // 2014.06.19 Moca ファイル名のタイプ別のm_encodingに変更 - CCodeMediator cmediator( type->m_encoding ); - nCharCode = cmediator.CheckKanjiCodeOfFile( pszFullPath ); - if( !IsValidCodeOrCPType(nCharCode) ){ - pszCodeName = L" [(DetectError)]"; - }else if( IsValidCodeType(nCharCode) ){ - pszCodeName = CCodeTypeName(nCharCode).Bracket(); + /* 正規表現検索 */ + if( sSearchOption.bRegularExp ){ + int nIndex = 0; +#ifdef _DEBUG + int nIndexPrev = -1; +#endif + while( nIndex <= nLineLen && pRegexp->Match( pLine, nLineLen, nIndex ) ){ + nIndex = pRegexp->GetIndex(); + int matchlen = pRegexp->GetMatchLen(); +#ifdef _DEBUG + if( nIndex <= nIndexPrev ){ + MYTRACE( L"ERROR: CGrepAgent::MatchAndEmitGrepLine() nIndex <= nIndexPrev break \n" ); + break; + } + nIndexPrev = nIndex; +#endif + ++nHitCount; + // 直列版(DoGrepFile)のみ *pnHitCount にも加算する(並列版は nullptr) + if( pnGlobalHitCount != nullptr ){ ++(*pnGlobalHitCount); } + if( sGrepOption.nGrepOutputLineType != 2 ){ + OutputPathInfo( + cmemMessage, sGrepOption, + paths.pszFullPath, paths.pszBaseFolder, paths.pszFolder, paths.pszRelPath, paths.pszCodeName, + bOutputBaseFolder, bOutputFolderName, bOutFileName + ); + SetGrepResult( + cmemMessage, paths.pszDispFilePath, paths.pszCodeName, + SGrepMatchInfo{ nLine, nIndex + 1, pLine, nLineLen, nEolCodeLen, pLine + nIndex, matchlen }, + sGrepOption + ); + } + if( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ){ + break; + } + if( matchlen <= 0 ){ + matchlen = CNativeW::GetSizeOfChar( pLine, nLineLen, nIndex ); + if( matchlen <= 0 ){ + matchlen = 1; + } + } + nIndex += matchlen; + } + } + /* 単語のみ検索 */ + else if( sSearchOption.bWordOnly ){ + const wchar_t* pszRes; + int nMatchLen; + int nIdx = 0; + while( ( pszRes = CSearchAgent::SearchStringWord( pLine, nLineLen, nIdx, searchWords, sSearchOption.bLoHiCase, &nMatchLen ) ) != nullptr ){ + nIdx = int(pszRes - pLine + nMatchLen); + ++nHitCount; + // 直列版(DoGrepFile)のみ *pnHitCount にも加算する(並列版は nullptr) + if( pnGlobalHitCount != nullptr ){ ++(*pnGlobalHitCount); } + if( sGrepOption.nGrepOutputLineType != 2 ){ + OutputPathInfo( + cmemMessage, sGrepOption, + paths.pszFullPath, paths.pszBaseFolder, paths.pszFolder, paths.pszRelPath, paths.pszCodeName, + bOutputBaseFolder, bOutputFolderName, bOutFileName + ); + SetGrepResult( + cmemMessage, paths.pszDispFilePath, paths.pszCodeName, + SGrepMatchInfo{ nLine, int(pszRes - pLine + 1), pLine, nLineLen, nEolCodeLen, pszRes, nMatchLen }, + sGrepOption + ); + } + if( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ){ + break; + } + } + } + else{ + /* 文字列検索 */ + int nColumnPrev = 0; + const wchar_t* pCompareData = pLine; + for(;;){ + const wchar_t* pszRes = CSearchAgent::SearchString( pCompareData, nLineLen, 0, pattern ); + if( !pszRes ){ break; } + int nColumn = int(pszRes - pCompareData + 1); + ++nHitCount; + // 直列版(DoGrepFile)のみ *pnHitCount にも加算する(並列版は nullptr) + if( pnGlobalHitCount != nullptr ){ ++(*pnGlobalHitCount); } + if( sGrepOption.nGrepOutputLineType != 2 ){ + OutputPathInfo( + cmemMessage, sGrepOption, + paths.pszFullPath, paths.pszBaseFolder, paths.pszFolder, paths.pszRelPath, paths.pszCodeName, + bOutputBaseFolder, bOutputFolderName, bOutFileName + ); + SetGrepResult( + cmemMessage, paths.pszDispFilePath, paths.pszCodeName, + SGrepMatchInfo{ nLine, nColumn + nColumnPrev, pCompareData, nLineLen, nEolCodeLen, pszRes, nKeyLen }, + sGrepOption + ); + } + if( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ){ + break; + } + int nPosDiff = nColumn + nKeyLen - 1; + pCompareData += nPosDiff; + nLineLen -= nPosDiff; + nColumnPrev += nPosDiff; + } + } + + /* 否ヒット行を出力 */ + if( sGrepOption.nGrepOutputLineType == 2 ){ + bool bNoHit = ( 0 == nHitCount ); + nHitCount = 0; + // 直列版(DoGrepFile)のみ *pnHitCount を行開始値へ巻き戻す(並列版は nullptr) + if( pnGlobalHitCount != nullptr ){ + *pnGlobalHitCount = nGlobalOld; + } + if( bNoHit ){ + nHitCount++; + // 直列版(DoGrepFile)のみ *pnHitCount にも加算する(並列版は nullptr) + if( pnGlobalHitCount != nullptr ){ + (*pnGlobalHitCount)++; + } + OutputPathInfo( + cmemMessage, sGrepOption, + paths.pszFullPath, paths.pszBaseFolder, paths.pszFolder, paths.pszRelPath, paths.pszCodeName, + bOutputBaseFolder, bOutputFolderName, bOutFileName + ); + SetGrepResult( + cmemMessage, paths.pszDispFilePath, paths.pszCodeName, + SGrepMatchInfo{ nLine, 1, pLine, nLineLen, nEolCodeLen, pLine, nLineLen }, + sGrepOption + ); + } + } + + return nHitCount; +} + +/*! @brief ワーカースレッド用ファイル内Grep処理(UI更新なし・atomic cancelフラグ使用) + + フォルダーヘッダー(bOutputBaseFolder/bOutputFolderName)は呼び出し元の + 並列オーケストレーターが管理するため、本関数内では出力しない(true固定)。 + WZ風スタイルのファイルヘッダー(bOutFileName)はファイル単位なので本関数内で制御する。 + + @retval -1 キャンセル + @retval それ以外 ヒット数 +*/ +int CGrepAgent::DoGrepFileWorker( + const SGrepSearchParams& searchParams, //!< [in] 検索条件(キー/検索/Grepオプション) + const SGrepFileTask& task, //!< [in] ファイルタスク + CBregexp* pLocalRegexp, //!< [in] スレッドローカルCBregexpインスタンス + const CSearchStringPattern& localPattern, //!< [in] スレッドローカル検索パターン + const std::atomic& bCancelled, //!< [in] キャンセルフラグ + CNativeW& cmemMessage, //!< [out] 結果バッファ(スレッドローカル) + CNativeW& cUnicodeBuffer //!< [out] 行読み込みバッファ(スレッドローカル) +) +{ + const wchar_t* pszKey = searchParams.pszKey; + const SSearchOption& sSearchOption = searchParams.sSearchOption; + const SGrepOption& sGrepOption = searchParams.sGrepOption; + + int nHitCount = 0; + LONGLONG nLine = 0; + ECodeType nCharCode; + BOOL bOutFileName = FALSE; + CEol cEol; + int nEolCodeLen; + + const STypeConfigMini* type = nullptr; + if( !CDocTypeManager().GetTypeConfigMini( CDocTypeManager().GetDocumentTypeOfPath( task.fileName.c_str() ), &type ) ){ + return -1; + } + // 拡張子ベースのタイプ別設定を使うため、判定対象はフルパスではなくファイル名にする。 + CFileLoad cfl( type->m_encoding ); + + auto nKeyLen = int(std::wstring_view(pszKey).length()); + const WCHAR* pszDispFilePath = ( sGrepOption.bGrepSeparateFolder || sGrepOption.bGrepOutputBaseFolder ) + ? task.relPath.c_str() : task.fullPath.c_str(); + const WCHAR* pszCodeName = L""; + + // フォルダーヘッダーはオーケストレーター側で管理するためワーカー内では出力しない。 + bool bOutputBaseFolder = true; + bool bOutputFolderName = true; + + /* 検索条件が長さゼロの場合はファイル名だけ返す */ + if( 0 == nKeyLen ){ + WCHAR szCpName[100]; + + if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ + CCodeMediator cmediator( type->m_encoding ); + nCharCode = cmediator.CheckKanjiCodeOfFile( task.fullPath.c_str() ); + + if( !IsValidCodeOrCPType(nCharCode) ){ + pszCodeName = L" [(DetectError)]"; + }else if( IsValidCodeType(nCharCode) ){ + pszCodeName = CCodeTypeName(nCharCode).Bracket(); + }else{ + CCodePage::GetNameBracket(szCpName, nCharCode); + pszCodeName = szCpName; + } + } + { + const wchar_t* pszFormatFullPath = L""; + const wchar_t* pszFormatFilePath = L""; + const wchar_t* pszFormatFilePath2 = L""; + if( 1 == sGrepOption.nGrepOutputStyle ){ + pszFormatFullPath = L"%s%s\r\n"; + pszFormatFilePath = L"・\"%s\"%s\r\n"; + pszFormatFilePath2 = L"・\"%s\"%s\r\n"; + }else if( 2 == sGrepOption.nGrepOutputStyle ){ + pszFormatFullPath = L"■\"%s\"%s\r\n"; + pszFormatFilePath = L"◆\"%s\"%s\r\n"; + pszFormatFilePath2 = L"■\"%s\"%s\r\n"; + }else if( 3 == sGrepOption.nGrepOutputStyle ){ + pszFormatFullPath = L"%s%s\r\n"; + pszFormatFilePath = L"%s\r\n"; + pszFormatFilePath2 = L"%s\r\n"; + } + auto pszWork = std::make_unique( task.fullPath.size() + std::wstring_view(pszCodeName).length() + 10 ); + wchar_t* szWork0 = &pszWork[0]; + if( sGrepOption.bGrepOutputBaseFolder || sGrepOption.bGrepSeparateFolder ){ + auto_sprintf( szWork0, + (sGrepOption.bGrepSeparateFolder ? pszFormatFilePath : pszFormatFilePath2), + pszDispFilePath, pszCodeName ); + cmemMessage.AppendString( szWork0 ); + }else{ + auto_sprintf( szWork0, pszFormatFullPath, task.fullPath.c_str(), pszCodeName ); + cmemMessage.AppendString( szWork0 ); + } + } + return 1; + } + + try{ + nCharCode = cfl.FileOpen( task.fullPath.c_str(), true, sGrepOption.nGrepCharSet, + GetDllShareData().m_Common.m_sFile.GetAutoMIMEdecode() ); + WCHAR szCpName[100]; + if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ + if( IsValidCodeType(nCharCode) ){ + wcscpy_s( szCpName, _countof(szCpName), CCodeTypeName(nCharCode).Bracket() ); + pszCodeName = szCpName; + }else{ + CCodePage::GetNameBracket(szCpName, nCharCode); + pszCodeName = szCpName; + } + } + + if( bCancelled.load() ){ + return -1; + } + + std::vector> searchWords; + if( sSearchOption.bWordOnly ){ + CSearchAgent::CreateWordList( searchWords, pszKey, nKeyLen ); + } + + while( RESULT_FAILURE != cfl.ReadLine( &cUnicodeBuffer, &cEol ) ) + { + const wchar_t* pLine = cUnicodeBuffer.GetStringPtr(); + int nLineLen = cUnicodeBuffer.GetStringLength(); + nEolCodeLen = cEol.GetLen(); + ++nLine; + + // キャンセルチェック(32行ごと、メインスレッドのUIは不要) + if( 0 == nLine % 32 && bCancelled.load() ){ + return -1; + } + + const SGrepOutputPaths grepPaths{ task.fullPath.c_str(), task.baseFolder.c_str(), task.folder.c_str(), task.relPath.c_str(), pszDispFilePath, pszCodeName }; + nHitCount += MatchAndEmitGrepLine( + pLine, nLineLen, nLine, nEolCodeLen, nKeyLen, + searchWords, sSearchOption, sGrepOption, localPattern, pLocalRegexp, + grepPaths, bOutputBaseFolder, bOutputFolderName, bOutFileName, + nullptr, cmemMessage + ); + + if( sGrepOption.bGrepOutputFileOnly && 1 <= nHitCount ){ + break; + } + } + cfl.FileClose(); + } + catch( const CError_FileOpen& ){ + CNativeW str(LS(STR_GREP_ERR_FILEOPEN)); + str.Replace(L"%s", task.fullPath); + cmemMessage.AppendNativeData( str ); + return 0; + } + catch( const CError_FileRead& ){ + CNativeW str(LS(STR_GREP_ERR_FILEREAD)); + str.Replace(L"%s", task.fullPath); + cmemMessage.AppendNativeData( str ); + } + + return nHitCount; +} + +/*! + Grep実行 (CFileLoadを使ったテスト版) + + @retval -1 GREPのキャンセル + @retval それ以外 ヒット数(ファイル検索時はファイル数) + + @date 2001/06/27 genta 正規表現ライブラリの差し替え + @date 2002/08/30 Moca CFileLoadを使ったテスト版 + @date 2004/03/28 genta 不要な引数nNest, bGrepSubFolder, pszPathを削除 +*/ +int CGrepAgent::DoGrepFile( + CEditView* pcViewDst, //!< + CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログへのポインタ + HWND hWndTarget, //!< [in] 対象Windows(NULLでファイル) + const wchar_t* pszKey, //!< [in] 検索パターン + const WCHAR* pszFile, //!< [in] 処理対象ファイル名(表示用) + const SSearchOption& sSearchOption, //!< [in] 検索オプション + const SGrepOption& sGrepOption, //!< [in] Grepオプション + const CSearchStringPattern& pattern, //!< [in] 検索パターン + CBregexp* pRegexp, //!< [in] 正規表現コンパイルデータ。既にコンパイルされている必要がある + int* pnHitCount, //!< [i/o] ヒット数の合計.元々の値に見つかった数を加算して返す. + const WCHAR* pszFullPath, //!< [in] 処理対象ファイルパス C:\Folder\SubFolder\File.ext + const WCHAR* pszBaseFolder, //!< [in] 検索フォルダー C:\Folder + const WCHAR* pszFolder, //!< [in] サブフォルダー SubFolder (!bGrepSeparateFolder) または C:\Folder\SubFolder (!bGrepSeparateFolder) + const WCHAR* pszRelPath, //!< [in] 相対パス File.ext(bGrepSeparateFolder) または SubFolder\File.ext(!bGrepSeparateFolder) + bool& bOutputBaseFolder, //!< + bool& bOutputFolderName, //!< + CNativeW& cmemMessage, //!< [i/o] Grep結果文字列 + CNativeW& cUnicodeBuffer +) +{ + int nHitCount; + LONGLONG nLine; + ECodeType nCharCode; + BOOL bOutFileName; + bOutFileName = FALSE; + CEol cEol; + int nEolCodeLen; + const STypeConfigMini* type = nullptr; + if( !CDocTypeManager().GetTypeConfigMini( CDocTypeManager().GetDocumentTypeOfPath( pszFile ), &type ) ){ + return -1; + } + CFileLoadOrWnd cfl( type->m_encoding, hWndTarget ); // 2012/12/18 Uchi 検査するファイルのデフォルトの文字コードを取得する様に + int nOldPercent = 0; + + auto nKeyLen = int(std::wstring_view(pszKey).length()); + // ファイル名表示 + const WCHAR* pszDispFilePath = ( sGrepOption.bGrepSeparateFolder || sGrepOption.bGrepOutputBaseFolder ) ? pszRelPath : pszFullPath; + + // ここでは正規表現コンパイルデータの初期化は不要 + + const WCHAR* pszCodeName; // 2002/08/29 const付加 + pszCodeName = L""; + nHitCount = 0; + nLine = 0; + + /* 検索条件が長さゼロの場合はファイル名だけ返す */ + // 2002/08/29 行ループの前からここに移動 + if( 0 == nKeyLen ){ + WCHAR szCpName[100]; + if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ + // 2003.06.10 Moca コード判別処理をここに移動. + // 判別エラーでもファイル数にカウントするため + // ファイルの日本語コードセット判別 + // 2014.06.19 Moca ファイル名のタイプ別のm_encodingに変更 + CCodeMediator cmediator( type->m_encoding ); + nCharCode = cmediator.CheckKanjiCodeOfFile( pszFullPath ); + if( !IsValidCodeOrCPType(nCharCode) ){ + pszCodeName = L" [(DetectError)]"; + }else if( IsValidCodeType(nCharCode) ){ + pszCodeName = CCodeTypeName(nCharCode).Bracket(); }else{ CCodePage::GetNameBracket(szCpName, nCharCode); pszCodeName = szCpName; @@ -1373,7 +1846,7 @@ int CGrepAgent::DoGrepFile( X / O : (D)Folder(Abs) -> (G)RelPath(File) X / X : (H)FullPath */ - auto pszWork = std::make_unique(wcslen(pszFullPath) + wcslen(pszCodeName) + 10); + auto pszWork = std::make_unique(std::wstring_view(pszFullPath).length() + std::wstring_view(pszCodeName).length() + 10); wchar_t* szWork0 = &pszWork[0]; if( sGrepOption.bGrepOutputBaseFolder || sGrepOption.bGrepSeparateFolder ){ if( !bOutputBaseFolder && sGrepOption.bGrepOutputBaseFolder ){ @@ -1391,7 +1864,7 @@ int CGrepAgent::DoGrepFile( if( pszFolder[0] ){ auto_sprintf( szWork0, L"■\"%s\"\r\n", pszFolder ); // (C), (D) }else{ - wcscpy( szWork0, L"■\r\n" ); + wcscpy_s( szWork0, 4, L"■\r\n" ); // "■\r\n" + 終端0 = 4文字、バッファは最小でも10文字以上確保済み } cmemMessage.AppendString( szWork0 ); bOutputFolderName = true; @@ -1420,7 +1893,7 @@ int CGrepAgent::DoGrepFile( { if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ if( IsValidCodeType(nCharCode) ){ - wcscpy( szCpName, CCodeTypeName(nCharCode).Bracket() ); + wcscpy_s( szCpName, CCodeTypeName(nCharCode).Bracket() ); pszCodeName = szCpName; }else{ CCodePage::GetNameBracket(szCpName, nCharCode); @@ -1459,7 +1932,6 @@ int CGrepAgent::DoGrepFile( nEolCodeLen = cEol.GetLen(); ++nLine; - pCompareData = pLine; /* 処理中のユーザー操作を可能にする */ // 2010.08.31 間隔を1/32にする @@ -1496,173 +1968,13 @@ int CGrepAgent::DoGrepFile( ApiWrap::DlgItem_SetText( pcDlgCancel->GetHwnd(), IDC_STATIC_CURPATH, pszFolder ); } } - int nHitOldLine = nHitCount; - int nHitCountOldLine = *pnHitCount; - - /* 正規表現検索 */ - if( sSearchOption.bRegularExp ){ - int nIndex = 0; -#ifdef _DEBUG - int nIndexPrev = -1; -#endif - - // Jun. 21, 2003 genta ループ条件見直し - // マッチ箇所を1行から複数検出するケースを標準に, - // マッチ箇所を1行から1つだけ検出する場合を例外ケースととらえ, - // ループ継続・打ち切り条件(nGrepOutputLineType)を逆にした. - // Jun. 27, 2001 genta 正規表現ライブラリの差し替え - // From Here 2005.03.19 かろと もはやBREGEXP構造体に直接アクセスしない - // 2010.08.25 行頭以外で^にマッチする不具合の修正 - while( nIndex <= nLineLen && pRegexp->Match( pLine, nLineLen, nIndex ) ){ - - // パターン発見 - nIndex = pRegexp->GetIndex(); - int matchlen = pRegexp->GetMatchLen(); -#ifdef _DEBUG - if( nIndex <= nIndexPrev ){ - MYTRACE( L"ERROR: CEditView::DoGrepFile() nIndex <= nIndexPrev break \n" ); - break; - } - nIndexPrev = nIndex; -#endif - ++nHitCount; - ++(*pnHitCount); - if( sGrepOption.nGrepOutputLineType != 2 ){ - OutputPathInfo( - cmemMessage, sGrepOption, - pszFullPath, pszBaseFolder, pszFolder, pszRelPath, pszCodeName, - bOutputBaseFolder, bOutputFolderName, bOutFileName - ); - SetGrepResult( - cmemMessage, pszDispFilePath, pszCodeName, - nLine, nIndex + 1, pLine, nLineLen, nEolCodeLen, - pLine + nIndex, matchlen, sGrepOption - ); - } - // To Here 2005.03.19 かろと もはやBREGEXP構造体に直接アクセスしない - // Jun. 21, 2003 genta 行単位で出力する場合は1つ見つかれば十分 - if ( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ) { - break; - } - // 探し始める位置を補正 - // 2003.06.10 Moca マッチした文字列の後ろから次の検索を開始する - if( matchlen <= 0 ){ - matchlen = CNativeW::GetSizeOfChar( pLine, nLineLen, nIndex ); - if( matchlen <= 0 ){ - matchlen = 1; - } - } - nIndex += matchlen; - } - } - /* 単語のみ検索 */ - else if( sSearchOption.bWordOnly ){ - /* - 2002/02/23 Norio Nakatani - 単語単位のGrepを試験的に実装。単語はWhereCurrentWord()で判別してますので、 - 英単語やC/C++識別子などの検索条件ならヒットします。 - - 2002/03/06 YAZAKI - Grepにも試験導入。 - WhereCurrentWordで単語を抽出して、その単語が検索語とあっているか比較する。 - */ - int nMatchLen; - int nIdx = 0; - // Jun. 26, 2003 genta 無駄なwhileは削除 - while( ( pszRes = CSearchAgent::SearchStringWord(pLine, nLineLen, nIdx, searchWords, sSearchOption.bLoHiCase, &nMatchLen) ) != nullptr ){ - nIdx = int(pszRes - pLine + nMatchLen); - ++nHitCount; - ++(*pnHitCount); - if( sGrepOption.nGrepOutputLineType != 2 ){ - OutputPathInfo( - cmemMessage, sGrepOption, - pszFullPath, pszBaseFolder, pszFolder, pszRelPath, pszCodeName, - bOutputBaseFolder, bOutputFolderName, bOutFileName - ); - SetGrepResult( - cmemMessage, pszDispFilePath, pszCodeName, - // Jun. 25, 2002 genta - // 桁位置は1始まりなので1を足す必要がある - nLine, int(pszRes - pLine + 1), pLine, nLineLen, nEolCodeLen, - pszRes, nMatchLen, sGrepOption - ); - } - - // 2010.10.31 ryoji 行単位で出力する場合は1つ見つかれば十分 - if ( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ) { - break; - } - } - } - else { - /* 文字列検索 */ - int nColumnPrev = 0; - // Jun. 21, 2003 genta ループ条件見直し - // マッチ箇所を1行から複数検出するケースを標準に, - // マッチ箇所を1行から1つだけ検出する場合を例外ケースととらえ, - // ループ継続・打ち切り条件(nGrepOutputLineType)を逆にした. - for (;;) { - pszRes = CSearchAgent::SearchString( - pCompareData, - nLineLen, - 0, - pattern - ); - if(!pszRes)break; - - nColumn = int(pszRes - pCompareData + 1); - - ++nHitCount; - ++(*pnHitCount); - if( sGrepOption.nGrepOutputLineType != 2 ){ - OutputPathInfo( - cmemMessage, sGrepOption, - pszFullPath, pszBaseFolder, pszFolder, pszRelPath, pszCodeName, - bOutputBaseFolder, bOutputFolderName, bOutFileName - ); - SetGrepResult( - cmemMessage, pszDispFilePath, pszCodeName, - nLine, nColumn + nColumnPrev, pCompareData, nLineLen, nEolCodeLen, - pszRes, nKeyLen, sGrepOption - ); - } - - // Jun. 21, 2003 genta 行単位で出力する場合は1つ見つかれば十分 - if ( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ) { - break; - } - // 探し始める位置を補正 - // 2003.06.10 Moca マッチした文字列の後ろから次の検索を開始する - // nClom : マッチ位置 - // matchlen : マッチした文字列の長さ - int nPosDiff = nColumn += nKeyLen - 1; - pCompareData += nPosDiff; - nLineLen -= nPosDiff; - nColumnPrev += nPosDiff; - } - } - // 2014.09.23 否ヒット行を出力 - if( sGrepOption.nGrepOutputLineType == 2 ){ - bool bNoHit = nHitOldLine == nHitCount; - // ヒット数を戻す - nHitCount = nHitOldLine; - *pnHitCount = nHitCountOldLine; - // 否ヒット行だった - if( bNoHit ){ - nHitCount++; - (*pnHitCount)++; - OutputPathInfo( - cmemMessage, sGrepOption, - pszFullPath, pszBaseFolder, pszFolder, pszRelPath, pszCodeName, - bOutputBaseFolder, bOutputFolderName, bOutFileName - ); - SetGrepResult( - cmemMessage, pszDispFilePath, pszCodeName, - nLine, 1, pLine, nLineLen, nEolCodeLen, - pLine, nLineLen, sGrepOption - ); - } - } + const SGrepOutputPaths grepPaths{ pszFullPath, pszBaseFolder, pszFolder, pszRelPath, pszDispFilePath, pszCodeName }; + nHitCount += MatchAndEmitGrepLine( + pLine, nLineLen, nLine, nEolCodeLen, nKeyLen, + searchWords, sSearchOption, sGrepOption, pattern, pRegexp, + grepPaths, bOutputBaseFolder, bOutputFolderName, bOutFileName, + pnHitCount, cmemMessage + ); if( 0 < cmemMessage.GetStringLength() && (nHitCount - nOutputHitCount >= 10) && (::GetTickCount() - m_dwTickAddTail) >= ADDTAIL_INTERVAL_MILLISEC @@ -1699,7 +2011,7 @@ int CGrepAgent::DoGrepFile( class CError_WriteFileOpen { public: - virtual ~CError_WriteFileOpen(){} + virtual ~CError_WriteFileOpen() = default; }; class CWriteData{ @@ -1711,12 +2023,13 @@ class CWriteData{ ,bBom(bBom_) ,bOldSave(bOldSave_) ,bufferSize(0) - ,out(nullptr) ,pcCodeBase(CCodeFactory::CreateCodeBase(code_,0)) ,memMessage(message) { name += L".skrnew"; } + CWriteData(const CWriteData&) = delete; + CWriteData& operator=(const CWriteData&) = delete; void AppendBuffer(const CNativeW& strLine) { if( !out ){ @@ -1734,7 +2047,7 @@ class CWriteData{ { if( !out ){ try{ - out = new CBinaryOutputStream(name.c_str(), true); + out = std::make_unique(name.c_str(), true); }catch( const CError_FileOpen& ){ throw CError_WriteFileOpen(); } @@ -1761,8 +2074,7 @@ class CWriteData{ { if( nHitCount && out ){ out->Close(); - delete out; - out = nullptr; + out.reset(); if( bOldSave ){ std::wstring oldFile = fileName; oldFile += L".skrold"; @@ -1797,8 +2109,7 @@ class CWriteData{ { if( out ){ out->Close(); - delete out; - out = nullptr; + out.reset(); ::DeleteFile( name.c_str() ); } } @@ -1810,7 +2121,7 @@ class CWriteData{ bool bOldSave; size_t bufferSize; std::deque buffer; - CBinaryOutputStream* out; + std::unique_ptr out; std::unique_ptr pcCodeBase; CNativeW& memMessage; }; @@ -1847,7 +2158,7 @@ int CGrepAgent::DoGrepReplaceFile( CEol cEol; int nEolCodeLen; int nOldPercent = 0; - auto nKeyLen = int(wcslen(pszKey)); + auto nKeyLen = int(std::wstring_view(pszKey).length()); const WCHAR* pszCodeName = L""; const STypeConfigMini* type = nullptr; @@ -1855,7 +2166,7 @@ int CGrepAgent::DoGrepReplaceFile( return -1; } CFileLoad cfl( type->m_encoding ); // 2012/12/18 Uchi 検査するファイルのデフォルトの文字コードを取得する様に - bool bBom; + bool bBom = false; // ファイル名表示 const WCHAR* pszDispFilePath = ( sGrepOption.bGrepSeparateFolder || sGrepOption.bGrepOutputBaseFolder ) ? pszRelPath : pszFullPath; @@ -1869,7 +2180,7 @@ int CGrepAgent::DoGrepReplaceFile( { if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ if( IsValidCodeType(nCharCode) ){ - wcscpy( szCpName, CCodeTypeName(nCharCode).Bracket() ); + wcscpy_s( szCpName, CCodeTypeName(nCharCode).Bracket() ); pszCodeName = szCpName; }else{ CCodePage::GetNameBracket(szCpName, nCharCode); @@ -1885,7 +2196,6 @@ int CGrepAgent::DoGrepReplaceFile( if( pcDlgCancel->IsCanceled() ){ return -1; } - int nOutputHitCount = 0; std::vector > searchWords; if( sSearchOption.bWordOnly ){ @@ -1930,7 +2240,7 @@ int CGrepAgent::DoGrepReplaceFile( } } } - cOutBuffer.SetString( L"", 0 ); + cOutBuffer.SetStringHoldBuffer( L"", 0 ); // P7: 確保済みバッファを保持したまま長さ 0 に戻す bool bOutput = true; if( sGrepOption.bGrepOutputFileOnly && 1 <= nHitCount ){ bOutput = false; @@ -1963,9 +2273,7 @@ int CGrepAgent::DoGrepReplaceFile( /* Grep結果を、cmemMessageに格納する */ SetGrepResult( cmemMessage, pszDispFilePath, pszCodeName, - nLine, nIndex + 1, - pLine, nLineLen, nEolCodeLen, - pLine + nIndex, matchlen, + SGrepMatchInfo{ nLine, nIndex + 1, pLine, nLineLen, nEolCodeLen, pLine + nIndex, matchlen }, sGrepOption ); // To Here 2005.03.19 かろと もはやBREGEXP構造体に直接アクセスしない @@ -2032,8 +2340,7 @@ int CGrepAgent::DoGrepReplaceFile( cmemMessage, pszDispFilePath, pszCodeName, // Jun. 25, 2002 genta // 桁位置は1始まりなので1を足す必要がある - nLine, int(pszRes - pLine + 1), pLine, nLineLen, nEolCodeLen, - pszRes, nMatchLen, + SGrepMatchInfo{ nLine, int(pszRes - pLine + 1), pLine, nLineLen, nEolCodeLen, pszRes, nMatchLen }, sGrepOption ); if( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ){ @@ -2074,8 +2381,7 @@ int CGrepAgent::DoGrepReplaceFile( /* Grep結果を、cmemMessageに格納する */ SetGrepResult( cmemMessage, pszDispFilePath, pszCodeName, - nLine, nColumn + nColumnPrev + 1, pLine, nLineLen, nEolCodeLen, - pszRes, nKeyLen, + SGrepMatchInfo{ nLine, nColumn + nColumnPrev + 1, pLine, nLineLen, nEolCodeLen, pszRes, nKeyLen }, sGrepOption ); if( sGrepOption.nGrepOutputLineType != 0 || sGrepOption.bGrepOutputFileOnly ){ @@ -2105,7 +2411,6 @@ int CGrepAgent::DoGrepReplaceFile( if( 0 < cmemMessage.GetStringLength() && (::GetTickCount() - m_dwTickAddTail > ADDTAIL_INTERVAL_MILLISEC) ){ - nOutputHitCount = nHitCount; AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); cmemMessage._SetStringLength(0); } @@ -2136,3 +2441,598 @@ int CGrepAgent::DoGrepReplaceFile( return nHitCount; } + +// 結果ヘッダ生成("検索条件 ...") +CNativeW CGrepAgent::BuildGrepHeader( + const wchar_t* pszKey, + const wchar_t* pszFile, + const wchar_t* pszFolder, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption, + const wchar_t* pszReplace +) +{ + if( pszKey == nullptr ) pszKey = L""; + if( pszFile == nullptr ) pszFile = L""; + if( pszFolder == nullptr ) pszFolder = L""; + + CNativeW cmemMessage; + auto nWork = (int)std::wstring_view(pszKey).length(); + + cmemMessage.AppendString( LS( STR_GREP_SEARCH_CONDITION ) ); + if( 0 < nWork ){ + cmemMessage.AppendString( L"\"" ); + cmemMessage.AppendString( pszKey ); + cmemMessage.AppendString( L"\"\r\n" ); + }else{ + cmemMessage.AppendString( LS( STR_GREP_SEARCH_FILE ) ); + } + + if( sGrepOption.bGrepReplace ){ + cmemMessage.AppendString( LS( STR_GREP_REPLACE_TO ) ); + if( sGrepOption.bGrepPaste ){ + cmemMessage.AppendString( LS( STR_GREP_PASTE_CLIPBOAD ) ); + }else if( pszReplace != nullptr ){ + cmemMessage.AppendString( L"\"" ); + cmemMessage.AppendString( pszReplace ); + cmemMessage.AppendString( L"\"\r\n" ); + } + } + + cmemMessage.AppendString( LS( STR_GREP_SEARCH_TARGET ) ); + cmemMessage.AppendString( pszFile ); + cmemMessage.AppendString( L"\r\n" ); + + cmemMessage.AppendString( LS( STR_GREP_SEARCH_FOLDER ) ); + cmemMessage.AppendString( pszFolder ); + cmemMessage.AppendString( L"\r\n" ); + + const wchar_t* pszWork; + if( sGrepOption.bGrepSubFolder ){ + pszWork = LS( STR_GREP_SUBFOLDER_YES ); + }else{ + pszWork = LS( STR_GREP_SUBFOLDER_NO ); + } + cmemMessage.AppendString( pszWork ); + + if( 0 < nWork ){ + if( sSearchOption.bWordOnly ){ + cmemMessage.AppendString( LS( STR_GREP_COMPLETE_WORD ) ); + } + if( sSearchOption.bLoHiCase ){ + pszWork = LS( STR_GREP_CASE_SENSITIVE ); + }else{ + pszWork = LS( STR_GREP_IGNORE_CASE ); + } + cmemMessage.AppendString( pszWork ); + + if( sSearchOption.bRegularExp ){ + CBregexp cRegexp; + cmemMessage.AppendString( LS( STR_GREP_REGEX_DLL ) ); + cmemMessage.AppendString( cRegexp.GetVersionW() ); + cmemMessage.AppendString( L")\r\n" ); + } + } + + if( CODE_AUTODETECT == sGrepOption.nGrepCharSet ){ + cmemMessage.AppendString( LS( STR_GREP_CHARSET_AUTODETECT ) ); + }else if(IsValidCodeOrCPType(sGrepOption.nGrepCharSet)){ + cmemMessage.AppendString( LS( STR_GREP_CHARSET ) ); + WCHAR szCpName[100]; + CCodePage::GetNameNormal(szCpName, sGrepOption.nGrepCharSet); + cmemMessage.AppendString( szCpName ); + cmemMessage.AppendString( L")\r\n" ); + } + + if( 0 < nWork ){ + if( sGrepOption.nGrepOutputLineType == 1 ){ + pszWork = LS( STR_GREP_SHOW_MATCH_LINE ); + }else if( sGrepOption.nGrepOutputLineType == 2 ){ + pszWork = LS( STR_GREP_SHOW_MATCH_NOHITLINE ); + }else{ + if( sGrepOption.bGrepReplace && sSearchOption.bRegularExp && !sGrepOption.bGrepPaste ){ + pszWork = LS(STR_GREP_SHOW_FIRST_LINE); + }else{ + pszWork = LS( STR_GREP_SHOW_MATCH_AREA ); + } + } + cmemMessage.AppendString( pszWork ); + + if( sGrepOption.bGrepOutputFileOnly ){ + pszWork = LS( STR_GREP_SHOW_FIRST_MATCH ); + cmemMessage.AppendString( pszWork ); + } + } + + cmemMessage.AppendString( L"\r\n\r\n" ); + return cmemMessage; +} + +// 結果フッタ生成("該当 N 件" / "N 件を置換") +CNativeW CGrepAgent::BuildGrepFooter(int nHitCount, bool bGrepReplace) +{ + CNativeW cmemMessage; + WCHAR szBuffer[kGrepFooterBufSize]; + const WCHAR* pszFormat = bGrepReplace ? LS( STR_GREP_REPLACE_COUNT ) : LS( STR_GREP_MATCH_COUNT ); + + // バッファサイズの根拠: フォーマット部 ≦ 64文字 + %d 展開 ≦ 11文字 + 終端0 << 128 + // 言語リソース変更時の回帰検出のため Debug ビルドで動的検証する。 + assert(wcsnlen_s(pszFormat, kGrepFooterBufSize) + 12 < kGrepFooterBufSize); + auto_sprintf( szBuffer, pszFormat, nHitCount ); + cmemMessage.SetString( szBuffer ); + return cmemMessage; +} + +#ifdef _DEBUG +// 0-5: 性能計測基盤(Debug 限定) +// 並列 Grep の 列挙時間 / 検索時間 / resultMutex ロック待ち / バッチ終端待ち を集計し、 +// Grep 終了時に MYTRACE で出力する。Release ビルドでは一切のコードを生成しない。 +struct SGrepPerfStats { + std::atomic enumUs{ 0 }; // ファイル列挙時間(メインスレッド、μ秒) + std::atomic searchUs{ 0 }; // ファイル内検索時間(全ワーカー合算、μ秒) + std::atomic lockWaitUs{ 0 }; // resultMutex ロック待ち(全ワーカー合算、μ秒) + std::atomic batchWaitUs{ 0 }; // バッチ終端待ち(メインスレッド、μ秒) + void Trace() const { + MYTRACE( L"[GrepPerf] enum=%lldus search=%lldus lockWait=%lldus batchWait=%lldus\n", + enumUs.load(), searchUs.load(), lockWaitUs.load(), batchWaitUs.load() ); + } +}; +static long long GrepPerfNowUs() +{ + using namespace std::chrono; + return duration_cast( steady_clock::now().time_since_epoch() ).count(); +} +#define GREP_PERF_BEGIN( var ) const long long var = GrepPerfNowUs() +#define GREP_PERF_ADD( field, var ) perfStats.field.fetch_add( GrepPerfNowUs() - (var) ) +#define GREP_PERF_TRACE() perfStats.Trace() +#else +#define GREP_PERF_BEGIN( var ) ((void)0) +#define GREP_PERF_ADD( field, var ) ((void)0) +#define GREP_PERF_TRACE() ((void)0) +#endif + +/*! @brief 並列Grep結果マージ時のフォルダーヘッダー出力(重複排除つき・最初のマッチ時のみ出力) + @note resultMutex を保持した状態で呼び出すこと。 +*/ +static void AppendGrepFolderHeader( + CNativeW& sharedMessage, //!< [i/o] 共有結果バッファ + const SGrepOption& sGrepOption, //!< [in] Grep オプション + const std::wstring& baseFolder, //!< [in] ベースフォルダー + const std::wstring& folder, //!< [in] サブフォルダー + std::set>& writtenBaseFolders, //!< [i/o] 出力済みベースフォルダー + std::set>& writtenFolders //!< [i/o] 出力済みフォルダー +) +{ + if( sGrepOption.bGrepOutputBaseFolder && !writtenBaseFolders.contains(baseFolder) ){ + writtenBaseFolders.insert( baseFolder ); + sharedMessage.AppendString( sGrepOption.bGrepSeparateFolder ? L"◎\"" : L"■\"" ); + sharedMessage.AppendString( baseFolder ); + sharedMessage.AppendString( L"\"\r\n" ); + } + if( sGrepOption.bGrepSeparateFolder && !writtenFolders.contains(folder) ){ + writtenFolders.insert( folder ); + if( !folder.empty() ){ + sharedMessage.AppendString( L"■\"" ); + sharedMessage.AppendString( folder ); + sharedMessage.AppendString( L"\"\r\n" ); + }else{ + sharedMessage.AppendString( L"■\r\n" ); + } + } +} + +/*! @brief 並列Grep: スレッドプールでファイル検索を並列化する + @retval -1 キャンセル + @retval 0 完了 +*/ +int CGrepAgent::RunParallelGrep( + CEditView* pcViewDst, //!< [in] 出力先ビュー(nullptr 可) + CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログ(nullptr 可) + const SGrepSearchParams& searchParams, //!< [in] 検索条件(キー/検索/Grep オプション) + const SGrepEnumContext& enumCtx, //!< [in] 列挙・除外オブジェクト群 + const std::vector& vPaths, //!< [in] 検索対象パス群 + CNativeW& cmemMessage, //!< [out] 結果メッセージ追記先 + int& nHitCountOut //!< [out] 合計ヒット数(キャンセル時も部分値) +) +{ + int nGrepTreeResult = 0; + + const std::wstring searchKey( searchParams.pszKey ); + const auto& sSearchOption = searchParams.sSearchOption; + const auto& sGrepOption = searchParams.sGrepOption; + auto& cGrepEnumKeys = enumCtx.keys; + auto& cGrepExceptAbsFiles = enumCtx.exceptFiles; + auto& cGrepExceptAbsFolders = enumCtx.exceptFolders; + + const std::vector& regexPatterns = cGrepEnumKeys.m_vecExceptFileRegexPatterns; + + // スレッド数 = max(設定値, 論理コア数 / 4) ※設定値はiniファイルの nGrepThreadCount(1〜8) + const int nIniThreads = GetDllShareData().m_Common.m_sSearch.m_nGrepThreadCount; + const auto nClampedIni = (unsigned int)std::max( 1, std::min( nIniThreads, 8 ) ); + const unsigned int nThreads = std::max( + nClampedIni, + std::thread::hardware_concurrency() / 4 ); + + // ヒット数(全バッチ共通・バッチ間で引き継ぐ) + std::atomic atomicHitCount{ 0 }; + +#ifdef _DEBUG + // 0-5: 性能計測(Debug 限定) + SGrepPerfStats perfStats; +#endif + + // ===== スレッドプール: バッチ間でスレッドを再利用し生成・破棄コストを排除 ===== + // バッチ番号インクリメントで新バッチを通知し、condition_variable で待機中ワーカーを起床させる。 + std::mutex poolMutex; + std::condition_variable cvPoolStart; + std::condition_variable cvBatchDone; // P2-1: バッチ終端通知(poolMutex で保護) + size_t poolBatchId = 0; // バッチ番号(インクリメントで新バッチ通知) + bool bPoolShutdown = false; // true: ワーカーに終了を指示 + const std::vector* pPoolBatch = nullptr; // 現在バッチのタスクリスト + std::atomic poolNextTask{ 0 }; // 次に処理するタスクのインデックス + std::atomic poolBatchActive{ 0 }; // 現在バッチを処理中のワーカー数 + std::atomic bWorkCancelled{ false }; // true: キャンセル済み(バッチ間で維持) + std::mutex resultMutex; + CNativeW sharedMessage; + std::set> writtenBaseFolders; + std::set> writtenFolders; + + // スレッドプールのワーカーを生成(1回のみ)。 + // 各ワーカーはスレッドローカルの正規表現とバッファを持ち、毎回の再初期化を避ける。 + std::vector poolWorkers; + poolWorkers.reserve( nThreads ); + std::atomic nActiveWorkers{ nThreads }; // 初期化成功したワーカー数(デッドロック防止用) + std::atomic nInitDone{ 0 }; // 初期化完了ワーカー数(成功・失敗を含む、バリア用) + std::mutex initMutex; + std::condition_variable cvInitDone; + + struct PoolJoinGuard { + std::vector& workers; + std::mutex& mtx; + std::condition_variable& cv; + bool& bShutdown; + PoolJoinGuard( std::vector& w, std::mutex& m, std::condition_variable& c, bool& s ) + : workers(w), mtx(m), cv(c), bShutdown(s) {} + PoolJoinGuard(const PoolJoinGuard&) = delete; + PoolJoinGuard& operator=(const PoolJoinGuard&) = delete; + ~PoolJoinGuard(){ + { + std::scoped_lock lk( mtx ); + bShutdown = true; + } + cv.notify_all(); + for( auto& th : workers ){ + if( th.joinable() ) th.join(); + } + } + }; + PoolJoinGuard poolJoinGuard{ poolWorkers, poolMutex, cvPoolStart, bPoolShutdown }; + + for( unsigned int t = 0; t < nThreads; t++ ){ + poolWorkers.emplace_back( [&](){ + size_t localBatchId = 0; + + // --- スレッドローカル初期化(スレッド生存中に1回のみ実行)--- + CBregexp localRegexp; + CSearchStringPattern localPattern; + try{ + localPattern.SetPattern( nullptr, + searchKey.c_str(), searchKey.size(), + sSearchOption, &localRegexp ); + }catch(...){ + // 初期化失敗: 実働ワーカー数を減らし、バリア通知後シャットダウンまで待機 + nActiveWorkers.fetch_sub( 1 ); + { + std::scoped_lock lkInit( initMutex ); + nInitDone.fetch_add( 1 ); + } + cvInitDone.notify_one(); + std::unique_lock lk( poolMutex ); + cvPoolStart.wait( lk, [&]{ return bPoolShutdown; } ); + return; + } + + // 初期化成功: バリアへ通知 + { + std::scoped_lock lkInit( initMutex ); + nInitDone.fetch_add( 1 ); + } + cvInitDone.notify_one(); + + std::vector excludeRegexps; + if( !regexPatterns.empty() ){ + // P4: 全除外パターンを1本に結合し、ファイルあたりのマッチ回数を1回にする + excludeRegexps = std::vector( 1 ); + InitRegexp( nullptr, excludeRegexps[0], false ); + // 後方参照・名前付きグループを含む場合は結合で意味が変わるため個別方式へ(P4 安全化) + if( !CanCombineExcludePatterns( regexPatterns ) + || !excludeRegexps[0].Compile( BuildCombinedExcludePattern( regexPatterns ).c_str(), CBregexp::optCaseSensitive ) ){ + // 結合パターンのコンパイルに失敗した場合は従来の個別パターン方式にフォールバック + excludeRegexps = std::vector( regexPatterns.size() ); + for( size_t ri = 0; ri < regexPatterns.size(); ri++ ){ + InitRegexp( nullptr, excludeRegexps[ri], false ); + excludeRegexps[ri].Compile( regexPatterns[ri].c_str(), CBregexp::optCaseSensitive ); + } + } + } + + CNativeW localMessage; + CNativeW localUnicodeBuffer; + localMessage.AllocStringBuffer( 4000 ); + localUnicodeBuffer.AllocStringBuffer( 4000 ); + + // P1: 結果マージのロック競合削減 + // ヒットのたびに resultMutex を取得せず、ワーカーローカルに結果をため込み、 + // しきい値(32 ファイル分 または 約 64KB)に達した時点でまとめてマージする。 + // フォルダーヘッダー管理(writtenBaseFolders/writtenFolders)はマージ時にまとめて処理する。 + // 出力順序はもともとワーカー完了順で非決定的なため、本変更による順序仕様の変化はない。 + struct SPendingResult { + const std::wstring* pBaseFolder; //!< バッチ内の SGrepFileTask を参照(バッチ生存中のみ有効) + const std::wstring* pFolder; + CNativeW message; + }; + constexpr size_t kFlushFileCount = 32; + constexpr size_t kFlushCharCount = 64 * 1024 / sizeof(wchar_t); + std::vector pendingResults; + pendingResults.reserve( kFlushFileCount ); + size_t nPendingChars = 0; + DWORD dwLastFlushTick = ::GetTickCount(); // リアルタイム表示維持用の時間ベースフラッシュ判定 + + auto FlushPendingResults = [&](){ + if( pendingResults.empty() ) return; + GREP_PERF_BEGIN( perfLockStart ); + std::scoped_lock lk( resultMutex ); + GREP_PERF_ADD( lockWaitUs, perfLockStart ); + for( const auto& pending : pendingResults ){ + AppendGrepFolderHeader( sharedMessage, sGrepOption, + *pending.pBaseFolder, *pending.pFolder, + writtenBaseFolders, writtenFolders ); + sharedMessage.AppendNativeData( pending.message ); + } + pendingResults.clear(); + nPendingChars = 0; + dwLastFlushTick = ::GetTickCount(); + }; + + while( true ){ + // 新しいバッチまたはシャットダウンを待機 + const std::vector* pBatch = nullptr; + { + std::unique_lock lk( poolMutex ); + cvPoolStart.wait( lk, [&]{ + return poolBatchId > localBatchId || bPoolShutdown; + } ); + if( bPoolShutdown && poolBatchId <= localBatchId ) break; + localBatchId = poolBatchId; + pBatch = pPoolBatch; + } + + // バッチ処理(try/catch で例外をキャンセル扱い) + try{ + while( true ){ + const size_t idx = poolNextTask.fetch_add( 1 ); + if( idx >= pBatch->size() ) break; + if( bWorkCancelled.load() ) break; + + const SGrepFileTask& task = (*pBatch)[idx]; + + // 正規表現除外判定(フルパス全体に対してマッチング) + const auto nFullPathLen = (int)task.fullPath.size(); + bool bExcluded = false; + for( auto& reExcl : excludeRegexps ){ + if( reExcl.Match( task.fullPath.c_str(), nFullPathLen, 0 ) ){ + bExcluded = true; + break; + } + } + if( bExcluded ) continue; + + // ファイル内検索 + localMessage._SetStringLength(0); + const SGrepSearchParams workerParams{ searchKey.c_str(), sSearchOption, sGrepOption }; + GREP_PERF_BEGIN( perfSearchStart ); + const int fileHits = DoGrepFileWorker( + workerParams, task, + &localRegexp, localPattern, + bWorkCancelled, + localMessage, localUnicodeBuffer + ); + GREP_PERF_ADD( searchUs, perfSearchStart ); + + if( fileHits == -1 ){ + bWorkCancelled.store( true ); + break; + } + + if( fileHits > 0 || localMessage.GetStringLength() > 0 ){ + // P1: ここではロックを取らずローカルに蓄積し、しきい値到達時にまとめてマージする + auto& pending = pendingResults.emplace_back(); + pending.pBaseFolder = &task.baseFolder; + pending.pFolder = &task.folder; + pending.message.AppendNativeData( localMessage ); + nPendingChars += (size_t)localMessage.GetStringLength(); + atomicHitCount.fetch_add( fileHits ); + // しきい値到達、またはリアルタイム表示の遅延を押さえるため ADDTAIL_INTERVAL 経過でマージする + // (ロック取得は最大でも 1000/ADDTAIL_INTERVAL 回/秒/ワーカーに抑制される) + if( pendingResults.size() >= kFlushFileCount || nPendingChars >= kFlushCharCount + || (::GetTickCount() - dwLastFlushTick) > ADDTAIL_INTERVAL_MILLISEC ){ + FlushPendingResults(); + } + } + } + }catch(...){ + // 予期せぬ例外(std::bad_alloc 等)をキャンセル扱いにする + bWorkCancelled.store( true ); + } + // P1: ローカルにため込んだ結果を残さずマージする(キャンセル・例外経路でも実行) + try{ + FlushPendingResults(); + }catch(...){ + bWorkCancelled.store( true ); + } + // try/catch どちらの経路でも必ず実行し、デッドロックを防止する + // P2-1: ロストウェイクアップ防止のためデクリメントは poolMutex 内で行い、メインスレッドへ終端を通知する + { + std::scoped_lock lk( poolMutex ); + poolBatchActive.fetch_sub( 1 ); + } + cvBatchDone.notify_all(); + } + } ); + } + + // 全ワーカーの初期化完了を待ってからバッチ処理を開始(4-1: デッドロック防止) + { + std::unique_lock lk( initMutex ); + cvInitDone.wait( lk, [&]{ return nInitDone.load() >= nThreads; } ); + } + + // 全ワーカーが初期化に失敗した場合、結果 0 件のまま無警告で終了せずエラーとして中断する + if( nActiveWorkers.load() == 0 ){ + ErrorMessage( pcViewDst != nullptr ? pcViewDst->m_hwndParent : nullptr, + L"Grep: 検索用ワーカースレッドの初期化にすべて失敗しました。検索を中断します。" ); // TODO: Phase 5 E-1 で LS() 化 + nHitCountOut = 0; + return -1; + } + + // バッチ実行ラムダ: vecTasks をプールのワーカーに割り当て結果を出力する + // 戻り値: true=継続, false=キャンセル発生 + auto RunBatch = [&]( const std::vector& vecTasks ) { + if( vecTasks.empty() ) return true; + if( bWorkCancelled.load() ) return false; + + // バッチ設定(ロック下でバッチ番号をインクリメントしてワーカーを起床) + { + std::scoped_lock lk( poolMutex ); + pPoolBatch = &vecTasks; + poolNextTask.store( 0 ); + poolBatchActive.store( (int)nActiveWorkers.load() ); + ++poolBatchId; + } + cvPoolStart.notify_all(); + + // メインスレッド: 全ワーカーが完全終了するまでUI更新・キャンセル監視・結果フラッシュを継続 + // P2-1: Sleep(5) ポーリングを廃止し、バッチ終端は cvBatchDone で即時通知を受ける。 + // UI ポンプ(BlockingHook/進捗更新)とリアルタイム表示のフラッシュが必要なため完全ブロックにはせず、 + // ADDTAIL_INTERVAL_MILLISEC のタイムアウト付き待機で定期的にループを回す(従来の表示更新間隔を維持)。 + GREP_PERF_BEGIN( perfBatchStart ); + while( true ){ + { + std::unique_lock lk( poolMutex ); + if( cvBatchDone.wait_for( lk, std::chrono::milliseconds( ADDTAIL_INTERVAL_MILLISEC ), + [&]{ return poolBatchActive.load() <= 0; } ) ){ + break; // 全ワーカーがバッチを抜けた + } + } + + if( DWORD dwNow = ::GetTickCount(); pcDlgCancel != nullptr && dwNow - m_dwTickUICheck > UICHECK_INTERVAL_MILLISEC ){ + m_dwTickUICheck = dwNow; + if( !::BlockingHook( pcDlgCancel->GetHwnd() ) ){ + bWorkCancelled.store( true ); + } + if( pcDlgCancel->IsCanceled() ){ + bWorkCancelled.store( true ); + } + CEditWnd::getInstance()->SetDrawSwitchOfAllViews( + 0 != ::IsDlgButtonChecked( pcDlgCancel->GetHwnd(), IDC_CHECK_REALTIMEVIEW ) ); + ::SetDlgItemInt( pcDlgCancel->GetHwnd(), IDC_STATIC_HITCOUNT, + atomicHitCount.load(), FALSE ); + } + + // 共有バッファをリアルタイム出力にフラッシュ + { + std::scoped_lock lk( resultMutex ); + if( sharedMessage.GetStringLength() > 0 ){ + cmemMessage.AppendNativeData( sharedMessage ); + sharedMessage._SetStringLength(0); + } + } + if( pcViewDst != nullptr && + 0 < cmemMessage.GetStringLength() && + (::GetTickCount() - m_dwTickAddTail) > ADDTAIL_INTERVAL_MILLISEC ){ + AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); + cmemMessage._SetStringLength(0); + } + } + GREP_PERF_ADD( batchWaitUs, perfBatchStart ); + + // 最終フラッシュ + { + std::scoped_lock lk( resultMutex ); + if( sharedMessage.GetStringLength() > 0 ){ + cmemMessage.AppendNativeData( sharedMessage ); + sharedMessage._SetStringLength(0); + } + } + if( pcViewDst != nullptr && 0 < cmemMessage.GetStringLength() ){ + AddTail( pcViewDst, cmemMessage, sGrepOption.bGrepStdout ); + cmemMessage._SetStringLength(0); + } + + return !bWorkCancelled.load(); + }; + + // 各検索パスをサブフォルダー単位でバッチ処理 + for( int nPath = 0; nPath < (int)vPaths.size() && nGrepTreeResult != -1; nPath++ ){ + std::wstring sPath = ChopYen( vPaths[nPath] ); + + // 検索パスが変わるたびにフォルダーヘッダー出力履歴をリセット + { + std::scoped_lock lk( resultMutex ); + writtenBaseFolders.clear(); + writtenFolders.clear(); + } + + // バッチ0: 検索パス直下のファイル(サブフォルダーは含まない) + { + SGrepOption sGrepOptionNoSub = sGrepOption; + sGrepOptionNoSub.bGrepSubFolder = false; + bool bEnumCancelled = false; + std::vector vecTasks; + GREP_PERF_BEGIN( perfEnumStart ); + DoGrepTreeEnumerate( + pcDlgCancel, sGrepOptionNoSub, + SGrepEnumContext{ cGrepEnumKeys, cGrepExceptAbsFiles, cGrepExceptAbsFolders }, + sPath.c_str(), sPath.c_str(), + vecTasks, bEnumCancelled + ); + GREP_PERF_ADD( enumUs, perfEnumStart ); + if( bEnumCancelled ){ nGrepTreeResult = -1; break; } + if( !RunBatch(vecTasks) ){ nGrepTreeResult = -1; break; } + } + + // バッチ1..N: 直下サブフォルダーをそれぞれ独立バッチで列挙・検索・解放 + if( !sGrepOption.bGrepSubFolder || nGrepTreeResult == -1 ){ + continue; + } + CGrepEnumOptions cGrepEnumOptionsDir; + CGrepEnumFilterFolders cTopFolders; + GREP_PERF_BEGIN( perfEnumDirStart ); + cTopFolders.Enumerates( + sPath.c_str(), cGrepEnumKeys, cGrepEnumOptionsDir, cGrepExceptAbsFolders ); + GREP_PERF_ADD( enumUs, perfEnumDirStart ); + + const int nFolderCount = cTopFolders.GetCount(); + for( int fi = 0; fi < nFolderCount && nGrepTreeResult != -1; fi++ ){ + std::wstring subFolder = sPath + L"\\" + cTopFolders.GetFileName(fi); + bool bEnumCancelled = false; + std::vector vecTasks; + GREP_PERF_BEGIN( perfEnumSubStart ); + DoGrepTreeEnumerate( + pcDlgCancel, sGrepOption, + SGrepEnumContext{ cGrepEnumKeys, cGrepExceptAbsFiles, cGrepExceptAbsFolders }, + subFolder.c_str(), sPath.c_str(), + vecTasks, bEnumCancelled + ); + GREP_PERF_ADD( enumUs, perfEnumSubStart ); + if( bEnumCancelled ){ nGrepTreeResult = -1; break; } + if( !RunBatch(vecTasks) ){ nGrepTreeResult = -1; break; } + } + } + + // スレッドプールのシャットダウンと join は PoolJoinGuard のデストラクタが + // 担う(正常終了・例外伝播のどちらの経路でも確実に実行される)。 + nHitCountOut = atomicHitCount.load(); + GREP_PERF_TRACE(); + return nGrepTreeResult; +} diff --git a/sakura_core/agent/CGrepAgent.h b/sakura_core/agent/CGrepAgent.h index e344496066..31f4331d1e 100644 --- a/sakura_core/agent/CGrepAgent.h +++ b/sakura_core/agent/CGrepAgent.h @@ -1,7 +1,7 @@ /*! @file */ /* Copyright (C) 2008, kobake - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -9,7 +9,12 @@ #define SAKURA_CGREPAGENT_97F2B632_71C8_4E4A_AC42_13A6098B248F_H_ #pragma once +#include +#include +#include +#include #include "doc/CDocListener.h" +#include "extmodule/CBregexp.h" class CDlgCancel; class CEditView; class CSearchStringPattern; @@ -17,6 +22,16 @@ class CGrepEnumKeys; class CGrepEnumFiles; class CGrepEnumFolders; +//! 並列Grep処理で使用するファイルタスク情報 +// 1 ファイルを 1 タスクとして扱い、検索対象の実体情報と表示用文字列を分けて保持する。 +struct SGrepFileTask { + std::wstring fullPath; //!< 処理対象ファイルのフルパス + std::wstring fileName; //!< ファイル名(タイプ別設定取得用) + std::wstring baseFolder; //!< ベースフォルダー + std::wstring folder; //!< 表示用フォルダー(bGrepSeparateFolder時) + std::wstring relPath; //!< 相対パス(bGrepSeparateFolder時はファイル名のみ) +}; + struct SGrepOption{ bool bGrepReplace; //!< Grep置換 bool bGrepSubFolder; //!< サブフォルダーからも検索する @@ -47,6 +62,50 @@ struct SGrepOption{ {} }; +//! Grep の検索対象・除外をまとめるオブジェクト群(非所有参照) +struct SGrepEnumContext { + CGrepEnumKeys& keys; //!< [in] 検索対象/除外ファイルパターン(!で除外) + CGrepEnumFiles& exceptFiles; //!< [in] 除外ファイル(絶対パス) + CGrepEnumFolders& exceptFolders; //!< [in] 除外フォルダー(絶対パス) +}; + +//! 並列 Grep の検索条件(非所有参照) +struct SGrepSearchParams { + const wchar_t* pszKey; //!< [in] 検索パターン(c_str) + const SSearchOption& sSearchOption; //!< [in] 検索オプション + const SGrepOption& sGrepOption; //!< [in] Grep オプション +}; + +//! 検索結果 1 件のヒット位置情報 +struct SGrepMatchInfo { + LONGLONG nLine; //!< [in] ヒットした行番号 + int nColumn; //!< [in] ヒットした桁番号 + const wchar_t* pCompareData; //!< [in] 行の文字列 + int nLineLen; //!< [in] 行の文字列長 + int nEolCodeLen; //!< [in] EOL の長さ + const wchar_t* pMatchData; //!< [in] ヒットした文字列 + int nMatchLen; //!< [in] ヒットした文字列長 +}; + +//! 検索結果出力の宛先パス・表示情報(DoGrepFile / DoGrepFileWorker 共通化・静的時) +struct SGrepOutputPaths { + const WCHAR* pszFullPath; //!< [in] フルパス + const WCHAR* pszBaseFolder; //!< [in] ベースフォルダー + const WCHAR* pszFolder; //!< [in] 表示用フォルダー + const WCHAR* pszRelPath; //!< [in] 相対パス + const WCHAR* pszDispFilePath; //!< [in] 表示用ファイルパス + const WCHAR* pszCodeName; //!< [in] 文字コード表記("[SJIS]" 等) +}; + +//! DoGrep の検索入力文字列(非所有) +//! @note 全メンバーは非 null 必須(DoGrep 内で無条件に参照されるメンバーがある) +struct SGrepInput { + const CNativeW* pcmGrepKey; //!< [in] 検索キー + const CNativeW* pcmGrepReplace; //!< [in] 置換文字列 + const CNativeW* pcmGrepFile; //!< [in] 対象ファイルパターン + const CNativeW* pcmGrepFolder; //!< [in] 検索フォルダー +}; + // Jun. 26, 2001 genta 正規表現ライブラリの差し替え // Mar. 28, 2004 genta DoGrepFileから不要な引数を削除 class CGrepAgent : public CDocListenerEx{ @@ -57,35 +116,70 @@ class CGrepAgent : public CDocListenerEx{ ECallbackResult OnBeforeClose() override; void OnAfterSave(const SSaveInfo& sSaveInfo) override; + //! 検索結果 1 件分のフォーマット生成(HWND 非依存) + static void FormatGrepResultLine( + CNativeW& cmemMessage, //!< [out] 整形結果の追記先 + const WCHAR* pszFilePath, //!< [in] 表示用ファイルパス + const WCHAR* pszCodeName, //!< [in] 文字コード表記("[SJIS]" 等) + const SGrepMatchInfo& mi, //!< [in] ヒット位置情報 + const SGrepOption& sGrepOption //!< [in] Grep オプション + ); + + //! 結果ヘッダ生成("検索条件 ...") + static CNativeW BuildGrepHeader( + const wchar_t* pszKey, + const wchar_t* pszFile, + const wchar_t* pszFolder, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption, + const wchar_t* pszReplace = nullptr + ); + + //! 結果フッタ生成("該当 N 件" / "N 件を置換") + static CNativeW BuildGrepFooter(int nHitCount, bool bGrepReplace = false); + static void CreateFolders( const WCHAR* pszPath, std::vector& vPaths ); static std::wstring ChopYen( const std::wstring& str ); void AddTail( CEditView* pcEditView, const CNativeW& cmem, bool bAddStdout ); // Grep実行 DWORD DoGrep( - CEditView* pcViewDst, - bool bGrepReplace, - const CNativeW* pcmGrepKey, - const CNativeW* pcmGrepReplace, - const CNativeW* pcmGrepFile, - const CNativeW* pcmGrepFolder, - bool bGrepCurFolder, - BOOL bGrepSubFolder, - bool bGrepStdout, - bool bGrepHeader, - const SSearchOption& sSearchOption, - ECodeType nGrepCharSet, // 2002/09/21 Moca 文字コードセット選択 - int nGrepOutputLineType, - int nGrepOutputStyle, - bool bGrepOutputFileOnly, //!< [in] ファイル毎最初のみ出力 - bool bGrepOutputBaseFolder, //!< [in] ベースフォルダー表示 - bool bGrepSeparateFolder, //!< [in] フォルダー毎に表示 - bool bGrepPaste, - bool bGrepBackup + CEditView* pcViewDst, //!< [in] 出力先ビュー + const SGrepInput& grepInput, //!< [in] 検索入力(キー/置換/ファイル/フォルダー) + const SSearchOption& sSearchOption, //!< [in] 検索オプション + SGrepOption sGrepOption, //!< [in] Grep オプション(内部で正規化するため値渡し) + bool bGrepCurFolder, //!< [in] カレントフォルダーを変更しない + bool bGrepExcludeFileRegexp //!< [in] 除外ファイルを正規表現として扱う + ); + + // 並列 Grep のワーカー本体。UI に触れず、1 ファイル分の検索だけを担当する。 + // テストから直接呼び出せるように public 化。 + int DoGrepFileWorker( + const SGrepSearchParams& searchParams, //!< [in] 検索条件(キー/検索/Grep オプション) + const SGrepFileTask& task, //!< [in] 検索対象 1 ファイル + CBregexp* pLocalRegexp, //!< [in] スレッド専用正規表現(コンパイル済み) + const CSearchStringPattern& localPattern, //!< [in] スレッド専用検索パターン + const std::atomic& bCancelled, //!< [in] キャンセル要求フラグ + CNativeW& cmemMessage, //!< [out] 結果メッセージ追記先 + CNativeW& cUnicodeBuffer //!< [i/o] 変換用テンポラリ + ); + + // 並列 Grep のスレッドプール管理・バッチ駆動コア。 + // pcViewDst / pcDlgCancel は nullptr 可(nullptr 時は UI 操作をスキップ)。 + // @retval -1 キャンセル @retval 0 完了 + // @param[out] nHitCountOut 合計ヒット数(キャンセル時も部分カウントを返す) + int RunParallelGrep( + CEditView* pcViewDst, //!< [in] 出力先ビュー(nullptr 可) + CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログ(nullptr 可) + const SGrepSearchParams& searchParams, //!< [in] 検索条件(キー/検索/Grep オプション) + const SGrepEnumContext& enumCtx, //!< [in] 列挙・除外オブジェクト群 + const std::vector& vPaths, //!< [in] 検索対象パス群 + CNativeW& cmemMessage, //!< [out] 結果メッセージ追記先 + int& nHitCountOut //!< [out] 合計ヒット数(キャンセル時も部分値) ); -private: // Grep実行 + // キャンセル経路(FlushAndCancel)をテストから直接呼び出せるように public 化(DoGrepFileWorker と同様)。 int DoGrepTree( CEditView* pcViewDst, CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログへのポインタ @@ -100,14 +194,15 @@ class CGrepAgent : public CDocListenerEx{ const SGrepOption& sGrepOption, //!< [in] Grepオプション const CSearchStringPattern& pattern, //!< [in] 検索パターン CBregexp* pRegexp, //!< [in] 正規表現コンパイルデータ。既にコンパイルされている必要がある - int nNest, //!< [in] ネストレベル bool& bOutputBaseFolder, int* pnHitCount, //!< [i/o] ヒット数の合計 CNativeW& cmemMessage, - CNativeW& cUnicodeBuffer + CNativeW& cUnicodeBuffer, + std::vector* pExclRegexps = nullptr //!< [in] コンパイル済み除外正規表現(再帰呼び出し用・nullptrなら内部でコンパイル) ); // Grep実行 + // 直列経路(MatchAndEmitGrepLine の pnHitCount 非 null 分岐)をテストから直接呼び出せるように public 化(DoGrepFileWorker と同様)。 int DoGrepFile( CEditView* pcViewDst, CDlgCancel* pcDlgCancel, @@ -129,6 +224,30 @@ class CGrepAgent : public CDocListenerEx{ CNativeW& cUnicodeBuffer ); +private: + //! 1 行分の検索一致判定と結果出力(DoGrepFile / DoGrepFileWorker 共通化・C-1) + //! 正規表現/単語のみ/文字列の 3 分割と否ヒット行(nGrepOutputLineType==2)処理をまとめる。 + //! @param[in] pnGlobalHitCount 直列版は *pnHitCount を渡す。並列版は nullptr。 + //! @return この行で追加されたヒット数 + int MatchAndEmitGrepLine( + const wchar_t* pLine, //!< [in] 行の文字列 + int nLineLen, //!< [in] 行の文字列長 + LONGLONG nLine, //!< [in] 行番号 + int nEolCodeLen, //!< [in] EOL の長さ + int nKeyLen, //!< [in] 検索キー長 + const std::vector>& searchWords, //!< [in] 単語検索用(bWordOnly時) + const SSearchOption& sSearchOption, //!< [in] 検索オプション + const SGrepOption& sGrepOption, //!< [in] Grep オプション + const CSearchStringPattern& pattern, //!< [in] 検索パターン + CBregexp* pRegexp, //!< [in] コンパイル済み正規表現 + const SGrepOutputPaths& paths, //!< [in] 出力先パス情報 + bool& bOutputBaseFolder, //!< [i/o] ベースフォルダー出力済みフラグ + bool& bOutputFolderName, //!< [i/o] フォルダー名出力済みフラグ + BOOL& bOutFileName, //!< [i/o] ファイル名出力済みフラグ + int* pnGlobalHitCount, //!< [i/o] 合計ヒット数(nullptr 可) + CNativeW& cmemMessage //!< [out] 結果メッセージ追記先 + ); + int DoGrepReplaceFile( CEditView* pcViewDst, CDlgCancel* pcDlgCancel, @@ -152,24 +271,29 @@ class CGrepAgent : public CDocListenerEx{ // Grep結果をpszWorkに格納 void SetGrepResult( - // データ格納先 - CNativeW& cmemMessage, - // マッチしたファイルの情報 - const WCHAR* pszFilePath, // フルパス or 相対パス - const WCHAR* pszCodeName, // 文字コード情報"[SJIS]"とか - // マッチした行の情報 - LONGLONG nLine, // マッチした行番号 - int nColumn, // マッチした桁番号 - const wchar_t* pCompareData, // 行の文字列 - int nLineLen, // 行の文字列の長さ - int nEolCodeLen, // EOLの長さ - // マッチした文字列の情報 - const wchar_t* pMatchData, // マッチした文字列 - int nMatchLen, // マッチした文字列の長さ - // オプション - const SGrepOption& sGrepOption + CNativeW& cmemMessage, //!< [out] 整形結果の追記先 + const WCHAR* pszFilePath, //!< [in] フルパス or 相対パス + const WCHAR* pszCodeName, //!< [in] 文字コード情報"[SJIS]"とか + const SGrepMatchInfo& mi, //!< [in] ヒット位置情報 + const SGrepOption& sGrepOption //!< [in] Grep オプション + ); + + // ファイル列挙(メインスレッド用・キャンセル対応) + // 検索は行わず、ワーカーへ渡すタスクだけを作る。 + void DoGrepTreeEnumerate( + CDlgCancel* pcDlgCancel, //!< [in] Cancelダイアログ(nullptr 可) + const SGrepOption& sGrepOption, //!< [in] Grep オプション(パス整形フラグ用) + const SGrepEnumContext& enumCtx, //!< [in] 列挙・除外オブジェクト群 + const WCHAR* pszPath, //!< [in] 列挙対象パス + const WCHAR* pszBasePath, //!< [in] ベースパス + std::vector& vecTasks, //!< [out] 生成されたファイルタスク + bool& bCancelled //!< [out] キャンセルされたら true ); + // UICHECK 間隔ごとのキャンセル確認・表示設定更新(DoGrepTreeEnumerate 用) + // @retval 0 続行 / -1 キャンセル(BlockingHook 失敗 or 中断ボタン) + int CheckGrepCancelUI( CDlgCancel* pcDlgCancel, DWORD dwNow ); + DWORD m_dwTickAddTail = 0; // AddTail() を呼び出した時間 DWORD m_dwTickUICheck = 0; // 処理中にユーザーによるUI操作が行われていないか確認した時間 DWORD m_dwTickUIFileName = 0; // Cancelダイアログのファイル名表示更新を行った時間 diff --git a/sakura_core/basis/GrepInfo.h b/sakura_core/basis/GrepInfo.h index d9268a7c4b..98f9a355f8 100644 --- a/sakura_core/basis/GrepInfo.h +++ b/sakura_core/basis/GrepInfo.h @@ -1,6 +1,6 @@ /*! @file */ /* - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -39,6 +39,7 @@ struct GrepInfo { bool bGrepReplace = false; //!< Grep置換 bool bGrepPaste = false; //!< クリップボードから貼り付け bool bGrepBackup = false; //!< 置換でバックアップを保存 + bool bGrepExcludeFileRegexp = false; //!< 除外ファイルを正規表現として扱う // コンストラクタ GrepInfo() noexcept; diff --git a/sakura_core/cmd/CViewCommander_Grep.cpp b/sakura_core/cmd/CViewCommander_Grep.cpp index 9666e82f09..592961e98e 100644 --- a/sakura_core/cmd/CViewCommander_Grep.cpp +++ b/sakura_core/cmd/CViewCommander_Grep.cpp @@ -7,7 +7,7 @@ Copyright (C) 2003, MIK Copyright (C) 2005, genta Copyright (C) 2006, ryoji - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holders to use this code for other purpose. @@ -88,27 +88,30 @@ void CViewCommander::Command_GREP( void ) GetDocument()->OnChangeType(); } - CEditApp::getInstance()->m_pcGrepAgent->DoGrep( - m_pCommanderView, - false, - &cmWork1, - &cmWork4, - &cmWork2, - &cmWork3, - false, - GetEditWindow()->m_cDlgGrep.m_bSubFolder, - false, - true, // Header - GetEditWindow()->m_cDlgGrep.m_sSearchOption, - GetEditWindow()->m_cDlgGrep.m_nGrepCharSet, - GetEditWindow()->m_cDlgGrep.m_nGrepOutputLineType, - GetEditWindow()->m_cDlgGrep.m_nGrepOutputStyle, - GetEditWindow()->m_cDlgGrep.m_bGrepOutputFileOnly, - GetEditWindow()->m_cDlgGrep.m_bGrepOutputBaseFolder, - GetEditWindow()->m_cDlgGrep.m_bGrepSeparateFolder, - false, - false - ); + { + const SGrepInput grepInput{ &cmWork1, &cmWork4, &cmWork2, &cmWork3 }; + SGrepOption sGrepOption; + sGrepOption.bGrepReplace = false; + sGrepOption.bGrepSubFolder = GetEditWindow()->m_cDlgGrep.m_bSubFolder != FALSE; + sGrepOption.bGrepStdout = false; + sGrepOption.bGrepHeader = true; + sGrepOption.nGrepCharSet = GetEditWindow()->m_cDlgGrep.m_nGrepCharSet; + sGrepOption.nGrepOutputLineType = GetEditWindow()->m_cDlgGrep.m_nGrepOutputLineType; + sGrepOption.nGrepOutputStyle = GetEditWindow()->m_cDlgGrep.m_nGrepOutputStyle; + sGrepOption.bGrepOutputFileOnly = GetEditWindow()->m_cDlgGrep.m_bGrepOutputFileOnly; + sGrepOption.bGrepOutputBaseFolder = GetEditWindow()->m_cDlgGrep.m_bGrepOutputBaseFolder; + sGrepOption.bGrepSeparateFolder = GetEditWindow()->m_cDlgGrep.m_bGrepSeparateFolder; + sGrepOption.bGrepPaste = false; + sGrepOption.bGrepBackup = false; + CEditApp::getInstance()->m_pcGrepAgent->DoGrep( + m_pCommanderView, + grepInput, + GetEditWindow()->m_cDlgGrep.m_sSearchOption, + sGrepOption, + false, + GetEditWindow()->m_cDlgGrep.m_bExcludeFileRegularExp != FALSE + ); + } //プラグイン:DocumentOpenイベント実行 CJackManager::getInstance()->InvokePlugins( PP_DOCUMENT_OPEN, &GetEditWindow()->GetActiveView() ); @@ -182,32 +185,35 @@ void CViewCommander::Command_GREP_REPLACE( void ) !CAppMode::getInstance()->IsDebugMode() ) ){ - CEditApp::getInstance()->m_pcGrepAgent->DoGrep( - m_pCommanderView, - true, - &cmWork1, - &cmWork4, - &cmWork2, - &cmWork3, - false, - cDlgGrepRep.m_bSubFolder, - false, // Stdout - true, // Header - cDlgGrepRep.m_sSearchOption, - cDlgGrepRep.m_nGrepCharSet, - cDlgGrepRep.m_nGrepOutputLineType, - cDlgGrepRep.m_nGrepOutputStyle, - cDlgGrepRep.m_bGrepOutputFileOnly, - cDlgGrepRep.m_bGrepOutputBaseFolder, - cDlgGrepRep.m_bGrepSeparateFolder, - cDlgGrepRep.m_bPaste, - cDlgGrepRep.m_bBackup - ); + { + const SGrepInput grepInput{ &cmWork1, &cmWork4, &cmWork2, &cmWork3 }; + SGrepOption sGrepOption; + sGrepOption.bGrepReplace = true; + sGrepOption.bGrepSubFolder = cDlgGrepRep.m_bSubFolder != FALSE; + sGrepOption.bGrepStdout = false; + sGrepOption.bGrepHeader = true; + sGrepOption.nGrepCharSet = cDlgGrepRep.m_nGrepCharSet; + sGrepOption.nGrepOutputLineType = cDlgGrepRep.m_nGrepOutputLineType; + sGrepOption.nGrepOutputStyle = cDlgGrepRep.m_nGrepOutputStyle; + sGrepOption.bGrepOutputFileOnly = cDlgGrepRep.m_bGrepOutputFileOnly; + sGrepOption.bGrepOutputBaseFolder = cDlgGrepRep.m_bGrepOutputBaseFolder; + sGrepOption.bGrepSeparateFolder = cDlgGrepRep.m_bGrepSeparateFolder; + sGrepOption.bGrepPaste = cDlgGrepRep.m_bPaste; + sGrepOption.bGrepBackup = cDlgGrepRep.m_bBackup; + CEditApp::getInstance()->m_pcGrepAgent->DoGrep( + m_pCommanderView, + grepInput, + cDlgGrepRep.m_sSearchOption, + sGrepOption, + false, + cDlgGrepRep.m_bExcludeFileRegularExp != FALSE + ); + } } else{ // 編集ウィンドウの上限チェック if( GetDllShareData().m_sNodes.m_nEditArrNum >= MAX_EDITWINDOWS ){ //最大値修正 //@@@ 2003.05.31 MIK - OkMessage( m_pCommanderView->GetHwnd(), L"編集ウィンドウ数の上限は%dです。\nこれ以上は同時に開けません。", MAX_EDITWINDOWS ); + OkMessage( m_pCommanderView->GetHwnd(), LS(STR_MAXWINDOW), MAX_EDITWINDOWS ); return; } /*======= Grepの実行 =============*/ @@ -235,20 +241,21 @@ void CViewCommander::Command_GREP_REPLACE( void ) //GOPTオプション WCHAR pOpt[64]; pOpt[0] = L'\0'; - if( cDlgGrepRep.m_bSubFolder )wcscat( pOpt, L"S" ); // サブフォルダーからも検索する - if( cDlgGrepRep.m_sSearchOption.bWordOnly )wcscat( pOpt, L"W" ); // 単語単位で探す - if( cDlgGrepRep.m_sSearchOption.bLoHiCase )wcscat( pOpt, L"L" ); // 英大文字と英小文字を区別する - if( cDlgGrepRep.m_sSearchOption.bRegularExp )wcscat( pOpt, L"R" ); // 正規表現 - if( cDlgGrepRep.m_nGrepOutputLineType == 1 )wcscat( pOpt, L"P" ); // 行を出力する + if( cDlgGrepRep.m_bSubFolder )wcscat_s( pOpt, L"S" ); // サブフォルダーからも検索する + if( cDlgGrepRep.m_sSearchOption.bWordOnly )wcscat_s( pOpt, L"W" ); // 単語単位で探す + if( cDlgGrepRep.m_sSearchOption.bLoHiCase )wcscat_s( pOpt, L"L" ); // 英大文字と英小文字を区別する + if( cDlgGrepRep.m_sSearchOption.bRegularExp )wcscat_s( pOpt, L"R" ); // 正規表現 + if( cDlgGrepRep.m_nGrepOutputLineType == 1 )wcscat_s( pOpt, L"P" ); // 行を出力する // if( cDlgGrepRep.m_nGrepOutputLineType == 2 )wcscat( pOpt, L"N" ); // 否ヒット行を出力する 2014.09.23 - if( 1 == cDlgGrepRep.m_nGrepOutputStyle )wcscat( pOpt, L"1" ); // Grep: 出力形式 - if( 2 == cDlgGrepRep.m_nGrepOutputStyle )wcscat( pOpt, L"2" ); // Grep: 出力形式 - if( 3 == cDlgGrepRep.m_nGrepOutputStyle )wcscat( pOpt, L"3" ); - if( cDlgGrepRep.m_bGrepOutputFileOnly )wcscat( pOpt, L"F" ); - if( cDlgGrepRep.m_bGrepOutputBaseFolder )wcscat( pOpt, L"B" ); - if( cDlgGrepRep.m_bGrepSeparateFolder )wcscat( pOpt, L"D" ); - if( cDlgGrepRep.m_bPaste )wcscat( pOpt, L"C" ); // クリップボードから貼り付け - if( cDlgGrepRep.m_bBackup )wcscat( pOpt, L"O" ); // バックアップ作成 + if( 1 == cDlgGrepRep.m_nGrepOutputStyle )wcscat_s( pOpt, L"1" ); // Grep: 出力形式 + if( 2 == cDlgGrepRep.m_nGrepOutputStyle )wcscat_s( pOpt, L"2" ); // Grep: 出力形式 + if( 3 == cDlgGrepRep.m_nGrepOutputStyle )wcscat_s( pOpt, L"3" ); + if( cDlgGrepRep.m_bGrepOutputFileOnly )wcscat_s( pOpt, L"F" ); + if( cDlgGrepRep.m_bGrepOutputBaseFolder )wcscat_s( pOpt, L"B" ); + if( cDlgGrepRep.m_bGrepSeparateFolder )wcscat_s( pOpt, L"D" ); + if( cDlgGrepRep.m_bPaste )wcscat_s( pOpt, L"C" ); // クリップボードから貼り付け + if( cDlgGrepRep.m_bBackup )wcscat_s( pOpt, L"O" ); // バックアップ作成 + if( cDlgGrepRep.m_bExcludeFileRegularExp )wcscat_s( pOpt, L"E" ); // 除外ファイルを正規表現として扱う if( pOpt[0] ) { cCmdLine.AppendString( L" -GOPT=" ); cCmdLine.AppendString( pOpt ); diff --git a/sakura_core/dlg/CDlgGrep.cpp b/sakura_core/dlg/CDlgGrep.cpp index 4e14fa77e5..ba1122b17e 100644 --- a/sakura_core/dlg/CDlgGrep.cpp +++ b/sakura_core/dlg/CDlgGrep.cpp @@ -11,7 +11,7 @@ Copyright (C) 2006, ryoji Copyright (C) 2010, ryoji Copyright (C) 2012, Uchi - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holder to use this code for other purpose. @@ -20,6 +20,7 @@ #include "dlg/CDlgGrep.h" #include "agent/CGrepAgent.h" #include "grep/CGrepEnumKeys.h" +#include "grep/GrepPathFormat.h" #include "func/Funccode.h" // Stonee, 2001/03/12 #include "charset/CCodePage.h" #include "util/module.h" @@ -53,6 +54,7 @@ const DWORD p_helpids[] = { //12000 IDC_COMBO_FILE, HIDC_GREP_COMBO_FILE, //ファイル IDC_COMBO_FOLDER, HIDC_GREP_COMBO_FOLDER, //フォルダー IDC_COMBO_EXCLUDE_FILE, HIDC_GREP_COMBO_EXCLUDE_FILE, //除外ファイル + IDC_CHK_EXCLUDE_FILE_REGEXP, HIDC_GREP_COMBO_EXCLUDE_FILE, //除外ファイル正規表現 IDC_COMBO_EXCLUDE_FOLDER, HIDC_GREP_COMBO_EXCLUDE_FOLDER, //除外フォルダー IDC_BUTTON_FOLDER_UP, HIDC_GREP_BUTTON_FOLDER_UP, //上 IDC_RADIO_OUTPUTLINE, HIDC_GREP_RADIO_OUTPUTLINE, //結果出力:行単位 @@ -157,6 +159,8 @@ static void AppendExcludeFolderPatterns(CNativeW& cFilePattern, const CNativeW& static void AppendExcludeFilePatterns(CNativeW& cFilePattern, const CNativeW& cmWorkExcludeFile) { auto patterns = CGrepEnumKeys::SplitPattern(cmWorkExcludeFile.GetStringPtr()); + // 除外ファイルは常に '!' を付与する。 + // 正規表現/ワイルドカードの区別は -GOPT=E(チェックボックス m_bExcludeFileRegularExp)で持つ。 for (const auto& pattern : patterns) { LPCWSTR escapeStr = GetEscapePattern(pattern); @@ -170,7 +174,8 @@ static void AppendExcludeFilePatterns(CNativeW& cFilePattern, const CNativeW& cm CNativeW CDlgGrep::GetPackedGFileString() const { // ダイアログデータを取得 - CNativeW cmFilePattern( m_szFile ); + // ファイルパターンが空なら UI 既定値の "*.*" を補って、検索対象なしの状態を避ける。 + CNativeW cmFilePattern( m_szFile[0] != L'\0' ? m_szFile : L"*.*" ); CNativeW cmExcludeFiles( m_szExcludeFile ); CNativeW cmExcludeFolders( m_szExcludeFolder ); @@ -240,6 +245,7 @@ BOOL CDlgGrep::OnCbnDropDown( HWND hwndCtl, int wID ) int CDlgGrep::DoModal( HINSTANCE hInstance, HWND hwndParent, const WCHAR* pszCurrentFilePath ) { m_bSubFolder = m_pShareData->m_Common.m_sSearch.m_bGrepSubFolder; // Grep: サブフォルダーも検索 + m_bExcludeFileRegularExp = m_pShareData->m_Common.m_sSearch.m_bGrepExcludeFileRegexp; m_sSearchOption = m_pShareData->m_Common.m_sSearch.m_sSearchOption; // 検索オプション m_nGrepCharSet = m_pShareData->m_Common.m_sSearch.m_nGrepCharSet; // 文字コードセット m_nGrepOutputLineType = m_pShareData->m_Common.m_sSearch.m_nGrepOutputLineType; // 行を出力/該当部分/否マッチ行 を出力 @@ -250,46 +256,30 @@ int CDlgGrep::DoModal( HINSTANCE hInstance, HWND hwndParent, const WCHAR* pszCur // 2013.05.21 コンストラクタからDoModalに移動 // m_strText は呼び出し元で設定済み + LoadDefaultsFromShareData( pszCurrentFilePath ); + + return (int)CDialog::DoModal( hInstance, hwndParent, IDD_GREP, (LPARAM)nullptr ); +} + +/*! + 検索ファイル/フォルダー/除外パターン/カレントファイルパスの既定値を履歴から補完する + (CDlgGrep::DoModal / CDlgGrepReplace::DoModal 共通。2-E で重複解消) +*/ +void CDlgGrep::LoadDefaultsFromShareData( const WCHAR* pszCurrentFilePath ) +{ if( m_szFile[0] == L'\0' && m_pShareData->m_sSearchKeywords.m_aGrepFiles.size() ){ - wcscpy( m_szFile, m_pShareData->m_sSearchKeywords.m_aGrepFiles[0] ); /* 検索ファイル */ + wcscpy_s( m_szFile, std::size(m_szFile), m_pShareData->m_sSearchKeywords.m_aGrepFiles[0] ); /* 検索ファイル */ } if( m_szFolder[0] == L'\0' && m_pShareData->m_sSearchKeywords.m_aGrepFolders.size() ){ - wcscpy( m_szFolder, m_pShareData->m_sSearchKeywords.m_aGrepFolders[0] ); /* 検索フォルダー */ + wcscpy_s( m_szFolder, std::size(m_szFolder), m_pShareData->m_sSearchKeywords.m_aGrepFolders[0] ); /* 検索フォルダー */ } - - /* 除外ファイル */ - if (m_szExcludeFile[0] == L'\0') { - if (m_pShareData->m_sSearchKeywords.m_aExcludeFiles.size()) { - wcscpy(m_szExcludeFile, m_pShareData->m_sSearchKeywords.m_aExcludeFiles[0]); - } - else { - /* ユーザーの利便性向上のために除外ファイルに対して初期値を設定する */ - wcscpy(m_szExcludeFile, DEFAULT_EXCLUDE_FILE_PATTERN); /* 除外ファイル */ - /* 履歴に残して後で選択できるようにする */ - m_pShareData->m_sSearchKeywords.m_aExcludeFiles.push_back(DEFAULT_EXCLUDE_FILE_PATTERN); - } - } - - /* 除外フォルダー */ - if (m_szExcludeFolder[0] == L'\0') { - if (m_pShareData->m_sSearchKeywords.m_aExcludeFolders.size()) { - wcscpy(m_szExcludeFolder, m_pShareData->m_sSearchKeywords.m_aExcludeFolders[0]); - } - else { - /* ユーザーの利便性向上のために除外フォルダーに対して初期値を設定する */ - wcscpy(m_szExcludeFolder, DEFAULT_EXCLUDE_FOLDER_PATTERN); /* 除外フォルダー */ - - /* 履歴に残して後で選択できるようにする */ - m_pShareData->m_sSearchKeywords.m_aExcludeFolders.push_back(DEFAULT_EXCLUDE_FOLDER_PATTERN); - } - } + /* 除外ファイル / 除外フォルダー */ + DetermineDefaultExcludePatterns(); if( pszCurrentFilePath ){ // 2010.01.10 ryoji - wcscpy(m_szCurrentFilePath, pszCurrentFilePath); + wcscpy_s(m_szCurrentFilePath, std::size(m_szCurrentFilePath), pszCurrentFilePath); } - - return (int)CDialog::DoModal( hInstance, hwndParent, IDD_GREP, (LPARAM)nullptr ); } BOOL CDlgGrep::OnInitDialog( HWND hwndDlg, WPARAM wParam, LPARAM lParam ) @@ -354,8 +344,8 @@ BOOL CDlgGrep::OnInitDialog( HWND hwndDlg, WPARAM wParam, LPARAM lParam ) // フォント設定 2012/11/27 Uchi const int nItemIds[] = { IDC_COMBO_TEXT, IDC_COMBO_FILE, IDC_COMBO_FOLDER, IDC_COMBO_EXCLUDE_FILE, IDC_COMBO_EXCLUDE_FOLDER }; - m_cFontDeleters.resize( int(std::size(nItemIds)) ); - for( size_t i2 = 0; i2 < int(std::size(nItemIds)); ++i2 ){ + m_cFontDeleters.resize( std::size(nItemIds) ); + for( size_t i2 = 0; i2 < std::size(nItemIds); ++i2 ){ HWND hwndItem = GetItemHwnd( nItemIds[i2] ); HFONT hFontOld = (HFONT)::SendMessageAny( hwndItem, WM_GETFONT, 0, 0 ); HFONT hFont = SetMainFont( hwndItem ); @@ -454,29 +444,12 @@ BOOL CDlgGrep::OnBnClicked( int wID ) CGrepAgent::CreateFolders( szFolder, vPaths ); if( 0 < vPaths.size() ){ // 最後のパスが操作対象 - wcsncpy( szFolder, vPaths.rbegin()->c_str(), nMaxPath ); - szFolder[nMaxPath-1] = L'\0'; + wcsncpy_s( szFolder, nMaxPath, vPaths.rbegin()->c_str(), _TRUNCATE ); if( DirectoryUp( szFolder ) ){ *(vPaths.rbegin()) = szFolder; - szFolder[0] = L'\0'; - for( int i = 0 ; i < (int)vPaths.size(); i++ ){ - WCHAR szFolderItem[nMaxPath]; - wcsncpy( szFolderItem, vPaths[i].c_str(), nMaxPath ); - szFolderItem[nMaxPath-1] = L'\0'; - if( wcschr( szFolderItem, L';' ) ){ - szFolderItem[0] = L'"'; - wcsncpy( szFolderItem + 1, vPaths[i].c_str(), nMaxPath - 1 ); - szFolderItem[nMaxPath-1] = L'\0'; - wcscat( szFolderItem, L"\"" ); - szFolderItem[nMaxPath-1] = L'\0'; - } - if( i ){ - wcscat( szFolder, L";" ); - szFolder[nMaxPath-1] = L'\0'; - } - wcscat_s( szFolder, nMaxPath, szFolderItem ); - } - ::SetWindowText( hwnd, szFolder ); + // ';' 区切り連結と引用は共通ヘルパに集約(2-E) + const std::wstring strJoined = FormatPathList( vPaths ); + ::SetWindowText( hwnd, strJoined.c_str() ); } } } @@ -485,6 +458,19 @@ BOOL CDlgGrep::OnBnClicked( int wID ) // case IDC_CHK_LOHICASE: /* 英大文字と英小文字を区別する */ // MYTRACE( L"IDC_CHK_LOHICASE\n" ); // return TRUE; + case IDC_CHK_EXCLUDE_FILE_REGEXP: + { + bool bRegexp = 0 != ::IsDlgButtonChecked( GetHwnd(), IDC_CHK_EXCLUDE_FILE_REGEXP ); + WCHAR szCur[MAX_GREP_PATH] = { 0 }; + ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_EXCLUDE_FILE, szCur, std::size(szCur) ); + const WCHAR* pszOther = bRegexp ? DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD : DEFAULT_EXCLUDE_FILE_PATTERN_REGEX; + const WCHAR* pszThis = bRegexp ? DEFAULT_EXCLUDE_FILE_PATTERN_REGEX : DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD; + if( szCur[0] == L'\0' || wcscmp( szCur, pszOther ) == 0 ){ + ApiWrap::DlgItem_SetText( GetHwnd(), IDC_COMBO_EXCLUDE_FILE, pszThis ); + } + } + return TRUE; + case IDC_CHK_REGULAREXP: /* 正規表現 */ // MYTRACE( L"IDC_CHK_REGULAREXP ::IsDlgButtonChecked( GetHwnd(), IDC_CHK_REGULAREXP ) = %d\n", ::IsDlgButtonChecked( GetHwnd(), IDC_CHK_REGULAREXP ) ); if( ::IsDlgButtonChecked( GetHwnd(), IDC_CHK_REGULAREXP ) ){ @@ -608,6 +594,7 @@ void CDlgGrep::SetData( void ) /* 除外ファイル */ ApiWrap::DlgItem_SetText( GetHwnd(), IDC_COMBO_EXCLUDE_FILE, m_szExcludeFile); + ::CheckDlgButton( GetHwnd(), IDC_CHK_EXCLUDE_FILE_REGEXP, m_bExcludeFileRegularExp ); /* 除外フォルダー */ ApiWrap::DlgItem_SetText( GetHwnd(), IDC_COMBO_EXCLUDE_FOLDER, m_szExcludeFolder); @@ -739,8 +726,7 @@ void CDlgGrep::SetDataFromThisText( bool bChecked ) ApiWrap::DlgItem_SetText( GetHwnd(), IDC_COMBO_EXCLUDE_FOLDER, L"" ); bEnableControls = FALSE; }else{ - std::wstring strFile(m_szFile); - if (strFile.substr(0, 6) == L":HWND:") { + if (IsHwndFileToken(m_szFile)) { wcsncpy_s(m_szFile, std::size(m_szFile), L"*.*", _TRUNCATE); } ApiWrap::DlgItem_SetText(GetHwnd(), IDC_COMBO_FILE, m_szFile); @@ -761,11 +747,33 @@ void CDlgGrep::SetDataFromThisText( bool bChecked ) return; } -/*! ダイアログデータの取得 +/* ダイアログデータの取得 @retval TRUE 正常 @retval FALSE 入力エラー */ int CDlgGrep::GetData( void ) +{ + GetDataSimpleOptions(); + + const bool bFromThisText = m_bFromThisText != FALSE; + + if( !GetDataFilePattern( bFromThisText ) ){ + return FALSE; + } + if( !GetDataFolderPath( bFromThisText ) ){ + return FALSE; + } + if( !ValidateSearchText() ){ + return FALSE; + } + + CommitDataToShareData( bFromThisText ); + + return TRUE; +} + +// チェックボックス・ラジオ・コンボ等の単純フィールド取得(C-9) +void CDlgGrep::GetDataSimpleOptions( void ) { /* サブフォルダーからも検索する*/ m_bSubFolder = ::IsDlgButtonChecked( GetHwnd(), IDC_CHK_SUBFOLDER ); @@ -819,39 +827,25 @@ int CDlgGrep::GetData( void ) m_bGrepSeparateFolder = IsDlgButtonCheckedBool( GetHwnd(), IDC_CHECK_SEP_FOLDER ); /* 検索文字列 */ - m_bSetText = ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_TEXT, m_strText );; + m_bSetText = ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_TEXT, m_strText ); +} +// 検索ファイルパターンの取得・検証(C-9) +BOOL CDlgGrep::GetDataFilePattern( bool bFromThisText ) +{ /* 検索ファイル */ ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_FILE, m_szFile, std::size(m_szFile) ); - bool bFromThisText = IsDlgButtonCheckedBool(GetHwnd(), IDC_CHK_FROMTHISTEXT); if( bFromThisText ){ - WCHAR szHwnd[_MAX_PATH]; -#ifdef _WIN64 - auto_sprintf(szHwnd, L":HWND:%016I64x", ::GetParent(GetHwnd())); -#else - auto_sprintf(szHwnd, L":HWND:%08x", ::GetParent(GetHwnd())); -#endif - m_szFile = szHwnd; + m_szFile.assign(BuildHwndFileToken(::GetParent(GetHwnd()))); }else{ - std::wstring strFile(m_szFile); - if (strFile.substr(0, 6) == L":HWND:") { + if (IsHwndFileToken(m_szFile)) { ErrorMessage(GetHwnd(), LS(STR_DLGGREP_THISDOC_ERROR)); return FALSE; } } - /* 検索フォルダー */ - ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_FOLDER, m_szFolder, std::size(m_szFolder) ); /* 除外ファイル */ ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_EXCLUDE_FILE, m_szExcludeFile, std::size(m_szExcludeFile)); - /* 除外フォルダー */ - ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_EXCLUDE_FOLDER, m_szExcludeFolder, std::size(m_szExcludeFolder)); - - m_pShareData->m_Common.m_sSearch.m_nGrepCharSet = m_nGrepCharSet; // 文字コード自動判別 - m_pShareData->m_Common.m_sSearch.m_nGrepOutputLineType = m_nGrepOutputLineType; // 行を出力/該当部分/否マッチ行 を出力 - m_pShareData->m_Common.m_sSearch.m_nGrepOutputStyle = m_nGrepOutputStyle; // Grep: 出力形式 - m_pShareData->m_Common.m_sSearch.m_bGrepOutputFileOnly = m_bGrepOutputFileOnly; - m_pShareData->m_Common.m_sSearch.m_bGrepOutputBaseFolder = m_bGrepOutputBaseFolder; - m_pShareData->m_Common.m_sSearch.m_bGrepSeparateFolder = m_bGrepSeparateFolder; + m_bExcludeFileRegularExp = ::IsDlgButtonChecked( GetHwnd(), IDC_CHK_EXCLUDE_FILE_REGEXP ); if( m_szFile[0] != '\0' ) { CGrepEnumKeys enumKeys; @@ -869,8 +863,19 @@ int CDlgGrep::GetData( void ) // Jun. 16, 2003 Moca // 検索パターンが指定されていない場合のメッセージ表示をやめ、 // 「*.*」が指定されたものと見なす. - wcscpy( m_szFile, L"*.*" ); + wcscpy_s( m_szFile, std::size(m_szFile), L"*.*" ); } + return TRUE; +} + +// 検索フォルダーの取得・複数パス解決・検証(C-9) +BOOL CDlgGrep::GetDataFolderPath( bool bFromThisText ) +{ + /* 検索フォルダー */ + ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_FOLDER, m_szFolder, std::size(m_szFolder) ); + /* 除外フォルダー */ + ApiWrap::DlgItem_GetText( GetHwnd(), IDC_COMBO_EXCLUDE_FOLDER, m_szExcludeFolder, std::size(m_szExcludeFolder)); + if( m_szFolder[0] == L'\0' ){ WarningMessage( GetHwnd(), LS(STR_DLGGREP4) ); return FALSE; @@ -885,10 +890,8 @@ int CDlgGrep::GetData( void ) // 2011.11.24 Moca 複数フォルダー指定 std::vector vPaths; CGrepAgent::CreateFolders( m_szFolder, vPaths ); - int nFolderLen = 0; const int nMaxPath = MAX_GREP_PATH; - WCHAR szFolder[nMaxPath]; - szFolder[0] = L'\0'; + std::wstring strFolder; for( int i = 0 ; i < (int)vPaths.size(); i ++ ){ // 相対パス→絶対パス if( !::SetCurrentDirectory( vPaths[i].c_str() ) ){ @@ -897,26 +900,25 @@ int CDlgGrep::GetData( void ) } WCHAR szFolderItem[nMaxPath]; ::GetCurrentDirectory( nMaxPath, szFolderItem ); - // ;がフォルダー名に含まれていたら""で囲う - if( wcschr( szFolderItem, L';' ) ){ - szFolderItem[0] = L'"'; - ::GetCurrentDirectory( nMaxPath, szFolderItem + 1 ); - wcscat(szFolderItem, L"\""); - } - auto nFolderItemLen = int(wcslen(szFolderItem)); - if( nMaxPath < nFolderLen + nFolderItemLen + 1 ){ + // ;がフォルダー名に含まれていたら""で囲う(引用は共通ヘルパに集約(2-E)) + const std::wstring strFolderItem = QuotePathIfNeeded( szFolderItem ); + if( nMaxPath < int(strFolder.length() + strFolderItem.length() + 1) ){ WarningMessage( GetHwnd(), LS(STR_DLGGREP6) ); return FALSE; } if( i ){ - wcscat( szFolder, L";" ); + strFolder += L';'; } - wcscat( szFolder, szFolderItem ); - nFolderLen = (int)wcslen( szFolder ); + strFolder += strFolderItem; } - wcscpy( m_szFolder, szFolder ); + wcscpy_s( m_szFolder, std::size(m_szFolder), strFolder.c_str() ); } + return TRUE; +} +// 検索文字列の正規表現構文検証(C-9) +BOOL CDlgGrep::ValidateSearchText( void ) +{ //@@@ 2002.2.2 YAZAKI CShareData.AddToSearchKeyArr()追加に伴う変更 /* 検索文字列 */ if( 0 < m_strText.size() ){ @@ -928,18 +930,39 @@ int CDlgGrep::GetData( void ) return FALSE; } // To Here Jun. 26, 2001 genta 正規表現ライブラリ差し替え + } + return TRUE; +} + +// 検証通過後の共有設定・履歴への書き込み(C-9) +void CDlgGrep::CommitDataToShareData( bool bFromThisText ) +{ + // ==== ここから下では入力検証エラーで戻らない(共有設定への書き込みは全検証通過後に集約する) ==== + m_pShareData->m_Common.m_sSearch.m_nGrepCharSet = m_nGrepCharSet; // 文字コード自動判別 + m_pShareData->m_Common.m_sSearch.m_nGrepOutputLineType = m_nGrepOutputLineType; // 行を出力/該当部分/否マッチ行 を出力 + m_pShareData->m_Common.m_sSearch.m_nGrepOutputStyle = m_nGrepOutputStyle; // Grep: 出力形式 + m_pShareData->m_Common.m_sSearch.m_bGrepOutputFileOnly = m_bGrepOutputFileOnly; + m_pShareData->m_Common.m_sSearch.m_bGrepOutputBaseFolder = m_bGrepOutputBaseFolder; + m_pShareData->m_Common.m_sSearch.m_bGrepSeparateFolder = m_bGrepSeparateFolder; + m_pShareData->m_Common.m_sSearch.m_bGrepExcludeFileRegexp = m_bExcludeFileRegularExp != FALSE; + + if( m_strText.size() > 0 ){ if( m_strText.size() < _MAX_PATH ){ - CSearchKeywordManager().AddToSearchKeyArr( m_strText.c_str() ); + if( !bFromThisText ){ + CSearchKeywordManager().AddToSearchKeyArr( m_strText.c_str() ); + } m_pShareData->m_Common.m_sSearch.m_sSearchOption = m_sSearchOption; // 検索オプション } }else{ // 2014.07.01 空キーも登録する - CSearchKeywordManager().AddToSearchKeyArr( L"" ); + if( !bFromThisText ){ + CSearchKeywordManager().AddToSearchKeyArr( L"" ); + } } // この編集中のテキストから検索する場合、履歴に残さない Uchi 2008/5/23 // 2016.03.08 Moca 「このファイルから検索」の場合はサブフォルダー共通設定を更新しない - if (!m_bFromThisText) { + if (!bFromThisText) { /* 検索ファイル */ CSearchKeywordManager().AddToGrepFileArr( m_szFile ); @@ -955,8 +978,6 @@ int CDlgGrep::GetData( void ) // Grep:サブフォルダーも検索 m_pShareData->m_Common.m_sSearch.m_bGrepSubFolder = m_bSubFolder; } - - return TRUE; } //@@@ 2002.01.18 add start @@ -968,11 +989,58 @@ LPVOID CDlgGrep::GetHelpIdTable(void) static void SetGrepFolder( HWND hwndCtrl, LPCWSTR folder ) { - if( wcschr( folder, L';') ){ - std::wstring strQuoteFolder; - strQuoteFolder = std::wstring(L"\"") + folder + std::wstring(L"\""); - ::SetWindowText( hwndCtrl, strQuoteFolder.c_str() ); - }else{ - ::SetWindowText( hwndCtrl, folder ); + // ';' を含むパスの引用は共通ヘルパに集約(2-E) + const std::wstring strFolder = QuotePathIfNeeded( folder ); + ::SetWindowText( hwndCtrl, strFolder.c_str() ); +} + +void CDlgGrep::DetermineDefaultExcludePatterns() +{ + // 初期表示とテストで共通に使う、除外パターンの既定値補完ロジック。 + /* 除外ファイル */ + if (m_szExcludeFile[0] == L'\0') { + if (m_pShareData->m_sSearchKeywords.m_aExcludeFiles.size()) { + wcscpy_s(m_szExcludeFile, std::size(m_szExcludeFile), m_pShareData->m_sSearchKeywords.m_aExcludeFiles[0]); + } + else { + const WCHAR* pszDefault = m_bExcludeFileRegularExp + ? DEFAULT_EXCLUDE_FILE_PATTERN_REGEX + : DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD; + wcscpy_s(m_szExcludeFile, std::size(m_szExcludeFile), pszDefault); + m_pShareData->m_sSearchKeywords.m_aExcludeFiles.push_back(pszDefault); + } } + + /* 除外フォルダー */ + if (m_szExcludeFolder[0] == L'\0') { + if (m_pShareData->m_sSearchKeywords.m_aExcludeFolders.size()) { + wcscpy_s(m_szExcludeFolder, std::size(m_szExcludeFolder), m_pShareData->m_sSearchKeywords.m_aExcludeFolders[0]); + } + else { + /* ユーザーの利便性向上のために除外フォルダーに対して初期値を設定する */ + wcscpy_s(m_szExcludeFolder, std::size(m_szExcludeFolder), DEFAULT_EXCLUDE_FOLDER_PATTERN); /* 除外フォルダー */ + + /* 履歴に残して後で選択できるようにする */ + m_pShareData->m_sSearchKeywords.m_aExcludeFolders.push_back(DEFAULT_EXCLUDE_FOLDER_PATTERN); + } + } +} + +std::wstring CDlgGrep::BuildHwndFileToken(HWND hwnd) +{ + // HWND を 16 進の文字列にして、コンボボックス内の擬似ファイル名として保持する。 + WCHAR buf[_MAX_PATH]; +#ifdef _WIN64 + auto_sprintf(buf, L"%s%016I64x", HWND_FILE_TOKEN_PREFIX, reinterpret_cast(hwnd)); +#else + auto_sprintf(buf, L"%s%08x", HWND_FILE_TOKEN_PREFIX, reinterpret_cast(hwnd)); +#endif + return std::wstring(buf); +} + +bool CDlgGrep::IsHwndFileToken(const wchar_t* s) +{ + // 実際の値比較ではなく、特別な擬似ファイル名であるかどうかだけを判定する。 + constexpr auto cchPrefix = std::size(HWND_FILE_TOKEN_PREFIX) - 1; + return s != nullptr && wcsncmp(s, HWND_FILE_TOKEN_PREFIX, cchPrefix) == 0; } diff --git a/sakura_core/dlg/CDlgGrep.h b/sakura_core/dlg/CDlgGrep.h index b51892b8b4..7a9a81f26b 100644 --- a/sakura_core/dlg/CDlgGrep.h +++ b/sakura_core/dlg/CDlgGrep.h @@ -8,7 +8,7 @@ /* Copyright (C) 1998-2001, Norio Nakatani Copyright (C) 2002, Moca - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holder to use this code for other purpose. @@ -25,8 +25,11 @@ #include "recent/CRecentExcludeFile.h" #include "recent/CRecentExcludeFolder.h" -#define DEFAULT_EXCLUDE_FILE_PATTERN L"*.msi;*.exe;*.obj;*.pdb;*.ilk;*.res;*.pch;*.iobj;*.ipdb" -#define DEFAULT_EXCLUDE_FOLDER_PATTERN L".git;.svn;.vs" +// 正規表現ONのときの既定(フルパスに対する正規表現) +inline constexpr const wchar_t* DEFAULT_EXCLUDE_FILE_PATTERN_REGEX = L".*\\.msi$;.*\\.exe$;.*\\.obj$;.*\\.pdb$;.*\\.ilk$;.*\\.res$;.*\\.pch$;.*\\.iobj$;.*\\.ipdb$"; +// 正規表現OFFのときの既定(ワイルドカード) +inline constexpr const wchar_t* DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD = L"*.msi;*.exe;*.obj;*.pdb;*.ilk;*.res;*.pch;*.iobj;*.ipdb"; +inline constexpr const wchar_t* DEFAULT_EXCLUDE_FOLDER_PATTERN = L".git;.svn;.vs"; //! GREPダイアログボックス class CDlgGrep : public CDialog @@ -44,10 +47,28 @@ class CDlgGrep : public CDialog int DoModal( HINSTANCE, HWND, const WCHAR* ); /* モーダルダイアログの表示 */ // HWND DoModeless( HINSTANCE, HWND, const char* ); /* モードレスダイアログの表示 */ + //! 除外パターンの初期値を決定する + // 履歴が空なら既定値を補い、次回以降に選べるよう履歴にも積む。 + void DetermineDefaultExcludePatterns(); + + //! 検索ファイル/フォルダー/除外パターン/カレントファイルパスの既定値を履歴から補完する + // CDlgGrep::DoModal / CDlgGrepReplace::DoModal 共通の初期化(2-E で重複解消)。 + void LoadDefaultsFromShareData(const WCHAR* pszCurrentFilePath); + + //! HWND ファイルトークンの接頭辞(":HWND:..." 形式の共通定義) + static constexpr const wchar_t HWND_FILE_TOKEN_PREFIX[] = L":HWND:"; + + //! HWND ファイルトークンの生成と判定(":HWND:..." 形式) + // 「編集中のテキストから検索」時の擬似ファイル名として使う。 + static std::wstring BuildHwndFileToken(HWND hwnd); + // 実ファイルパスと区別するため、接頭辞だけを見て判定する。 + static bool IsHwndFileToken(const wchar_t* s); + bool m_bEnableThisText; bool m_bSelectOnceThisText; BOOL m_bSubFolder;/*!< サブフォルダーからも検索する */ BOOL m_bFromThisText;/*!< この編集中のテキストから検索する */ + BOOL m_bExcludeFileRegularExp = FALSE; //!< 除外ファイルを正規表現として扱う SSearchOption m_sSearchOption; //!< 検索オプション @@ -85,6 +106,12 @@ class CDlgGrep : public CDialog void SetData( void ) override; /* ダイアログデータの設定 */ int GetData( void ) override; /* ダイアログデータの取得 */ void SetDataFromThisText(bool bChecked); /* 現在編集中ファイルから検索チェックでの設定 */ + // C-9: GetData 分解(Phase 3) + void GetDataSimpleOptions( void ); //!< チェックボックス・ラジオ・コンボ等の単純フィールド取得 + BOOL GetDataFilePattern( bool bFromThisText ); //!< 検索ファイルパターンの取得・検証 + BOOL GetDataFolderPath( bool bFromThisText ); //!< 検索フォルダーの取得・複数パス解決・検証 + BOOL ValidateSearchText( void ); //!< 検索文字列の正規表現構文検証 + void CommitDataToShareData( bool bFromThisText ); //!< 検証通過後の共有設定・履歴への書き込み static LRESULT CALLBACK OnFolderProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); }; #endif /* SAKURA_CDLGGREP_01A0D5CB_326B_4C56_A527_C811F84FD8D8_H_ */ diff --git a/sakura_core/dlg/CDlgGrepReplace.cpp b/sakura_core/dlg/CDlgGrepReplace.cpp index c014b590fa..a95496088c 100644 --- a/sakura_core/dlg/CDlgGrepReplace.cpp +++ b/sakura_core/dlg/CDlgGrepReplace.cpp @@ -11,7 +11,7 @@ Copyright (C) 2006, ryoji Copyright (C) 2010, ryoji Copyright (C) 2014, Moca - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holder to use this code for other purpose. @@ -84,44 +84,8 @@ int CDlgGrepReplace::DoModal( HINSTANCE hInstance, HWND hwndParent, const WCHAR* m_bPaste = false; m_bBackup = m_pShareData->m_Common.m_sSearch.m_bGrepBackup; - if( m_szFile[0] == L'\0' && m_pShareData->m_sSearchKeywords.m_aGrepFiles.size() ){ - wcscpy( m_szFile, m_pShareData->m_sSearchKeywords.m_aGrepFiles[0] ); /* 検索ファイル */ - } - if( m_szFolder[0] == L'\0' && m_pShareData->m_sSearchKeywords.m_aGrepFolders.size() ){ - wcscpy( m_szFolder, m_pShareData->m_sSearchKeywords.m_aGrepFolders[0] ); /* 検索フォルダー */ - } - - /* 除外ファイル */ - if (m_szExcludeFile[0] == L'\0') { - if (m_pShareData->m_sSearchKeywords.m_aExcludeFiles.size()) { - wcscpy(m_szExcludeFile, m_pShareData->m_sSearchKeywords.m_aExcludeFiles[0]); - } - else { - /* ユーザーの利便性向上のために除外ファイルに対して初期値を設定する */ - wcscpy(m_szExcludeFile, DEFAULT_EXCLUDE_FILE_PATTERN); /* 除外ファイル */ - - /* 履歴に残して後で選択できるようにする */ - m_pShareData->m_sSearchKeywords.m_aExcludeFiles.push_back(DEFAULT_EXCLUDE_FILE_PATTERN); - } - } - - /* 除外フォルダー */ - if (m_szExcludeFolder[0] == L'\0') { - if (m_pShareData->m_sSearchKeywords.m_aExcludeFolders.size()) { - wcscpy(m_szExcludeFolder, m_pShareData->m_sSearchKeywords.m_aExcludeFolders[0]); - } - else { - /* ユーザーの利便性向上のために除外フォルダーに対して初期値を設定する */ - wcscpy(m_szExcludeFolder, DEFAULT_EXCLUDE_FOLDER_PATTERN); /* 除外フォルダー */ - - /* 履歴に残して後で選択できるようにする */ - m_pShareData->m_sSearchKeywords.m_aExcludeFolders.push_back(DEFAULT_EXCLUDE_FOLDER_PATTERN); - } - } - - if( pszCurrentFilePath ){ // 2010.01.10 ryoji - wcscpy(m_szCurrentFilePath, pszCurrentFilePath); - } + // 既定値の補完は基底クラスと共通化(2-E: コピペ削除。Phase 1 の wcscpy_s 応急コードもここで撤去) + LoadDefaultsFromShareData( pszCurrentFilePath ); return (int)CDialog::DoModal( hInstance, hwndParent, IDD_GREP_REPLACE, lParam ); } @@ -227,12 +191,14 @@ int CDlgGrepReplace::GetData( void ) } m_bBackup = IsDlgButtonCheckedBool( GetHwnd(), IDC_CHK_BACKUP ); - m_pShareData->m_Common.m_sSearch.m_bGrepBackup = m_bBackup; if( !CDlgGrep::GetData() ){ return FALSE; } + // 全検証通過後に共有設定へ書き込む(入力エラー時に共有設定を汚さない) + m_pShareData->m_Common.m_sSearch.m_bGrepBackup = m_bBackup; + if( m_strText2.size() < _MAX_PATH ){ CSearchKeywordManager().AddToReplaceKeyArr( m_strText2.c_str() ); } diff --git a/sakura_core/env/CShareData.cpp b/sakura_core/env/CShareData.cpp index ea55f86a75..ce8193aa2f 100644 --- a/sakura_core/env/CShareData.cpp +++ b/sakura_core/env/CShareData.cpp @@ -18,7 +18,7 @@ Copyright (C) 2009, nasukoji, ryoji Copyright (C) 2011, nasukoji Copyright (C) 2012, Moca, ryoji - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization This source code is designed for sakura editor. Please contact the copyright holder to use this code for other purpose. @@ -450,6 +450,7 @@ bool CShareData::InitShareData() sSearch.m_bGrepOutputBaseFolder = false; sSearch.m_bGrepSeparateFolder = false; sSearch.m_bGrepBackup = true; + sSearch.m_bGrepExcludeFileRegexp = false; // 既定はワイルドカード sSearch.m_bGrepDefaultFolder=FALSE; /* Grep: フォルダーの初期値をカレントフォルダーにする */ sSearch.m_nGrepCharSet = CODE_AUTODETECT; /* Grep: 文字コードセット */ @@ -461,6 +462,7 @@ bool CShareData::InitShareData() sSearch.m_bGTJW_LDBLCLK = TRUE; /* ダブルクリックでタグジャンプ */ sSearch.m_bGrepExitConfirm = FALSE; /* Grepモードで保存確認するか */ + sSearch.m_nGrepThreadCount = 2; /* Grep並列スレッド数(最低値・デフォルト2) */ sSearch.m_bAutoCloseDlgFind = TRUE; /* 検索ダイアログを自動的に閉じる */ sSearch.m_bSearchAll = FALSE; /* 検索/置換/ブックマーク 先頭(末尾)から再検索 2002.01.26 hor */ diff --git a/sakura_core/env/CShareData_IO.cpp b/sakura_core/env/CShareData_IO.cpp index acd17ed072..5d9539e3fe 100644 --- a/sakura_core/env/CShareData_IO.cpp +++ b/sakura_core/env/CShareData_IO.cpp @@ -2,7 +2,7 @@ //2008.XX.XX kobake CShareDataから分離 /* Copyright (C) 2008, kobake - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -518,10 +518,12 @@ void CShareData_IO::ShareData_IO_Common( CDataProfile& cProfile ) cProfile.IOProfileData( pszSecName, L"bGrepSeparateFolder" , common.m_sSearch.m_bGrepSeparateFolder ); cProfile.IOProfileData( pszSecName, L"bGrepDefaultFolder" , common.m_sSearch.m_bGrepDefaultFolder ); cProfile.IOProfileData( pszSecName, L"bGrepBackup" , common.m_sSearch.m_bGrepBackup ); + cProfile.IOProfileData( pszSecName, L"bGrepExcludeFileRegexp", common.m_sSearch.m_bGrepExcludeFileRegexp ); // 2002/09/21 Moca 追加 cProfile.IOProfileData(pszSecName, L"nGrepCharSet", common.m_sSearch.m_nGrepCharSet ); cProfile.IOProfileData( pszSecName, L"bGrepRealTime" , common.m_sSearch.m_bGrepRealTimeView ); // 2003.06.16 Moca + cProfile.IOProfileData( pszSecName, L"nGrepThreadCount" , common.m_sSearch.m_nGrepThreadCount ); cProfile.IOProfileData( pszSecName, L"bCaretTextForSearch" , common.m_sSearch.m_bCaretTextForSearch ); // 2006.08.23 ryoji カーソル位置の文字列をデフォルトの検索文字列にする cProfile.IOProfileData( pszSecName, L"m_bInheritKeyOtherView" , common.m_sSearch.m_bInheritKeyOtherView ); cProfile.IOProfileData( pszSecName, L"nTagJumpMode" , common.m_sSearch.m_nTagJumpMode ); @@ -1686,7 +1688,7 @@ void CShareData_IO::ShareData_IO_Type_One( CDataProfile& cProfile, STypeConfig& cProfile.IOProfileData( pszSecName, L"bUseRegexKeyword", types.m_bUseRegexKeyword );/* 正規表現キーワード使用するか? */ wchar_t* pKeyword = types.m_RegexKeywordList; int nPos = 0; - constexpr auto nKeywordSize = int(std::size(types.m_RegexKeywordList)); + constexpr auto nKeywordSize = int(std::extent_v); for(j = 0; j < int(std::size(types.m_RegexKeywordArr)); j++) { auto_sprintf( szKeyName, L"RxKey[%03d]", j ); diff --git a/sakura_core/env/CommonSetting.h b/sakura_core/env/CommonSetting.h index 4ad22df39a..8557ce4aee 100644 --- a/sakura_core/env/CommonSetting.h +++ b/sakura_core/env/CommonSetting.h @@ -2,7 +2,7 @@ //2007.09.28 kobake Common整理 /* Copyright (C) 2008, kobake - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -388,6 +388,7 @@ struct CommonSetting_Search bool m_bGrepOutputBaseFolder; //!< Grep: ベースフォルダー表示 bool m_bGrepSeparateFolder; //!< Grep: フォルダー毎に表示 bool m_bGrepBackup; //!< Grep: バックアップ作成 + bool m_bGrepExcludeFileRegexp; //!< Grep: 除外ファイルを正規表現として扱う BOOL m_bCaretTextForSearch; //!< カーソル位置の文字列をデフォルトの検索文字列にする 2006.08.23 ryoji bool m_bInheritKeyOtherView; //!< 次・前検索で他のビューの検索条件を引き継ぐ @@ -396,6 +397,7 @@ struct CommonSetting_Search //Grep BOOL m_bGrepExitConfirm; //!< Grepモードで保存確認するか BOOL m_bGrepRealTimeView; //!< Grep結果のリアルタイム表示 2003.06.16 Moca + int m_nGrepThreadCount; //!< Grep並列スレッド数(最低値・デフォルト2) BOOL m_bGTJW_RETURN; //!< エンターキーでタグジャンプ BOOL m_bGTJW_LDBLCLK; //!< ダブルクリックでタグジャンプ diff --git a/sakura_core/grep/CGrepEnumFileBase.h b/sakura_core/grep/CGrepEnumFileBase.h index 7ac8e7f494..e37a106fad 100644 --- a/sakura_core/grep/CGrepEnumFileBase.h +++ b/sakura_core/grep/CGrepEnumFileBase.h @@ -15,6 +15,10 @@ #define SAKURA_CGREPENUMFILEBASE_6B85547E_13E4_4183_AE06_B4D6395ABC88_H_ #pragma once +#include +#include +#include +#include #include #include #include @@ -23,7 +27,7 @@ #include "grep/CGrepEnumKeys.h" #include "util/string_ex.h" -typedef std::pair< LPWSTR, DWORD > PairGrepEnumItem; +typedef std::pair< std::wstring, DWORD > PairGrepEnumItem; typedef std::vector< PairGrepEnumItem > VPGrepEnumItem; class CGrepEnumOptions { @@ -41,9 +45,17 @@ class CGrepEnumOptions { class CGrepEnumFileBase { private: VPGrepEnumItem m_vpItems; + std::unordered_set< std::wstring > m_setItemNames; //!< IsExist 用の名前索引(m_vpItems と同期して維持する) using Me = CGrepEnumFileBase; + //! FindFirstFile ハンドルの RAII クローザ + struct FindHandleCloser { + void operator()( HANDLE h ) const { + if( h && h != INVALID_HANDLE_VALUE ) ::FindClose( h ); + } + }; + public: CGrepEnumFileBase() noexcept = default; CGrepEnumFileBase(const Me&) = delete; @@ -55,22 +67,13 @@ class CGrepEnumFileBase { } void ClearItems( void ){ - for( int i = 0; i < GetCount(); i++ ){ - LPWSTR lp = m_vpItems[ i ].first; - m_vpItems[ i ].first = nullptr; - delete [] lp; - } m_vpItems.clear(); + m_setItemNames.clear(); return; } BOOL IsExist( LPCWSTR lpFileName ){ - for( int i = 0; i < GetCount(); i++ ){ - if( wcscmp( m_vpItems[ i ].first, lpFileName ) == 0 ){ - return TRUE; - } - } - return FALSE; + return m_setItemNames.count( lpFileName ) != 0 ? TRUE : FALSE; } virtual BOOL IsValid( WIN32_FIND_DATA& w32fd, LPCWSTR pFile = nullptr ){ @@ -86,7 +89,7 @@ class CGrepEnumFileBase { LPCWSTR GetFileName( int i ){ if( i < 0 || i >= GetCount() ) return nullptr; - return m_vpItems[ i ].first; + return m_vpItems[ i ].first.c_str(); } DWORD GetFileSizeLow( int i ){ @@ -94,37 +97,22 @@ class CGrepEnumFileBase { return m_vpItems[ i ].second; } - int Enumerates( LPCWSTR lpBaseFolder, VGrepEnumKeys& vecKeys, CGrepEnumOptions& option, CGrepEnumFileBase* pExceptItems = nullptr ){ + int Enumerates( LPCWSTR lpBaseFolder, const VGrepEnumKeys& vecKeys, CGrepEnumOptions& option, CGrepEnumFileBase* pExceptItems = nullptr ){ int found = 0; - const auto cchBaseFolder = lpBaseFolder ? wcsnlen_s(lpBaseFolder, 4096 - 1) : 0; // FIXME: パス長の上限は暫定値。 - for( int i = 0; i < (int)vecKeys.size(); i++ ){ - const auto baseLen = cchBaseFolder; - LPWSTR lpPath = new WCHAR[ baseLen + wcslen( vecKeys[ i ] ) + 2 ]; - if( nullptr == lpPath ) break; - wcscpy( lpPath, lpBaseFolder ); - wcscpy( lpPath + baseLen, L"\\" ); - wcscpy( lpPath + baseLen + 1, vecKeys[ i ] ); - // vecKeys[ i ] ==> "subdir\*.h" 等の場合に後で(ファイル|フォルダー)名に "subdir\" を連結する - const WCHAR* keyDirYen = wcsrchr( vecKeys[ i ], L'\\' ); - const WCHAR* keyDirSlash = wcsrchr( vecKeys[ i ], L'/' ); - const WCHAR* keyDir; - if( keyDirYen == nullptr ){ - keyDir = keyDirSlash; - }else if( keyDirSlash == nullptr ){ - keyDir = keyDirYen; - }else if( keyDirYen < keyDirSlash ){ - keyDir = keyDirSlash; - }else{ - keyDir = keyDirYen; - } - const auto nKeyDirLen = keyDir ? keyDir - vecKeys[ i ] + 1 : 0; + const std::wstring baseFolder = lpBaseFolder ? lpBaseFolder : L""; + for( const auto& key : vecKeys ){ + const std::wstring path = baseFolder + L"\\" + key; + // key ==> "subdir\*.h" 等の場合に後で(ファイル|フォルダー)名に "subdir\" を連結する + const size_t keyDirPos = key.find_last_of( L"\\/" ); + const size_t nKeyDirLen = ( keyDirPos != std::wstring::npos ) ? keyDirPos + 1 : 0; WIN32_FIND_DATA w32fd; - HANDLE handle = ::FindFirstFile( lpPath, &w32fd ); - if( INVALID_HANDLE_VALUE != handle ){ + // ハンドルは RAII で管理し、例外発生時も FindClose を保証する + std::unique_ptr< void, FindHandleCloser > handle( ::FindFirstFile( path.c_str(), &w32fd ) ); + if( INVALID_HANDLE_VALUE != handle.get() ){ do{ - if( !::PathMatchSpec(w32fd.cFileName, vecKeys[ i ] + nKeyDirLen) ){ + if( !::PathMatchSpec(w32fd.cFileName, key.c_str() + nKeyDirLen) ){ continue; } if( option.m_bIgnoreHidden && (w32fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) ){ @@ -136,36 +124,31 @@ class CGrepEnumFileBase { if( option.m_bIgnoreSystem && (w32fd.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) ){ continue; } - LPWSTR lpName = new WCHAR[ nKeyDirLen + wcslen( w32fd.cFileName ) + 1 ]; - wcsncpy( lpName, vecKeys[ i ], nKeyDirLen ); - wcscpy( lpName + nKeyDirLen, w32fd.cFileName ); - LPWSTR lpFullPath = new WCHAR[ baseLen + wcslen(lpName) + 2 ]; - wcscpy( lpFullPath, lpBaseFolder ); - wcscpy( lpFullPath + baseLen, L"\\" ); - wcscpy( lpFullPath + baseLen + 1, lpName ); - if( IsValid( w32fd, lpName ) ){ - if( pExceptItems && pExceptItems->IsExist( lpFullPath ) ){ + std::wstring name = key.substr( 0, nKeyDirLen ) + w32fd.cFileName; + if( IsValid( w32fd, name.c_str() ) ){ + const std::wstring fullPath = baseFolder + L"\\" + name; + if( pExceptItems && pExceptItems->IsExist( fullPath.c_str() ) ){ }else{ - m_vpItems.emplace_back( lpName, w32fd.nFileSizeLow ); - found++; // 2011.11.19 if( pExceptItems && nKeyDirLen ){ // フォルダーを含んだパスなら検索済みとして除外指定に追加する - pExceptItems->m_vpItems.emplace_back( lpFullPath, w32fd.nFileSizeLow ); - }else{ - delete [] lpFullPath; + pExceptItems->AddItem( fullPath, w32fd.nFileSizeLow ); } - continue; + AddItem( std::move( name ), w32fd.nFileSizeLow ); + found++; // 2011.11.19 } } - delete [] lpName; - delete [] lpFullPath; - }while( ::FindNextFile( handle, &w32fd ) ); - ::FindClose( handle ); + }while( ::FindNextFile( handle.get(), &w32fd ) ); } - delete [] lpPath; } return found; } + +private: + //! 列挙結果へ 1 件追加する(IsExist 用の名前索引も同期して更新する) + void AddItem( std::wstring name, DWORD dwFileSizeLow ){ + m_setItemNames.insert( name ); + m_vpItems.emplace_back( std::move( name ), dwFileSizeLow ); + } }; #endif /* SAKURA_CGREPENUMFILEBASE_6B85547E_13E4_4183_AE06_B4D6395ABC88_H_ */ diff --git a/sakura_core/grep/CGrepEnumKeys.h b/sakura_core/grep/CGrepEnumKeys.h index 00e485f9ea..fe6c726fce 100644 --- a/sakura_core/grep/CGrepEnumKeys.h +++ b/sakura_core/grep/CGrepEnumKeys.h @@ -1,14 +1,13 @@ /*! @file - - @brief GREP support library - + @brief GREP support library - 検索/除外ファイルパターン管理 + @note !プレフィックス除外ファイル(正規表現/ワイルドカードは呼び出し側フラグで切替)・AddExceptFile/Folder 追加 @author wakura, Moca @date 2008/04/28 */ /* Copyright (C) 2008, wakura Copyright (C) 2011, Moca - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -16,7 +15,9 @@ #define SAKURA_CGREPENUMKEYS_FCE5732F_FA0C_4CB2_90D9_D1D440841D5C_H_ #pragma once +#include #include +#include #include #include #include @@ -24,7 +25,7 @@ #include "util/string_ex.h" #include "util/file.h" -typedef std::vector< LPCWSTR > VGrepEnumKeys; +typedef std::vector< std::wstring > VGrepEnumKeys; class CGrepEnumKeys { @@ -40,6 +41,9 @@ class CGrepEnumKeys { VGrepEnumKeys m_vecExceptAbsFileKeys; VGrepEnumKeys m_vecExceptAbsFolderKeys; + // 除外ファイルを正規表現モードのとき格納される除外パターン(SetFileKeys で格納、DoGrepTree 等でマッチング) + std::vector m_vecExceptFileRegexPatterns; + public: CGrepEnumKeys() noexcept = default; CGrepEnumKeys(const Me&) = delete; @@ -50,17 +54,16 @@ class CGrepEnumKeys { ClearItems(); } - // 除外ファイルの2つの解析済み配列から1つのリストを作る - auto GetExcludeFiles() const -> std::vector { - std::vector excludeFiles; - const auto& fileKeys = m_vecExceptFileKeys; - excludeFiles.insert( excludeFiles.cend(), fileKeys.cbegin(), fileKeys.cend() ); - const auto& absFileKeys = m_vecExceptAbsFileKeys; - excludeFiles.insert( excludeFiles.cend(), absFileKeys.cbegin(), absFileKeys.cend() ); + // 除外ファイルパターンの統合リストを返す(Grep ヘッダー表示用) + auto GetExcludeFiles() const -> std::vector { + std::vector excludeFiles; + for( const auto& k : m_vecExceptFileKeys ) excludeFiles.emplace_back( k ); + for( const auto& k : m_vecExceptAbsFileKeys ) excludeFiles.emplace_back( k ); + for( const auto& p : m_vecExceptFileRegexPatterns ) excludeFiles.emplace_back( p ); return excludeFiles; } - // 除外フォルダーの2つの解析済み配列から1つのリストを作る + // 除外フォルダーパターンの統合リストを返す(Grep ヘッダー表示用) auto GetExcludeFolders() const -> std::vector { std::vector excludeFolders; const auto& folderKeys = m_vecExceptFolderKeys; @@ -70,26 +73,43 @@ class CGrepEnumKeys { return excludeFolders; } - int SetFileKeys( LPCWSTR lpKeys ){ + int SetFileKeys( LPCWSTR lpKeys, bool bExcludeFileRegex = false ){ const WCHAR* WILDCARD_ANY = L"*.*"; //サブフォルダー探索用 ClearItems(); - + std::vector< std::wstring > patterns = SplitPattern(lpKeys); for (size_t i = 0; i < patterns.size(); i++) { const std::wstring& element = patterns[i]; const WCHAR* token = element.c_str(); + // !プレフィックス: 除外ファイル。正規表現/ワイルドカードの区別は + // 呼び出し側のフラグ(-GOPT=E / チェックボックス)で決める。 + if( token[0] == L'!' ){ + const WCHAR* p = token + 1; + if( bExcludeFileRegex ){ + // 正規表現モード: バリデーションせず正規表現リストへ + m_vecExceptFileRegexPatterns.emplace_back( p ); + continue; + } + int nValidStatus = ValidateKey( p ); + if( 0 != nValidStatus ){ + return nValidStatus; + } + if( _IS_REL_PATH( p ) ){ + push_back_unique( m_vecExceptFileKeys, p ); + }else{ + push_back_unique( m_vecExceptAbsFileKeys, p ); + } + continue; + } + //フィルタを種類ごとに振り分ける enum KeyFilterType{ FILTER_SEARCH, - FILTER_EXCEPT_FILE, FILTER_EXCEPT_FOLDER, }; KeyFilterType keyType = FILTER_SEARCH; - if( token[0] == L'!' ){ - token++; - keyType = FILTER_EXCEPT_FILE; - }else if( token[0] == L'#' ){ + if( token[0] == L'#' ){ token++; keyType = FILTER_EXCEPT_FOLDER; } @@ -108,12 +128,6 @@ class CGrepEnumKeys { // push_back_unique( m_vecSearchFileKeys, token ); return 2; // 絶対パス指定は不可 } - }else if( keyType == FILTER_EXCEPT_FILE ){ - if( bRelPath ){ - push_back_unique( m_vecExceptFileKeys, token ); - }else{ - push_back_unique( m_vecExceptAbsFileKeys, token ); - } }else if( keyType == FILTER_EXCEPT_FOLDER ){ if( bRelPath ){ push_back_unique( m_vecExceptFolderKeys, token ); @@ -131,18 +145,12 @@ class CGrepEnumKeys { return 0; } - /*! - @brief 除外ファイルパターンを追加する - @param[in] lpKeys 除外ファイルパターン - */ + // 除外ファイルパターンを追加する(既存パターンはクリアしない) int AddExceptFile(LPCWSTR lpKeys) { return ParseAndAddException(lpKeys, m_vecExceptFileKeys, m_vecExceptAbsFileKeys); } - /*! - @brief 除外フォルダーパターンを追加する - @param[in] lpKeys 除外フォルダーパターン - */ + // 除外フォルダーパターンを追加する(既存パターンはクリアしない) int AddExceptFolder(LPCWSTR lpKeys) { return ParseAndAddException(lpKeys, m_vecExceptFolderKeys, m_vecExceptAbsFolderKeys); } @@ -157,15 +165,12 @@ class CGrepEnumKeys { const WCHAR* WILDCARD_DELIMITER = L" ;,"; //リストの区切り auto nWildCardLen = int(wcslen(lpKeys)); - WCHAR* pWildCard = new WCHAR[nWildCardLen + 1]; - if (!pWildCard) { - return patterns; - } - wcscpy(pWildCard, lpKeys); + // my_strtok が書き換える作業バッファ。vector により例外時も解放される(RAII) + std::vector wildCard(lpKeys, lpKeys + nWildCardLen + 1); int nPos = 0; WCHAR* token; - while (nullptr != (token = my_strtok(pWildCard, nWildCardLen, &nPos, WILDCARD_DELIMITER))) { //トークン毎に繰り返す。 + while (nullptr != (token = my_strtok(wildCard.data(), nWildCardLen, &nPos, WILDCARD_DELIMITER))) { //トークン毎に繰り返す。 // "を取り除いて左に詰める WCHAR* p; WCHAR* q; @@ -181,43 +186,31 @@ class CGrepEnumKeys { } *q = L'\0'; - std::wstring element(token); - patterns.push_back(element); + patterns.emplace_back(token); } - delete[] pWildCard; return patterns; } private: void ClearItems( void ){ - ClearEnumKeys(m_vecExceptFileKeys); - ClearEnumKeys(m_vecSearchFileKeys); - ClearEnumKeys(m_vecExceptFolderKeys); - ClearEnumKeys(m_vecSearchFolderKeys); + m_vecExceptFileKeys.clear(); + m_vecSearchFileKeys.clear(); + m_vecExceptFolderKeys.clear(); + m_vecSearchFolderKeys.clear(); + m_vecExceptAbsFileKeys.clear(); + m_vecExceptAbsFolderKeys.clear(); + m_vecExceptFileRegexPatterns.clear(); // 正規表現除外パターンもクリア return; } - void ClearEnumKeys( VGrepEnumKeys& keys ){ - for( int i = 0; i < (int)keys.size(); i++ ){ - delete [] keys[ i ]; - } - keys.clear(); - } void push_back_unique( VGrepEnumKeys& keys, LPCWSTR addKey ){ if( ! IsExist( keys, addKey) ){ - WCHAR* newKey = new WCHAR[ wcslen( addKey ) + 1 ]; - wcscpy( newKey, addKey ); - keys.push_back( newKey ); + keys.emplace_back( addKey ); } } - BOOL IsExist( VGrepEnumKeys& keys, LPCWSTR addKey ){ - for( int i = 0; i < (int)keys.size(); i++ ){ - if( wcscmp( keys[ i ], addKey ) == 0 ){ - return TRUE; - } - } - return FALSE; + BOOL IsExist( const VGrepEnumKeys& keys, LPCWSTR addKey ){ + return std::find( keys.begin(), keys.end(), addKey ) != keys.end() ? TRUE : FALSE; } /* diff --git a/sakura_core/grep/GrepPathFormat.h b/sakura_core/grep/GrepPathFormat.h new file mode 100644 index 0000000000..f5413e223f --- /dev/null +++ b/sakura_core/grep/GrepPathFormat.h @@ -0,0 +1,51 @@ +/*! @file + @brief Grep パスリスト整形ヘルパ + + ';' を含むパスの引用符付けと、パスリストの ';' 区切り連結を共通化する。 + (旧: CGrepAgent.cpp の FormatPathList / CDlgGrep.cpp 内の個別実装) +*/ +/* + Copyright (C) 2026, Sakura Editor Organization + + SPDX-License-Identifier: Zlib +*/ +#ifndef SAKURA_GREPPATHFORMAT_3C2B1A9E_5D47_4F06_9A31_7B8E2D4C6F10_H_ +#define SAKURA_GREPPATHFORMAT_3C2B1A9E_5D47_4F06_9A31_7B8E2D4C6F10_H_ +#pragma once + +#include +#include + +//! ';' を含むパスを引用符 '"' で囲んで返す(含まなければそのまま返す) +inline std::wstring QuotePathIfNeeded( std::wstring_view path ) +{ + if( std::wstring_view::npos != path.find( L';' ) ){ + std::wstring quoted; + quoted.reserve( path.size() + 2 ); + quoted += L'"'; + quoted += path; + quoted += L'"'; + return quoted; + } + return std::wstring( path ); +} + +//! パスリストを ';' 区切りで連結して返す(';' を含む要素は引用符で囲む) +template +std::wstring FormatPathList( const ContainerType& container ) +{ + std::wstring strPatterns; + bool firstItem = true; + for( const auto& pattern : container ){ + // パスリストは ';' で区切る(2つ目以降の前に付加する) + if( firstItem ){ + firstItem = false; + }else{ + strPatterns += L';'; + } + strPatterns += QuotePathIfNeeded( std::wstring_view( pattern ) ); + } + return strPatterns; +} + +#endif /* SAKURA_GREPPATHFORMAT_3C2B1A9E_5D47_4F06_9A31_7B8E2D4C6F10_H_ */ diff --git a/sakura_core/sakura.vcxproj b/sakura_core/sakura.vcxproj index 47e6c3bf00..1b5b82a7e1 100644 --- a/sakura_core/sakura.vcxproj +++ b/sakura_core/sakura.vcxproj @@ -179,6 +179,7 @@ + diff --git a/sakura_core/sakura.vcxproj.filters b/sakura_core/sakura.vcxproj.filters index dc70bf5614..d2c909c07d 100644 --- a/sakura_core/sakura.vcxproj.filters +++ b/sakura_core/sakura.vcxproj.filters @@ -202,6 +202,9 @@ Cpp Source Files\grep + + Cpp Source Files\grep + Cpp Source Files\env diff --git a/sakura_core/sakura_rc.rc b/sakura_core/sakura_rc.rc index 82a125629f..c25988b110 100644 --- a/sakura_core/sakura_rc.rc +++ b/sakura_core/sakura_rc.rc @@ -126,7 +126,7 @@ BEGIN COMBOBOX IDC_COMBO_nSortType,34,3,94,65,CBS_DROPDOWNLIST | CBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP END -IDD_GREP DIALOGEX 0, 0, 390, 188 +IDD_GREP DIALOGEX 0, 0, 390, 202 STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Grep" @@ -150,23 +150,24 @@ BEGIN COMBOBOX IDC_COMBO_FILE,60,96,272,120,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP RTEXT "除外ファイル(&J):",IDC_STATIC,4,110,52,8,NOT WS_GROUP COMBOBOX IDC_COMBO_EXCLUDE_FILE,60,110,272,120,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - RTEXT "除外フォルダー(&K):",IDC_STATIC,4,124,53,8 - COMBOBOX IDC_COMBO_EXCLUDE_FOLDER,60,124,272,120,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - GROUPBOX "結果出力",IDC_STATIC,60,140,68,44,WS_GROUP - CONTROL "該当行(&1)",IDC_RADIO_OUTPUTLINE,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,68,152,56,8 - CONTROL "該当部分(&2)",IDC_RADIO_OUTPUTMARKED,"Button",BS_AUTORADIOBUTTON,68,162,56,8 - CONTROL "否該当行(&3)",IDC_RADIO_NOHIT,"Button",BS_AUTORADIOBUTTON,68,172,56,8 - GROUPBOX "結果出力形式",IDC_STATIC,132,140,68,44,WS_GROUP - CONTROL "ノーマル(&4)",IDC_RADIO_OUTPUTSTYLE1,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,140,152,56,8 - CONTROL "ファイル毎(&5)",IDC_RADIO_OUTPUTSTYLE2,"Button",BS_AUTORADIOBUTTON,140,162,56,8 - CONTROL "結果のみ(&6)",IDC_RADIO_OUTPUTSTYLE3,"Button",BS_AUTORADIOBUTTON,140,172,56,8 - GROUPBOX "その他",IDC_STATIC,204,140,180,44,WS_GROUP - CONTROL "ファイル毎最初のみ検索(&7)",IDC_CHECK_FILE_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,212,152,96,8 - CONTROL "フォルダー毎に表示(&8)",IDC_CHECK_SEP_FOLDER,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,212,162,96,8 - CONTROL "ベースフォルダー表示(&9)",IDC_CHECK_BASE_PATH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,212,172,96,8 - LTEXT "文字コードセット(&A):",IDC_STATIC,312,148,66,8 - COMBOBOX IDC_COMBO_CHARSET,312,158,64,120,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - CONTROL "C&P",IDC_CHECK_CP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,312,172,30,8 + CONTROL "正規表現(&R)",IDC_CHK_EXCLUDE_FILE_REGEXP,"Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,60,124,80,8 + RTEXT "除外フォルダー(&K):",IDC_STATIC,4,138,53,8 + COMBOBOX IDC_COMBO_EXCLUDE_FOLDER,60,138,272,120,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP + GROUPBOX "結果出力",IDC_STATIC,60,154,68,44,WS_GROUP + CONTROL "該当行(&1)",IDC_RADIO_OUTPUTLINE,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,68,166,56,8 + CONTROL "該当部分(&2)",IDC_RADIO_OUTPUTMARKED,"Button",BS_AUTORADIOBUTTON,68,176,56,8 + CONTROL "否該当行(&3)",IDC_RADIO_NOHIT,"Button",BS_AUTORADIOBUTTON,68,186,56,8 + GROUPBOX "結果出力形式",IDC_STATIC,132,154,68,44,WS_GROUP + CONTROL "ノーマル(&4)",IDC_RADIO_OUTPUTSTYLE1,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,140,166,56,8 + CONTROL "ファイル毎(&5)",IDC_RADIO_OUTPUTSTYLE2,"Button",BS_AUTORADIOBUTTON,140,176,56,8 + CONTROL "結果のみ(&6)",IDC_RADIO_OUTPUTSTYLE3,"Button",BS_AUTORADIOBUTTON,140,186,56,8 + GROUPBOX "その他",IDC_STATIC,204,154,180,44,WS_GROUP + CONTROL "ファイル毎最初のみ検索(&7)",IDC_CHECK_FILE_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,212,166,96,8 + CONTROL "フォルダー毎に表示(&8)",IDC_CHECK_SEP_FOLDER,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,212,176,96,8 + CONTROL "ベースフォルダー表示(&9)",IDC_CHECK_BASE_PATH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,212,186,96,8 + LTEXT "文字コードセット(&A):",IDC_STATIC,312,162,66,8 + COMBOBOX IDC_COMBO_CHARSET,312,172,64,120,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "C&P",IDC_CHECK_CP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,312,186,30,8 DEFPUSHBUTTON "検索(&F)",IDOK,336,4,50,14,WS_GROUP PUSHBUTTON "キャンセル(&X)",IDCANCEL,336,20,50,14 PUSHBUTTON "ヘルプ(&H)",IDC_BUTTON_HELP,336,36,50,14 diff --git a/sakura_core/tests1.vcxproj b/sakura_core/tests1.vcxproj index bfc10f989e..844bd0100a 100644 --- a/sakura_core/tests1.vcxproj +++ b/sakura_core/tests1.vcxproj @@ -158,6 +158,10 @@ + + + + diff --git a/sakura_core/tests1.vcxproj.filters b/sakura_core/tests1.vcxproj.filters index 96cb57f1f4..ebc8286680 100644 --- a/sakura_core/tests1.vcxproj.filters +++ b/sakura_core/tests1.vcxproj.filters @@ -124,6 +124,18 @@ Test Files + + Test Files + + + Test Files + + + Test Files + + + Test Files + Test Files diff --git a/src/main/resources/sakura_rc.h b/src/main/resources/sakura_rc.h index ffdeaa0f11..fed5e9352c 100644 --- a/src/main/resources/sakura_rc.h +++ b/src/main/resources/sakura_rc.h @@ -1850,15 +1850,16 @@ #define IDS_EOLTYPENAME_4 35054 #define IDS_EOLTYPENAME_5 35055 #define IDS_EOLTYPENAME_6 35056 +#define IDC_CHK_EXCLUDE_FILE_REGEXP 1742 #define IDC_STATIC -1 // Next default values for new objects -// +// #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 35057 #define _APS_NEXT_COMMAND_VALUE 101 -#define _APS_NEXT_CONTROL_VALUE 1741 +#define _APS_NEXT_CONTROL_VALUE 1743 #define _APS_NEXT_SYMED_VALUE 10000 #endif #endif diff --git a/src/test/cpp/tests1/test-ccommandline.cpp b/src/test/cpp/tests1/test-ccommandline.cpp index c4ee6bd19e..86a3983a5b 100644 --- a/src/test/cpp/tests1/test-ccommandline.cpp +++ b/src/test/cpp/tests1/test-ccommandline.cpp @@ -1,6 +1,6 @@ /*! @file */ /* - Copyright (C) 2018-2022, Sakura Editor Organization + Copyright (C) 2018-2026, Sakura Editor Organization SPDX-License-Identifier: Zlib */ @@ -678,6 +678,19 @@ TEST(CCommandLine, ParseGrepReplaceCreateBackupFiles) EXPECT_TRUE(cCommandLine.GetGrepInfoRef().bGrepBackup); } +/*! + * @brief パラメータ解析(-GOPT)の仕様:除外ファイルを正規表現として扱う + * @remark -GOPT=E が指定されていなければ false、指定されていたら true + */ +TEST(CCommandLine, ParseGrepExcludeFileRegexp) +{ + CCommandLine cCommandLine; + cCommandLine.ParseCommandLine(L"", false); + EXPECT_FALSE(cCommandLine.GetGrepInfoRef().bGrepExcludeFileRegexp); + cCommandLine.ParseCommandLine(L"-GOPT=E", false); + EXPECT_TRUE(cCommandLine.GetGrepInfoRef().bGrepExcludeFileRegexp); +} + /*! * @brief パラメータ解析(-GCODE)の仕様 * @remark -GCODEが指定されていなければSJIS @@ -940,3 +953,328 @@ TEST(CCommandLine, ParseMaxFilePath) } #endif //ifndef __MINGW32__ + +// ====================================================================== +// Phase 2-A: Grep CLI argument coverage (PR #2459) +// ====================================================================== + +// ----- -GOPT 複合・境界値 ----- + +/*! + * @brief パラメータ解析(-GOPT)の仕様:複数フラグの同時指定 + * @remark S(サブフォルダー), R(正規表現), L(大文字小文字区別), W(単語単位) がすべて同時に有効になることを確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_MultipleFlagsCombined) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=SRLW", false); + const auto& opt = c.GetGrepInfoRef(); + EXPECT_TRUE(opt.bGrepSubFolder); + EXPECT_TRUE(opt.sGrepSearchOption.bRegularExp); + EXPECT_TRUE(opt.sGrepSearchOption.bLoHiCase); + EXPECT_TRUE(opt.sGrepSearchOption.bWordOnly); +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:既知の全フラグを指定した場合の挙動 + * @remark サポートされている全オプション文字を一度に指定し、各対応フィールドが適切に切り替わること(後勝ち含む)を確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_AllKnownFlagsOn) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=XUHSLRKPNW123FBDCOE", false); + const auto& opt = c.GetGrepInfoRef(); + EXPECT_TRUE(opt.bGrepCurFolder); // X: 現フォルダー検索 + EXPECT_TRUE(opt.bGrepStdout); // U: 標準出力モード + EXPECT_FALSE(opt.bGrepHeader); // H: ヘッダー非表示 + EXPECT_TRUE(opt.bGrepSubFolder); // S: サブフォルダー検索 + EXPECT_TRUE(opt.sGrepSearchOption.bLoHiCase); // L: 大文字小文字区別 + EXPECT_TRUE(opt.sGrepSearchOption.bRegularExp); // R: 正規表現 + EXPECT_EQ(CODE_AUTODETECT, opt.nGrepCharSet); // K: 文字コード自動判別 + EXPECT_EQ(2, opt.nGrepOutputLineType); // P=1, N=2 (N wins) + EXPECT_TRUE(opt.sGrepSearchOption.bWordOnly); // W: 単語単位 + EXPECT_EQ(3, opt.nGrepOutputStyle); // 1->2->3 (3 wins) + EXPECT_TRUE(opt.bGrepOutputFileOnly); // F: ファイル毎最初のみ + EXPECT_TRUE(opt.bGrepOutputBaseFolder); // B: ベースフォルダー表示 + EXPECT_TRUE(opt.bGrepSeparateFolder); // D: フォルダー毎表示 + EXPECT_TRUE(opt.bGrepPaste); // C: クリップボード貼り付け + EXPECT_TRUE(opt.bGrepBackup); // O: バックアップ有効 + EXPECT_TRUE(opt.bGrepExcludeFileRegexp); // E: 除外ファイル正規表現 +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:同一フラグの重複指定 + * @remark 同一の文字を複数回指定しても冪等(同じ状態を維持すること)であることを確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_DuplicateSameFlag) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=SS", false); + EXPECT_TRUE(c.GetGrepInfoRef().bGrepSubFolder); +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:排他フラグ(出力スタイル)の後勝ち + * @remark 1, 2, 3 の出力スタイルを連続指定した場合、最後に指定された数値が有効になることを確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_StyleLastWins) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=123", false); + EXPECT_EQ(3, c.GetGrepInfoRef().nGrepOutputStyle); +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:排他フラグ(出力行タイプ)の後勝ち + * @remark P(該当行)とN(否該当行)を連続指定した場合、最後に指定された方が有効になることを確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_OutputLineTypeLastWins) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=PN", false); + EXPECT_EQ(2, c.GetGrepInfoRef().nGrepOutputLineType); +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:未知のフラグ文字の無視 + * @remark サポートされていない文字(ZやQなど)が含まれていても黙殺し、有効な文字(Sなど)は正常に処理することを確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_UnknownCharIgnored) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=SZQ", false); + EXPECT_TRUE(c.GetGrepInfoRef().bGrepSubFolder); +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:値なし指定時のフェイルセーフ + * @remark = の後に値が指定されていない場合、初期状態を維持すること(クラッシュしないこと)を確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_EmptyValueRejected) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=", false); + const auto& opt = c.GetGrepInfoRef(); + // 初期値のままであること + EXPECT_FALSE(opt.bGrepSubFolder); +} + +/*! + * @brief パラメータ解析(-GOPT)の仕様:数値と文字の混在指定 + * @remark 数字(スタイル)と英字(フラグ)が混在していてもすべて正しくパースすることを確認する。 + */ +TEST(CCommandLine, ParseGrepOpt_MixedDigitsAndLetters) +{ + CCommandLine c; + c.ParseCommandLine(L"-GOPT=1SRL", false); + const auto& opt = c.GetGrepInfoRef(); + EXPECT_EQ(1, opt.nGrepOutputStyle); + EXPECT_TRUE(opt.bGrepSubFolder); + EXPECT_TRUE(opt.sGrepSearchOption.bRegularExp); + EXPECT_TRUE(opt.sGrepSearchOption.bLoHiCase); +} + +// ----- -GREPR と -GKEY の連動 ----- + +/*! + * @brief パラメータ解析(-GREPR)の仕様:置換フラグの自動設定 + * @remark GREPR(置換後文字列)が指定された場合、bGrepReplaceが自動的にtrueになることを確認する。 + */ +TEST(CCommandLine, ParseGrepReplace_GREPRSetsReplaceFlag) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=foo -GREPR=bar", false); + EXPECT_STREQ(L"foo", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); + EXPECT_STREQ(L"bar", c.GetGrepInfoRef().cmGrepRep.GetStringPtr()); + EXPECT_TRUE(c.GetGrepInfoRef().bGrepReplace); +} + +/*! + * @brief パラメータ解析(-GREPR)の仕様:置換フラグの非設定 + * @remark GREPRが指定されずGKEYのみの場合、bGrepReplaceはfalseのままであることを確認する。 + */ +TEST(CCommandLine, ParseGrepReplace_OnlyGKEYDoesNotSetReplaceFlag) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=foo", false); + EXPECT_FALSE(c.GetGrepInfoRef().bGrepReplace); +} + +/*! + * @brief パラメータ解析(-GREPR)の仕様:空文字列での置換 + * @remark 置換文字列として空文字列("")を指定した場合もbGrepReplaceがtrueとして扱われることを確認する。 + */ +TEST(CCommandLine, ParseGrepReplace_GREPREmptyAllowed) +{ + CCommandLine c; + c.ParseCommandLine(L"-GREPR=\"\"", false); + EXPECT_STREQ(L"", c.GetGrepInfoRef().cmGrepRep.GetStringPtr()); + EXPECT_TRUE(c.GetGrepInfoRef().bGrepReplace); +} + +/*! + * @brief パラメータ解析(-GREPR)の仕様:クリップボードからの貼り付け置換 + * @remark GOPT=C (クリップボード貼り付け) と GREPR を両立して設定することを確認する。 + */ +TEST(CCommandLine, ParseGrepReplace_ClipboardPasteFlag) +{ + CCommandLine c; + c.ParseCommandLine(L"-GREPR=foo -GOPT=C", false); + EXPECT_TRUE(c.GetGrepInfoRef().bGrepReplace); + EXPECT_TRUE(c.GetGrepInfoRef().bGrepPaste); +} + +// ----- case-insensitive と引数順序 ----- + +/*! + * @brief パラメータ解析の仕様:オプション名の大文字小文字無視(小文字のみ) + * @remark -gkey のように小文字で指定しても -GKEY と同様に認識することを確認する。 + */ +TEST(CCommandLine, ParseGrep_OptionNameLowerCase) +{ + CCommandLine c; + c.ParseCommandLine(L"-gkey=foo", false); + EXPECT_STREQ(L"foo", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +/*! + * @brief パラメータ解析の仕様:オプション名の大文字小文字無視(混在) + * @remark -Gkey や -gFOLDER のように大文字小文字が混在していても正しく認識することを確認する。 + */ +TEST(CCommandLine, ParseGrep_OptionNameMixedCase) +{ + CCommandLine c; + c.ParseCommandLine(L"-Gkey=foo -gFOLDER=C:\\tmp", false); + EXPECT_STREQ(L"foo", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); + EXPECT_STREQ(L"C:\\tmp", c.GetGrepInfoRef().cmGrepFolder.GetStringPtr()); +} + +/*! + * @brief パラメータ解析の仕様:引数順序の非依存性 + * @remark 異なる順序で引数を指定しても、解析結果(GrepInfo)が完全に一致することを確認する。 + */ +TEST(CCommandLine, ParseGrep_ArgumentOrderInvariant) +{ + CCommandLine c1, c2; + c1.ParseCommandLine(L"-GKEY=foo -GFILE=*.cpp -GFOLDER=C:\\tmp", false); + c2.ParseCommandLine(L"-GFOLDER=C:\\tmp -GFILE=*.cpp -GKEY=foo", false); + EXPECT_STREQ(c1.GetGrepInfoRef().cmGrepKey.GetStringPtr(), c2.GetGrepInfoRef().cmGrepKey.GetStringPtr()); + EXPECT_STREQ(c1.GetGrepInfoRef().cmGrepFile.GetStringPtr(), c2.GetGrepInfoRef().cmGrepFile.GetStringPtr()); + EXPECT_STREQ(c1.GetGrepInfoRef().cmGrepFolder.GetStringPtr(), c2.GetGrepInfoRef().cmGrepFolder.GetStringPtr()); +} + +/*! + * @brief パラメータ解析の仕様:GrepモードとGrepダイアログの両立 + * @remark -GREPMODE と -GREPDLG が同時に指定された場合、両方のフラグが立つことを確認する。 + */ +TEST(CCommandLine, ParseGrep_GrepModeAndGrepDlgBothSpecified) +{ + CCommandLine c; + c.ParseCommandLine(L"-GREPMODE -GREPDLG", false); + EXPECT_TRUE(c.IsGrepMode()); + EXPECT_TRUE(c.IsGrepDlg()); +} + +/*! + * @brief パラメータ解析の仕様:同一オプションの重複指定(後勝ち) + * @remark -GKEY が複数回指定された場合、最後の値で上書きすることを確認する。 + */ +TEST(CCommandLine, ParseGrep_GKeyDuplicate_LaterWins) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=foo -GKEY=bar", false); + EXPECT_STREQ(L"bar", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +// ----- -GKEY / -GFILE / -GFOLDER の値境界 ----- + +/*! + * @brief パラメータ解析(-GKEY)の仕様:最小文字数(1文字) + * @remark 引数値が1文字のみの場合でも欠落せず正しく保持することを確認する。 + */ +TEST(CCommandLine, ParseGrepKey_SingleChar_Boundary) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=a", false); + EXPECT_STREQ(L"a", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GKEY)の仕様:長大な文字列の許容 + * @remark バッファサイズを超えるような非常に長い入力(4096文字)でも、切り詰めやクラッシュなく保持することを確認する。 + */ +TEST(CCommandLine, ParseGrepKey_VeryLongValue_4096Chars) +{ + CCommandLine c; + std::wstring longKey(4096, L'A'); + std::wstring cmd = L"-GKEY=" + longKey; + c.ParseCommandLine(cmd.c_str(), false); + EXPECT_STREQ(longKey.c_str(), c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GKEY)の仕様:マルチバイト文字・サロゲートペア + * @remark UTF-16 サロゲートペア(U+1F600)を含む値が正しく保持されることを確認する。 + * ソースファイルのコードページ(932)では直接記述できないため、\xD83D\xDE00 で UTF-16 リテラルとして指定する。 + */ +TEST(CCommandLine, ParseGrepKey_JapaneseCharsAndEmoji) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=日本語\xD83D\xDE00", false); + EXPECT_STREQ(L"日本語\xD83D\xDE00", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GKEY)の仕様:値の中に区切り文字(=)が含まれる場合 + * @remark 最初の '=' のみをオプションと値の区切りとして扱い、以降の '=' は値の一部として保持することを確認する。 + */ +TEST(CCommandLine, ParseGrepKey_ContainsEqualsInValue) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=foo=bar", false); + EXPECT_STREQ(L"foo=bar", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GKEY)の仕様:エスケープされたダブルクォート + * @remark `""` によってエスケープされたダブルクォートを、正しく1つの `"` としてデコードすることを確認する。 + */ +TEST(CCommandLine, ParseGrepKey_EscapedDoubleQuotes) +{ + CCommandLine c; + c.ParseCommandLine(L"-GKEY=\"he said \"\"hi\"\"\"", false); + EXPECT_STREQ(L"he said \"hi\"", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GFILE)の仕様:Grep除外用正規表現パターンの保持 + * @remark 正規表現を利用したファイル除外パターン `!.*test.*\\.cpp$` 等を文字列として壊れず保持することを確認する。 + */ +TEST(CCommandLine, ParseGrepFile_RegexExcludePatternFromPR2449) +{ + CCommandLine c; + c.ParseCommandLine(L"-GFILE=*.cpp;!.*test.*\\.cpp$", false); + EXPECT_STREQ(L"*.cpp;!.*test.*\\.cpp$", c.GetGrepInfoRef().cmGrepFile.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GFOLDER)の仕様:相対パスの入力 + * @remark CommandLineパース層では絶対パスへの変換を行わず、指定された相対パス(`.`)をそのまま保持することを確認する。 + */ +TEST(CCommandLine, ParseGrepFolder_RelativePath) +{ + CCommandLine c; + c.ParseCommandLine(L"-GFOLDER=.", false); + EXPECT_STREQ(L".", c.GetGrepInfoRef().cmGrepFolder.GetStringPtr()); +} + +/*! + * @brief パラメータ解析(-GFOLDER)の仕様:UNCパスの入力 + * @remark ネットワークパス (\\server\share\src) を、バックスラッシュの欠落などなく正常に保持することを確認する。 + */ +TEST(CCommandLine, ParseGrepFolder_UncPath) +{ + CCommandLine c; + c.ParseCommandLine(L"-GFOLDER=\\\\server\\share\\src", false); + EXPECT_STREQ(L"\\\\server\\share\\src", c.GetGrepInfoRef().cmGrepFolder.GetStringPtr()); +} diff --git a/src/test/cpp/tests1/test-cdlggrep.cpp b/src/test/cpp/tests1/test-cdlggrep.cpp new file mode 100644 index 0000000000..d602d478d1 --- /dev/null +++ b/src/test/cpp/tests1/test-cdlggrep.cpp @@ -0,0 +1,586 @@ +/*! @file */ +/* + Copyright (C) 2026, Sakura Editor Organization + + SPDX-License-Identifier: Zlib +*/ + +#include "pch.h" +#include + +#include +#include + +#include "dlg/CDlgGrep.h" +#include "dlg/ModalDialogCloser.hpp" +#include "env/ShareDataTestSuite.hpp" +#include "grep/CGrepEnumKeys.h" +#include "mem/CNativeW.h" +#include "sakura_rc.h" +#include "env/DLLSHAREDATA.h" + +namespace { + +struct CDlgGrepTest + : public ::testing::Test + , public env::ShareDataTestSuite +{ + static void SetUpTestSuite() + { + // CDlgGrep は履歴や既定値の取得で ShareData を参照するため、事前に初期化する。 + SetUpShareData(); + } + + static void TearDownTestSuite() + { + TearDownShareData(); + } +}; + +} // namespace + +// ====================================================================== +// Phase 3-A: CDlgGrep Unit Tests (PR #2459) +// ====================================================================== + +// ----- 既定値とパック処理 ----- + +/*! + * @brief コンストラクタ直後の既定値検証 (DG-01) + * @remark コンストラクタ呼び出し直後、サブフォルダー検索や出力形式などが既定値になっていることを確認する。 + */ +TEST_F(CDlgGrepTest, DefaultMemberValues_Constructor) +{ + CDlgGrep dlg; + + EXPECT_FALSE(dlg.m_bSubFolder); // 初期状態ではサブフォルダー検索は無効 + EXPECT_FALSE(dlg.m_bFromThisText); // 初期状態では編集中テキスト検索は無効 + EXPECT_EQ(CODE_SJIS, dlg.m_nGrepCharSet); // 既定の文字コードは SJIS + EXPECT_EQ(1, dlg.m_nGrepOutputLineType); // 既定の出力対象はヒット行 + EXPECT_EQ(1, dlg.m_nGrepOutputStyle); // 既定の出力形式は Normal + EXPECT_EQ(L'\0', dlg.m_szFile[0]); // 検索ファイル欄は空で開始 + EXPECT_EQ(L'\0', dlg.m_szFolder[0]); // 検索フォルダー欄は空で開始 + EXPECT_EQ(L'\0', dlg.m_szExcludeFile[0]); // 除外ファイル欄は空で開始 + EXPECT_EQ(L'\0', dlg.m_szExcludeFolder[0]); // 除外フォルダー欄は空で開始 +} + +/*! + * @brief 除外パターンの定数検証 (DG-02) + * @remark DEFAULT_EXCLUDE_FILE_PATTERN_REGEX / _WILDCARD と DEFAULT_EXCLUDE_FOLDER_PATTERN が仕様通り定義されていることを確認する。 + */ +TEST_F(CDlgGrepTest, DefaultExcludePatterns_Constants) +{ + EXPECT_STREQ(L".*\\.msi$;.*\\.exe$;.*\\.obj$;.*\\.pdb$;.*\\.ilk$;.*\\.res$;.*\\.pch$;.*\\.iobj$;.*\\.ipdb$", DEFAULT_EXCLUDE_FILE_PATTERN_REGEX); + EXPECT_STREQ(L"*.msi;*.exe;*.obj;*.pdb;*.ilk;*.res;*.pch;*.iobj;*.ipdb", DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD); + EXPECT_STREQ(L".git;.svn;.vs", DEFAULT_EXCLUDE_FOLDER_PATTERN); +} + +/*! + * @brief パック処理:除外指定なし (DG-03) + * @remark 除外指定がない場合、検索ファイルパターンをそのまま返すことを確認する。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_NoExclusions) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.cpp", packed.c_str()); +} + +/*! + * @brief パック処理:除外フォルダーあり (DG-04) + * @remark 除外フォルダーが指定された場合、`#` プレフィックスを付加して結合することを確認する。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_WithExcludeFolders) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szExcludeFolder, L".git;.svn"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.cpp;#.git;#.svn", packed.c_str()); +} + +/*! + * @brief パック処理:除外ファイルあり (DG-05) + * @remark 除外ファイルが指定された場合、`!` プレフィックスを付加して結合することを確認する。 + * 検索対象ファイルが空の場合は `*.*` を補う。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_WithExcludeFiles) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szFile, L""); // 空の場合は内部で *.* になる + wcscpy_s(dlg.m_szExcludeFile, L"*.obj;*.exe"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.*;!*.obj;!*.exe", packed.c_str()); +} + +/*! + * @brief パック処理:除外フォルダー内の「!」エスケープ (DG-06) + * @remark `!` が含まれるフォルダー名をダブルクォートでエスケープして結合することを確認する。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_EscapeBangInExcludeFolder) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szExcludeFolder, L"!special"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.*;\"#!special\"", packed.c_str()); +} + +/*! + * @brief パック処理:除外フォルダー内の「#」エスケープ (DG-07) + * @remark `#` が含まれるフォルダー名をダブルクォートでエスケープして結合することを確認する。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_EscapeHashInExcludeFolder) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szExcludeFolder, L"#hash"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.*;\"##hash\"", packed.c_str()); +} + +/*! + * @brief パック処理:除外フォルダー名のスペース区切り (DG-08) + * @remark SplitPattern はスペースを区切り文字として扱うため、 + * 「my folder」は「my」と「folder」に分割され、それぞれに # が付く。 + * スペース含みの単一フォルダー名は GetPackedGFileString 経由では扱えない(既知制限)。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_EscapeSpaceInExcludeFolder) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szExcludeFolder, L"my folder"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.*;#my;#folder", packed.c_str()); +} + +/*! + * @brief パック処理:除外フォルダー内のセミコロン (DG-09) + * @remark セミコロンは区切り文字として処理され、それぞれ独立した除外指定となることを確認する。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_EscapeSemicolonInExcludeFolder) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szExcludeFolder, L"a;b"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.*;#a;#b", packed.c_str()); +} + +/*! + * @brief パック処理:複合パターン (DG-10) + * @remark 除外フォルダーと除外ファイルの両方が指定された場合、すべてを正しくパック文字列に結合することを確認する。 + */ +TEST_F(CDlgGrepTest, GetPackedGFileString_CombinedAllExclusions) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szExcludeFolder, L"build;dist"); + wcscpy_s(dlg.m_szExcludeFile, L"*.obj;*.tmp"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + EXPECT_STREQ(L"*.cpp;#build;#dist;!*.obj;!*.tmp", packed.c_str()); +} + +/*! + * @brief パックとアンパックのラウンドトリップ検証 (GUI -> CLI -> EnumKeys) ワイルドカード (DG-11) + * @remark GUIで設定した複雑なパターンをパックし、bExcludeFileRegex=false で SetFileKeys を呼ぶと + * ! プレフィックスはワイルドカード除外リスト(m_vecExceptFileKeys)に入ることを確認する。 + */ +TEST_F(CDlgGrepTest, RoundTrip_GuiToCliToEnumKeys) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szExcludeFolder, L"build;dist"); + wcscpy_s(dlg.m_szExcludeFile, L"*.obj;*.tmp"); + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + + // bExcludeFileRegex=false(既定): ! プレフィックスはワイルドカード除外へ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(packed.c_str())); + + EXPECT_EQ(1, keys.m_vecSearchFileKeys.size()); // 検索対象は 1 件 + EXPECT_STREQ(L"*.cpp", keys.m_vecSearchFileKeys[0].c_str()); + + EXPECT_EQ(2, keys.m_vecExceptFolderKeys.size()); // 除外フォルダーは 2 件 + EXPECT_STREQ(L"build", keys.m_vecExceptFolderKeys[0].c_str()); + EXPECT_STREQ(L"dist", keys.m_vecExceptFolderKeys[1].c_str()); + + EXPECT_EQ(2, keys.m_vecExceptFileKeys.size()); // ! プレフィックスはワイルドカード除外に入る + EXPECT_STREQ(L"*.obj", keys.m_vecExceptFileKeys[0].c_str()); + EXPECT_STREQ(L"*.tmp", keys.m_vecExceptFileKeys[1].c_str()); + EXPECT_TRUE(keys.m_vecExceptFileRegexPatterns.empty()); // 正規表現除外リストは空 +} + +/*! + * @brief パックとアンパックのラウンドトリップ検証 (GUI -> CLI -> EnumKeys) 正規表現 (DG-11b) + * @remark GetPackedGFileString は regex ON/OFF にかかわらず常に ! を出力する。 + * SetFileKeys(packed, true) で呼ぶと ! プレフィックスは正規表現除外リストに入ることを確認する。 + */ +TEST_F(CDlgGrepTest, RoundTrip_GuiToCliToEnumKeys_RegexMode) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szExcludeFile, L".*skip.*\\.txt$"); + dlg.m_bExcludeFileRegularExp = TRUE; + std::wstring packed = dlg.GetPackedGFileString().GetStringPtr(); + + // パック文字列は常に '!' を使う(regex フラグ非依存) + EXPECT_NE(std::wstring::npos, packed.find(L"!")); + + // bExcludeFileRegex=true: ! プレフィックスは正規表現除外へ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(packed.c_str(), /*bExcludeFileRegex=*/true)); + + EXPECT_EQ(1, keys.m_vecExceptFileRegexPatterns.size()); + EXPECT_STREQ(L".*skip.*\\.txt$", keys.m_vecExceptFileRegexPatterns[0].c_str()); + EXPECT_TRUE(keys.m_vecExceptFileKeys.empty()); +} + +// ----- 切り出し関数のテスト ----- + +/*! + * @brief 既定除外パターンの決定:履歴が空の場合 (DG-12) + * @remark 履歴が空の場合、定数で定義された既定パターンを使用し、履歴にも追加することを確認する。 + */ +TEST_F(CDlgGrepTest, DetermineDefaultExcludePatterns_EmptyHistorySetsDefaults) +{ + CDlgGrep dlg; + GetDllShareDataPtr()->m_sSearchKeywords.m_aExcludeFiles.clear(); + dlg.DetermineDefaultExcludePatterns(); + EXPECT_STREQ(DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD, dlg.m_szExcludeFile); +} + +/*! + * @brief 既定除外パターンの決定:履歴が存在する場合 (DG-13) + * @remark 履歴が既に存在する場合、先頭の履歴を既定の除外パターンとして使用することを確認する。 + */ +TEST_F(CDlgGrepTest, DetermineDefaultExcludePatterns_NonEmptyHistoryUsesHistoryTop) +{ + CDlgGrep dlg; + GetDllShareDataPtr()->m_sSearchKeywords.m_aExcludeFiles.clear(); + GetDllShareDataPtr()->m_sSearchKeywords.m_aExcludeFiles.push_back(L"*.bak"); + dlg.DetermineDefaultExcludePatterns(); + EXPECT_STREQ(L"*.bak", dlg.m_szExcludeFile); +} + +/*! + * @brief 既定除外パターンの決定:既に値がセットされている場合 (DG-14) + * @remark 既に `m_szExcludeFile` に値がセットされている場合は、既定値の書き込みをスキップすることを確認する。 + */ +TEST_F(CDlgGrepTest, DetermineDefaultExcludePatterns_AlreadySetSkipped) +{ + CDlgGrep dlg; + wcscpy_s(dlg.m_szExcludeFile, L"*.tmp"); + dlg.DetermineDefaultExcludePatterns(); + EXPECT_STREQ(L"*.tmp", dlg.m_szExcludeFile); +} + +// ----- :HWND: トークンのテスト ----- + +/*! + * @brief HWNDフォーマット(BuildHwndFileToken)のテスト (DG-15) + * @remark 指定されたHWND値を `:HWND:` プレフィックス付きの固定長文字列として正しくフォーマットすることを確認する。 + */ +TEST_F(CDlgGrepTest, BuildHwndFileToken_Format) +{ + HWND hwnd = (HWND)(uintptr_t)0xABCD1234; + std::wstring token = CDlgGrep::BuildHwndFileToken(hwnd); +#ifdef _WIN64 + EXPECT_STREQ(L":HWND:00000000abcd1234", token.c_str()); // 64bit は 16 桁固定 +#else + EXPECT_STREQ(L":HWND:abcd1234", token.c_str()); // 32bit は 8 桁固定 +#endif +} + +/*! + * @brief HWNDフォーマット(BuildHwndFileToken)のテスト(NULL指定) (DG-16) + * @remark HWNDがNULLの場合でも正しくゼロ埋めしてフォーマットすることを確認する。 + */ +TEST_F(CDlgGrepTest, BuildHwndFileToken_NullHwnd) +{ + HWND hwnd = NULL; + std::wstring token = CDlgGrep::BuildHwndFileToken(hwnd); +#ifdef _WIN64 + EXPECT_STREQ(L":HWND:0000000000000000", token.c_str()); // NULL でもゼロ埋め +#else + EXPECT_STREQ(L":HWND:00000000", token.c_str()); // NULL でもゼロ埋め +#endif +} + +/*! + * @brief HWNDファイルトークン判定(IsHwndFileToken)のポジティブテスト (DG-17) + * @remark 正しいフォーマットの `:HWND:` 文字列に対して true を返すことを確認する。 + */ +TEST_F(CDlgGrepTest, IsHwndFileToken_PositiveCases) +{ + EXPECT_TRUE(CDlgGrep::IsHwndFileToken(L":HWND:00000000")); // 正しい接頭辞なら true + EXPECT_TRUE(CDlgGrep::IsHwndFileToken(L":HWND:abc123")); +} + +/*! + * @brief HWNDファイルトークン判定(IsHwndFileToken)のネガティブテスト (DG-18) + * @remark 異なるフォーマットや通常のファイル名に対して false を返すことを確認する。 + */ +TEST_F(CDlgGrepTest, IsHwndFileToken_NegativeCases) +{ + EXPECT_FALSE(CDlgGrep::IsHwndFileToken(L"*.cpp")); // 通常のファイル名は false + EXPECT_FALSE(CDlgGrep::IsHwndFileToken(L":hwnd:abc")); + EXPECT_FALSE(CDlgGrep::IsHwndFileToken(L":HWND")); + EXPECT_FALSE(CDlgGrep::IsHwndFileToken(L"")); +} + +// ============================================================================= +// フル GUI 結合テスト (Phase 5 / DG-20 ~ 27) +// ============================================================================= + +struct CDlgGrepGuiTest : public ::testing::Test, public env::ShareDataTestSuite { + static void SetUpTestSuite() { SetUpShareData(); } + static void TearDownTestSuite() { TearDownShareData(); } + + std::filesystem::path m_testDir; + + void SetUp() override { + if (::GetModuleHandleW(nullptr) == nullptr) { + GTEST_SKIP() << "GUI not available"; + } + + // tests1.exe の配置ディレクトリ基準で一時フォルダを作成(GetTempPath はパス非依存で環境差異が出るため使わない) + wchar_t exePath[MAX_PATH]; + ::GetModuleFileNameW(nullptr, exePath, MAX_PATH); + m_testDir = std::filesystem::path(exePath).parent_path() / L"test_grep_gui"; + std::filesystem::create_directories(m_testDir); + + // フォルダ存在チェックを通過させるためのダミーファイル + std::ofstream ofs(m_testDir / L"dummy.cpp", std::ios::trunc); + ofs << "int main() { return 0; }\n"; + } + + void TearDown() override { + std::error_code ec; + std::filesystem::remove_all(m_testDir, ec); + } +}; + +/*! + * @brief キャンセル即時送信テスト (DG-20) + * @remark DoModal に対して直ちに IDCANCEL を送信し、0 を返して正常終了することを確認する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalCancelImmediately_NoException) +{ + dialog::ModalDialogCloser closer; // デフォルトで IDCANCEL を送信 + CDlgGrep dlg; + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(0, rc); +} + +/*! + * @brief 有効入力でのOK送信テスト (DG-21) + * @remark 有効な検索キーワード・ファイル・フォルダを指定して IDOK を送信し、正常に受理されて 1 (OK) が返ることを確認する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_WithValidInputs_ReturnsOK) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + }); + CDlgGrep dlg; + dlg.m_strText = L"search"; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szFolder, m_testDir.c_str()); + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(1, rc); +} + +/*! + * @brief フォルダ未指定時のフォールバック動作 (DG-22) + * @remark フォルダが空の状態で IDOK を送信した場合、 + * GetData 内でカレントフォルダ等にフォールバックして成功する仕様を凍結する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_EmptyFolder_FallsBackToCurrentDir) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + HWND hOwner = ::GetWindow(hWnd, GW_OWNER); + if (hOwner != nullptr) { + // MessageBox を閉じる + ::SendMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDCANCEL, BN_CLICKED), 0); + // 本体をキャンセルで閉じて復帰させる + ::PostMessageW(hOwner, WM_COMMAND, MAKEWPARAM(IDCANCEL, BN_CLICKED), 0); + } else { + // 本体のダイアログには OK を送る + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + } + }); + + CDlgGrep dlg; + dlg.m_strText = L"search"; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + dlg.m_szFolder[0] = L'\0'; // 空のフォルダ + + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(1, rc); // 空フォルダでも GetData 内でカレントフォルダ等にフォールバックして成功する仕様を凍結 +} + +/*! + * @brief 不正正規表現指定時の DoModal 動作 (DG-23) + * @remark 不正な正規表現を指定して IDOK を送信した場合でも、 + * DoModal は成功(rc=1)を返す仕様を凍結する。 + * 構文チェックは DoModal 内ではブロッキングしない。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_InvalidRegex_StillReturnsOK) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + HWND hOwner = ::GetWindow(hWnd, GW_OWNER); + if (hOwner != nullptr) { + ::SendMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDCANCEL, BN_CLICKED), 0); + ::PostMessageW(hOwner, WM_COMMAND, MAKEWPARAM(IDCANCEL, BN_CLICKED), 0); + } else { + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + } + }); + + CDlgGrep dlg; + dlg.m_strText = L"(invalid"; + dlg.m_sSearchOption.bRegularExp = true; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szFolder, m_testDir.c_str()); + + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(1, rc); // 不正正規表現でも DoModal は成功する仕様を凍結(構文チェックは DoModal 内でブロッキングしない) +} + +/*! + * @brief セミコロン区切りフォルダの仕様凍結テスト (DG-24) + * @remark セミコロンは CreateFolders で区切り文字として分割される仕様(既知制限)。 + * セミコロン含みの単一フォルダとしては扱えないため、 + * セミコロンで区切った 2 つの実在フォルダを渡して IDOK が返ることを確認する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_FolderWithSemicolon_IsSplitBySeparator) +{ + // セミコロンを含むパスは CreateFolders で分割されるため、 + // 「セミコロン含みの単一フォルダ」としては扱えない。 + // これは Sakura の仕様(既知制限)。 + // + // 代わりに、セミコロンで区切った 2 つの実在フォルダを検証する。 + auto dir1 = m_testDir / L"folderA"; + auto dir2 = m_testDir / L"folderB"; + std::filesystem::create_directories(dir1); + std::filesystem::create_directories(dir2); + + dialog::ModalDialogCloser closer([](HWND hWnd) { + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + }); + CDlgGrep dlg; + dlg.m_strText = L"foo"; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + std::wstring folders = dir1.wstring() + L";" + dir2.wstring(); + wcscpy_s(dlg.m_szFolder, folders.c_str()); + + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(1, rc); +} + +/*! + * @brief 複数絶対パス指定時の挙動 (DG-25) + * @remark セミコロン区切りで各々が有効な絶対パスである場合、2つのフォルダとしてそのまま保持(または解決)することを確認する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_MultipleFolders_AllResolved) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + }); + auto sub1 = m_testDir / L"sub1"; + auto sub2 = m_testDir / L"sub2"; + std::filesystem::create_directories(sub1); + std::filesystem::create_directories(sub2); + const std::wstring folders = sub1.wstring() + L";" + sub2.wstring(); + + CDlgGrep dlg; + dlg.m_strText = L"foo"; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szFolder, folders.c_str()); + + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(1, rc); + // 正規化の詳細は実装依存のため、成功して戻ることのみ確認する +} + +/*! + * @brief 通常検索時の履歴追加 (DG-26) + * @remark 「自テキスト検索」ではない場合、実行によって検索キーワード等を共有データの履歴に記録することを確認する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_HistoryRecordedExceptFromThisText) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + }); + CDlgGrep dlg; + dlg.m_strText = L"history_test"; + wcscpy_s(dlg.m_szFile, L"*.h"); + wcscpy_s(dlg.m_szFolder, m_testDir.c_str()); + dlg.m_bFromThisText = FALSE; + + const auto hInstance = ::GetModuleHandleW(nullptr); + dlg.DoModal(hInstance, nullptr, nullptr); + + auto* share = GetDllShareDataPtr(); + EXPECT_STREQ(L"history_test", share->m_sSearchKeywords.m_aSearchKeys[0]); +} + +/*! + * @brief 自テキスト検索時の履歴非汚染 (DG-27) + * @remark 「自テキスト検索」モードでの実行時は、一時的な検索であるため共有データの履歴を汚染しないことを確認する。 + */ +TEST_F(CDlgGrepGuiTest, DoModalOK_FromThisText_DoesNotPolluteHistory) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDOK, BN_CLICKED), 0); + }); + + auto* share = GetDllShareDataPtr(); + // 履歴の先頭に別の値を入れておく + wcscpy_s(share->m_sSearchKeywords.m_aSearchKeys[0], _MAX_PATH, L"prev_key"); + + CDlgGrep dlg; + dlg.m_strText = L"pollute_test"; + wcscpy_s(dlg.m_szFile, L"*.cpp"); + wcscpy_s(dlg.m_szFolder, m_testDir.c_str()); + dlg.m_bFromThisText = TRUE; + + const auto hInstance = ::GetModuleHandleW(nullptr); + dlg.DoModal(hInstance, nullptr, nullptr); + + // 履歴が更新されていないことを確認 + EXPECT_STREQ(L"prev_key", share->m_sSearchKeywords.m_aSearchKeys[0]); +} + +/*! + * @brief 除外ファイル正規表現チェックボックスのトグル処理 (DG-28) + * @remark IDC_CHK_EXCLUDE_FILE_REGEXP の ON/OFF をそれぞれで OnBnClicked 内の + * 既定パターン入れ替え分岐(ワイルドカード→正規表現)を実行させること確認する。 + * アサーションは、クラッシュせず IDCANCEL で復帰すること(網羅目的)。 + */ +TEST_F(CDlgGrepGuiTest, DoModal_ToggleExcludeFileRegexp_SwapsDefaultPattern) +{ + dialog::ModalDialogCloser closer([](HWND hWnd) { + // 除外ファイル欄をワイルドカード既定値に固定してから ON にする + // → bRegexp=true 分岐(pszOther=WILDCARD と一致 → REGEX へ SetText) + ::SetDlgItemTextW(hWnd, IDC_COMBO_EXCLUDE_FILE, DEFAULT_EXCLUDE_FILE_PATTERN_WILDCARD); + ::CheckDlgButton(hWnd, IDC_CHK_EXCLUDE_FILE_REGEXP, BST_CHECKED); + ::SendMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDC_CHK_EXCLUDE_FILE_REGEXP, BN_CLICKED), 0); + + // 続けて OFF にする + // → bRegexp=false 分岐(pszOther=REGEX と一致 → WILDCARD へ SetText) + ::CheckDlgButton(hWnd, IDC_CHK_EXCLUDE_FILE_REGEXP, BST_UNCHECKED); + ::SendMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDC_CHK_EXCLUDE_FILE_REGEXP, BN_CLICKED), 0); + + ::PostMessageW(hWnd, WM_COMMAND, MAKEWPARAM(IDCANCEL, BN_CLICKED), 0); + }); + + CDlgGrep dlg; + const auto hInstance = ::GetModuleHandleW(nullptr); + const int rc = dlg.DoModal(hInstance, nullptr, nullptr); + EXPECT_EQ(0, rc); // キャンセルで 0 +} + diff --git a/src/test/cpp/tests1/test-cgrepagent-flow.cpp b/src/test/cpp/tests1/test-cgrepagent-flow.cpp new file mode 100644 index 0000000000..f88ef798ec --- /dev/null +++ b/src/test/cpp/tests1/test-cgrepagent-flow.cpp @@ -0,0 +1,426 @@ +/*! @file */ +/* + Copyright (C) 2026, Sakura Editor Organization + + SPDX-License-Identifier: Zlib +*/ +// ==================================================================================== +// Phase 3-B: CGrepAgent Unit Tests for Result Formatting & Flow Helpers (PR #2459) +// ==================================================================================== + +#include "pch.h" +#include +#include +#include +#include + +#include "agent/CGrepAgent.h" +#include "env/ShareDataTestSuite.hpp" +#include "window/EditorTestSuite.hpp" + +// ----- FormatGrepResultLine ----- + +namespace { +// FormatGrepResultLine は内部で GetDllShareData() を参照するため ShareData を初期化するフィクスチャを使用する +struct CGrepAgentTest + : public ::testing::Test + , public env::ShareDataTestSuite +{ + // BuildGrepHeader/FormatGrepResultLine は内部で共有データを参照するため、ここで初期化する。 + static void SetUpTestSuite() { SetUpShareData(); } + static void TearDownTestSuite() { TearDownShareData(); } +}; +} // namespace + +/*! + * @brief 検索結果行のフォーマット(Normalスタイル) (GA-01) + * @remark パス、行番号、桁番号、文字コード、およびヒットした行全体を指定の書式で出力することを確認する。 + */ +TEST_F(CGrepAgentTest, FormatGrepResultLine_NormalStyle1_ProducesPathLineColContent) +{ + CNativeW cmem; + SGrepOption opt; + opt.nGrepOutputStyle = 1; + opt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + cmem, L"C:\\test.cpp", L" [UTF-8]", + SGrepMatchInfo{ 42, 5, L" return true;\n", 15, 1, L"true", 4 }, opt + ); + EXPECT_STREQ(L"C:\\test.cpp(42,5) [UTF-8]: return true;\r\n", cmem.GetStringPtr()); // 正常形式の出力 +} + +/*! + * @brief 検索結果行のフォーマット(WZ風スタイル) (GA-02) + * @remark ファイル名出力がグループ化するスタイルにおいて、ファイルパス情報を含めずに行番号・桁情報から出力することを確認する。 + */ +TEST_F(CGrepAgentTest, FormatGrepResultLine_WzStyle2_FileGrouped) +{ + CNativeW cmem; + SGrepOption opt; + opt.nGrepOutputStyle = 2; + opt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + cmem, L"C:\\test.cpp", L"", + SGrepMatchInfo{ 42, 5, L" return true;\n", 15, 1, L"true", 4 }, opt + ); + std::wstring s(cmem.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"42")); // 行番号 + EXPECT_NE(std::wstring::npos, s.find(L"5")); // 桁番号 + EXPECT_NE(std::wstring::npos, s.find(L"return true;")); // 行内容 + // WZ風の場合、行頭に "・" が付く + EXPECT_EQ(L'・', s[0]); // グループ先頭マーカー +} + +/*! + * @brief 検索結果行のフォーマット(結果のみスタイル) (GA-03) + * @remark パスや行番号情報を出力せず、マッチした行のコンテンツのみを出力することを確認する。 + */ +TEST_F(CGrepAgentTest, FormatGrepResultLine_ResultOnlyStyle3_NoPath) +{ + CNativeW cmem; + SGrepOption opt; + opt.nGrepOutputStyle = 3; + opt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + cmem, L"C:\\test.cpp", L"", + SGrepMatchInfo{ 42, 5, L" return true;\n", 15, 1, L"true", 4 }, opt + ); + EXPECT_STREQ(L" return true;\r\n", cmem.GetStringPtr()); +} + +/*! + * @brief 検索結果行のフォーマット(該当部分のみ出力) (GA-04) + * @remark 行全体ではなく、マッチした部分の文字列だけを出力することを確認する。 + * @note nGrepOutputLineType=0 は内部で GetDllShareData() を参照するため ShareData 初期化が必要。 + */ +TEST_F(CGrepAgentTest, FormatGrepResultLine_OutputLineType0_OnlyMatchPart) +{ + CNativeW cmem; + SGrepOption opt; + opt.nGrepOutputStyle = 3; + opt.nGrepOutputLineType = 0; // 該当部分のみ + + CGrepAgent::FormatGrepResultLine( + cmem, L"C:\\test.cpp", L"", + SGrepMatchInfo{ 42, 5, L" return true;\n", 15, 1, L"true", 4 }, opt + ); + EXPECT_STREQ(L"true\r\n", cmem.GetStringPtr()); +} + +/*! + * @brief 検索結果行のフォーマット(行全体出力) (GA-05) + * @remark マッチした文字列を含む行全体を出力することを確認する。 + */ +TEST_F(CGrepAgentTest, FormatGrepResultLine_OutputLineType1_FullLine) +{ + CNativeW cmem; + SGrepOption opt; + opt.nGrepOutputStyle = 3; + opt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + cmem, L"C:\\test.cpp", L"", + SGrepMatchInfo{ 42, 5, L" return true;\n", 15, 1, L"true", 4 }, opt + ); + EXPECT_STREQ(L" return true;\r\n", cmem.GetStringPtr()); +} + +/*! + * @brief 検索結果行のフォーマット(否該当行出力) (GA-06) + * @remark nGrepOutputLineType=2 でも FormatGrepResultLine 自体は行全体を出力する。 + * 非該当行の判定・フィルタリングは呼び出し側 (DoGrepFile) の責任である。 + */ +TEST_F(CGrepAgentTest, FormatGrepResultLine_OutputLineType2_NegativeLine) +{ + CNativeW cmem; + SGrepOption opt; + opt.nGrepOutputStyle = 3; // 結果のみ(パス情報を除外して検証を簡潔化) + opt.nGrepOutputLineType = 2; // 否該当行 + + CGrepAgent::FormatGrepResultLine( + cmem, L"C:\\test.cpp", L"", + SGrepMatchInfo{ 42, 1, L"no match here", 13, 0, L"", 0 }, opt + ); + EXPECT_STREQ(L"no match here\r\n", cmem.GetStringPtr()); +} + +// ----- BuildGrepHeader / BuildGrepFooter ----- + +/*! + * @brief 検索ヘッダ生成(基本形) (GA-07) + * @remark 検索キーワード、対象ファイルパターン、検索対象フォルダーの情報がヘッダに含まれることを確認する。 + */ +TEST(CGrepAgent, BuildGrepHeader_BasicShape) +{ + SSearchOption sOpt; + SGrepOption gOpt; + + CNativeW header = CGrepAgent::BuildGrepHeader(L"foo", L"*.cpp", L"C:\\src", sOpt, gOpt); + std::wstring s(header.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"foo")); + EXPECT_NE(std::wstring::npos, s.find(L"*.cpp")); + EXPECT_NE(std::wstring::npos, s.find(L"C:\\src")); +} + +/*! + * @brief 検索ヘッダ生成(正規表現フラグON) (GA-08) + * @remark 正規表現オプションが有効な場合、ヘッダ文字列に「正規表現」のマーカーが含まれることを確認する。 + */ +TEST(CGrepAgent, BuildGrepHeader_WithRegexOption_IncludesMarker) +{ + SSearchOption sOpt; + sOpt.bRegularExp = true; + SGrepOption gOpt; + + CNativeW header = CGrepAgent::BuildGrepHeader(L"foo", L"*.cpp", L"C:\\src", sOpt, gOpt); + std::wstring s(header.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"正規表現")); +} + +/*! + * @brief 検索ヘッダ生成(大文字小文字区別フラグON) (GA-09) + * @remark 大文字小文字の区別オプションが有効な場合、ヘッダ文字列に「大文字小文字」のマーカーが含まれることを確認する。 + */ +TEST(CGrepAgent, BuildGrepHeader_WithCaseSensitive_IncludesMarker) +{ + SSearchOption sOpt; + sOpt.bLoHiCase = true; + SGrepOption gOpt; + + CNativeW header = CGrepAgent::BuildGrepHeader(L"foo", L"*.cpp", L"C:\\src", sOpt, gOpt); + std::wstring s(header.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"大文字小文字")); +} + +/*! + * @brief 検索フッタ生成(0件ヒット時) (GA-10) + * @remark 検索結果が0件の場合、件数「0」を含むフッタ文字列を生成することを確認する。 + * フッタの具体的な書式(「0 件」「0件」等)は実装依存のため、数字の存在のみを検証する。 + */ +TEST(CGrepAgent, BuildGrepFooter_ZeroHits_HasZeroMessage) +{ + CNativeW footer = CGrepAgent::BuildGrepFooter(0, false); + std::wstring s(footer.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"0")); +} + +/*! + * @brief 検索フッタ生成(ヒットあり・通常Grep) (GA-11) + * @remark 通常Grepで件数が含まれること、置換Grepで件数が含まれることをそれぞれ確認する。 + */ +TEST(CGrepAgent, BuildGrepFooter_PositiveHits_HasCount) +{ + { + CNativeW footer = CGrepAgent::BuildGrepFooter(42, false); + std::wstring s(footer.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"42")); + } + { + CNativeW footer = CGrepAgent::BuildGrepFooter(42, true); + std::wstring s(footer.GetStringPtr()); + EXPECT_NE(std::wstring::npos, s.find(L"42")); + } +} + +// ----- CreateFolders / ChopYen ----- + +/*! + * @brief CreateFolders: セミコロン区切りの分割 (GA-12) + * @remark 複数のフォルダーパスがセミコロンで区切られている場合、それぞれを独立したパス要素として抽出することを確認する。 + */ +TEST(CGrepAgent, CreateFolders_SemicolonSeparated_SplitsCorrectly) +{ + std::vector v; + CGrepAgent::CreateFolders(L"C:\\a;C:\\b", v); + ASSERT_EQ(2, v.size()); + EXPECT_STREQ(L"C:\\a", v[0].c_str()); + EXPECT_STREQ(L"C:\\b", v[1].c_str()); +} + +/*! + * @brief CreateFolders: ダブルクォートで囲まれたセミコロン (GA-13) + * @remark パスがダブルクォートで囲まれている場合、内部のセミコロンは区切り文字として扱われず、 + * ダブルクォートが除去された1つのパスとなることを確認する。 + */ +TEST(CGrepAgent, CreateFolders_QuotedSemicolon_PreservesAsOne) +{ + std::vector v; + CGrepAgent::CreateFolders(L"\"C:\\a;b\"", v); + ASSERT_EQ(1, v.size()); + EXPECT_STREQ(L"C:\\a;b", v[0].c_str()); +} + +/*! + * @brief CreateFolders: 長いファイル名の解決 (GA-14) + * @remark 8.3形式の短いパスが指定された場合、`GetLongFileName` で元の長いパスに解決してリストへ追加することを確認する。 + * @note 8.3 形式生成が無効化されたボリューム(NTFS Win10+ 既定など)では + * GetShortPathName が失敗するため GTEST_SKIP でスキップする。 + */ +TEST(CGrepAgent, CreateFolders_LongFileName_Resolved) +{ + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + + const std::wstring longPath = std::wstring(tempDir) + L"sakura_grep_test_very_long_file_name.tmp"; + + HANDLE hFile = ::CreateFileW(longPath.c_str(), GENERIC_WRITE, 0, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hFile) + << "一時ファイル作成失敗:テスト前提条件未達。GetLastError=" << ::GetLastError(); + ::CloseHandle(hFile); + + auto fileGuard = std::unique_ptr>( + reinterpret_cast(1), + [&longPath](void*) { ::DeleteFileW(longPath.c_str()); } + ); + + WCHAR shortPath[MAX_PATH]; + if (0 == ::GetShortPathNameW(longPath.c_str(), shortPath, MAX_PATH)) { + GTEST_SKIP() << "8.3 形式が無効なボリュームのためスキップ。GetLastError=" + << ::GetLastError(); + } + + std::vector v; + CGrepAgent::CreateFolders(shortPath, v); + ASSERT_EQ(1u, v.size()); + EXPECT_GT(v[0].length(), 0u); +} + +/*! + * @brief ChopYen: 末尾の円記号削除 (GA-15) + * @remark パスの末尾に円記号(バックスラッシュ)がある場合、それを除去することを確認する。 + */ +TEST(CGrepAgent, ChopYen_LastBackslashRemoved) +{ + EXPECT_STREQ(L"C:\\foo", CGrepAgent::ChopYen(L"C:\\foo\\").c_str()); +} + +/*! + * @brief ChopYen: 末尾に円記号がない場合 (GA-16) + * @remark パスの末尾に円記号がない場合は何も変更しないことを確認する。 + */ +TEST(CGrepAgent, ChopYen_NoTrailingYen_Unchanged) +{ + EXPECT_STREQ(L"C:\\foo", CGrepAgent::ChopYen(L"C:\\foo").c_str()); +} + +/*! + * @brief ChopYen: ルートパスの場合 (GA-17) + * @remark ドライブのルート(C:\ など)の場合でも、仕様として末尾の円記号を除去して(C: となる)ことを確認する。 + */ +TEST(CGrepAgent, ChopYen_RootPath_AlsoTrimmed) +{ + EXPECT_STREQ(L"C:", CGrepAgent::ChopYen(L"C:\\").c_str()); +} + +// ----- AddTail / OnBeforeClose ----- + +namespace { + +/*! + * @brief Windows 標準ハンドル(STD_OUTPUT_HANDLE 等)の一時差し替えと + * スコープ脱出時の確実な復帰を保証する RAII ガード。 + * + * SetStdHandle を直接呼ぶテストでは、例外発生時に元のハンドルへ復帰できず + * テストプロセスの stdout が破壊されたまま後続テストへ波及するリスクがある。 + * 本ガードは構築時に差し替え、デストラクタで必ず元に戻す。 + */ +class StdHandleGuard { +public: + StdHandleGuard(DWORD id, HANDLE replacement) + : m_id(id), m_saved(::GetStdHandle(id)) + { + ::SetStdHandle(m_id, replacement); + } + ~StdHandleGuard() + { + ::SetStdHandle(m_id, m_saved); + } + StdHandleGuard(const StdHandleGuard&) = delete; + StdHandleGuard& operator=(const StdHandleGuard&) = delete; + +private: + DWORD m_id; + HANDLE m_saved; +}; + +struct GrepAgentFlowTest + : public ::testing::Test + , public window::EditorTestSuite +{ + static void SetUpTestSuite() { SetUpEditor(); } + static void TearDownTestSuite() { TearDownEditor(); } +}; + +} // namespace + +/*! + * @brief AddTail: 標準出力への書き込み (GA-18) + * @remark bAddStdout が true に指定された場合、エディタへの挿入ではなく標準出力(stdout)へエンコード変換した結果を書き出すことを確認する。 + */ +TEST_F(GrepAgentFlowTest, AddTail_StdoutMode_WritesToStdout) +{ + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_stdout_test.txt"; + + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) + << "一時ファイル作成失敗。テスト前提条件未達。"; + + struct HandleDeleter { + void operator()(HANDLE h) const { + if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); + } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + { + // stdout 差し替えの RAII(このスコープを抜けると必ず元に戻る) + StdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CGrepAgent agent; + CNativeW msg(L"HelloGrepTest"); + + // SetUpEditor はフィクスチャ (GrepAgentFlowTest::SetUpTestSuite) で初期化済み + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + agent.AddTail(pView, msg, true); + } // ← stdout 復帰 + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[128] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + + EXPECT_TRUE(strstr(buf, "HelloGrepTest") != nullptr); + + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); +} + +/*! + * @brief OnBeforeClose: Grep実行中の割り込み (GA-19) + * @remark Grep処理が実行中(m_bGrepRunningがtrue)の場合、クローズ要求に対して割り込み(CALLBACK_INTERRUPT)を返すことを確認する。 + */ +TEST_F(GrepAgentFlowTest, OnBeforeClose_DuringGrepReturnsInterrupt) +{ + CGrepAgent agent; + agent.m_bGrepRunning = true; + EXPECT_EQ(CALLBACK_INTERRUPT, agent.OnBeforeClose()); +} + +/*! + * @brief OnBeforeClose: Grep非実行時の継続 (GA-20) + * @remark Grep処理が実行中でない場合、クローズ要求に対して継続(CALLBACK_CONTINUE)を返すことを確認する。 + */ +TEST_F(GrepAgentFlowTest, OnBeforeClose_NotRunningReturnsContinue) +{ + CGrepAgent agent; + agent.m_bGrepRunning = false; + EXPECT_EQ(CALLBACK_CONTINUE, agent.OnBeforeClose()); +} diff --git a/src/test/cpp/tests1/test-grep-irregular.cpp b/src/test/cpp/tests1/test-grep-irregular.cpp new file mode 100644 index 0000000000..ff5e5f8646 --- /dev/null +++ b/src/test/cpp/tests1/test-grep-irregular.cpp @@ -0,0 +1,899 @@ +/*! @file */ +/* + Copyright (C) 2026, Sakura Editor Organization + + SPDX-License-Identifier: Zlib +*/ +// ====================================================================== +// Phase 4: Grep Irregular and Boundary Unit Tests (PR #2459) +// ====================================================================== + +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "agent/CGrepAgent.h" +#include "agent/CSearchAgent.h" +#include "charset/CCodeFactory.h" +#include "charset/CCodeMediator.h" +#include "extmodule/CBregexp.h" +#include "grep/CGrepEnumKeys.h" +#include "_main/CCommandLine.h" +#include "window/EditorTestSuite.hpp" +#include "util/file.h" + +namespace { + +// このファイル内だけで使う補助関数とテスト用 RAII クラスをまとめる。 +// いずれも CGrepAgent や各テストクラスのメソッドではなく、テスト専用の自由関数/ヘルパー。 + +// ============================================================================= +// Helper: GrepTempDir (reused from Phase 2/3 infra) +// ============================================================================= +// 各テストで使う一時ディレクトリを RAII で生成・削除し、失敗時の後始末を安定させる。 +class GrepTempDir +{ +public: + explicit GrepTempDir(std::wstring_view prefix = L"grp_irr") + { + auto candidate = GetTempFilePath(prefix); + std::filesystem::remove(candidate); + std::filesystem::create_directories(candidate); + m_root = candidate; + } + + ~GrepTempDir() + { + std::error_code ec; + std::filesystem::remove_all(m_root, ec); + } + + const std::filesystem::path& Root() const noexcept { return m_root; } + + // 一時ルート配下の相対パスを組み立てる。 + std::filesystem::path Sub(std::wstring_view relative) const + { + return m_root / std::filesystem::path(relative); + } + + // テスト用の一時ルート配下に、必要なら親ディレクトリごと作る。 + void EnsureDir(std::wstring_view relative) const + { + std::filesystem::create_directories(Sub(relative)); + } + + // 指定した文字コードでテキストファイルを書き出す。 + std::filesystem::path WriteEncodedTextFile( + std::wstring_view relative, + ECodeType codeType, + std::wstring_view text, + bool withBom = false) const + { + const auto path = Sub(relative); + std::filesystem::create_directories(path.parent_path()); + + const auto encoded = CCodeFactory::ConvertToCode(codeType, std::wstring(text)); + std::ofstream os(path, std::ios::binary | std::ios::trunc); + if (withBom) { + switch (codeType) { + case CODE_UTF8: os.write("\xEF\xBB\xBF", 3); break; + case CODE_UNICODE: os.write("\xFF\xFE", 2); break; + case CODE_UNICODEBE: os.write("\xFE\xFF", 2); break; + default: break; + } + } + os.write(encoded.destination.data(), static_cast(encoded.destination.size())); + return path; + } + + // 生バイト列をそのままファイルに書き出す。 + std::filesystem::path WriteRawBytes( + std::wstring_view relative, std::string_view bytes) const + { + const auto path = Sub(relative); + std::filesystem::create_directories(path.parent_path()); + std::ofstream os(path, std::ios::binary | std::ios::trunc); + os.write(bytes.data(), static_cast(bytes.size())); + return path; + } + +private: + std::filesystem::path m_root; +}; + +SSearchOption MakeSearchOption(bool regex, bool caseSensitive, bool wordOnly = false) +{ + // 各ケースの比較条件を明示するため、必要なフラグだけを都度組み立てる自由関数。 + SSearchOption opt; + opt.Reset(); + opt.bRegularExp = regex; + opt.bLoHiCase = caseSensitive; + opt.bWordOnly = wordOnly; + return opt; +} + +// DoGrepFileWorker の戻り値に意味を持たせる定数 +constexpr int GREP_RESULT_NO_HIT = 0; // ヒットなし(ファイル読み取り不可含む) +constexpr int GREP_RESULT_CANCELLED = -1; // キャンセルまたはエラー + +SGrepOption MakeGrepOption(ECodeType charSet = CODE_AUTODETECT) +{ + // 不要な UI 要素の影響を避けるため、Grep オプションは最小構成に固定する自由関数。 + SGrepOption gopt; + gopt.nGrepCharSet = charSet; + gopt.nGrepOutputStyle = 1; + gopt.nGrepOutputLineType = 1; + gopt.bGrepHeader = false; + return gopt; +} + +int RunGrepFileWorker( + CGrepAgent& agent, + const std::filesystem::path& path, + std::wstring_view key, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption, + std::atomic& cancel) +{ + // 実ファイルを 1 件だけ検索するためのタスクを構築し、DoGrepFileWorker を直接叩く自由関数。 + const std::wstring keyStr(key); + const std::wstring fullPath = path.wstring(); + const std::wstring fileName = path.filename().wstring(); + const std::wstring baseFolder = path.parent_path().wstring(); + + SGrepFileTask task; + task.fullPath = fullPath; + task.fileName = fileName; + task.baseFolder = baseFolder; + task.folder = baseFolder; + task.relPath = fileName; + + CBregexp regexp; + if (sSearchOption.bRegularExp) { + if (!InitRegexp(nullptr, regexp, false)) return -1; + const DWORD flags = sSearchOption.bLoHiCase ? CBregexp::optCaseSensitive : 0; + if (!regexp.Compile(keyStr.c_str(), flags)) return -1; + } + + CSearchStringPattern pattern; + if (!pattern.SetPattern(nullptr, keyStr.c_str(), keyStr.size(), + sSearchOption, sSearchOption.bRegularExp ? ®exp : nullptr)) { + return -1; + } + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + cmemMessage.AllocStringBuffer(4000); + cUnicodeBuffer.AllocStringBuffer(4000); + + return agent.DoGrepFileWorker( + SGrepSearchParams{ keyStr.c_str(), sSearchOption, sGrepOption }, + task, sSearchOption.bRegularExp ? ®exp : nullptr, pattern, + cancel, + cmemMessage, cUnicodeBuffer); +} + +int RunGrepFileWorker( + CGrepAgent& agent, const std::filesystem::path& path, std::wstring_view key, + const SSearchOption& sSearchOption, const SGrepOption& sGrepOption) +{ + // キャンセルなしのテスト用薄いラッパ。これもメソッドではなく補助関数。 + std::atomic cancel{ false }; + return RunGrepFileWorker(agent, path, key, sSearchOption, sGrepOption, cancel); +} + +struct GrepIrregularTest : public ::testing::Test, public window::EditorTestSuite { + // 各ケースを独立させるため、毎回一時ディレクトリを作り直す。 + static void SetUpTestSuite() { SetUpEditor(); } + static void TearDownTestSuite() { TearDownEditor(); } + void SetUp() override { m_temp = std::make_unique(); } + void TearDown() override { m_temp.reset(); } + std::unique_ptr m_temp; +}; + +} // namespace + +// ============================================================================= +// CGrepEnumKeys 境界 (IRR-01 ~ 08) +// ============================================================================= + +/*! + * @brief 空文字列のキー解析 (IRR-01) + * @remark パターンが空文字列の場合、デフォルトの検索ファイルパターン(*.*)を適用することを確認する。 + */ +TEST(Irregular, SetFileKeys_EmptyString_DefaultsApplied) { + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"")); // 空文字列でも受理する + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // 検索対象は既定値で 1 件 + EXPECT_STREQ(L"*.*", keys.m_vecSearchFileKeys[0].c_str()); // 既定の検索対象 +} + +/*! + * @brief 空白のみの文字列のキー解析 (IRR-02) + * @remark パターンが空白文字のみの場合も、デフォルトの検索ファイルパターン(*.*)を適用することを確認する。 + */ +TEST(Irregular, SetFileKeys_OnlyWhitespace_DefaultsApplied) { + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L" ")); // 空白のみでも受理する + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // 検索対象は既定値で 1 件 + EXPECT_STREQ(L"*.*", keys.m_vecSearchFileKeys[0].c_str()); // 既定の検索対象 +} + +/*! + * @brief 除外パターンのみ指定された場合のキー解析 (IRR-03) + * @remark 検索ファイルパターンがなく除外パターンのみが指定された場合、検索対象として「*.*」を補完することを確認する。 + */ +TEST(Irregular, SetFileKeys_OnlyExcludePatterns_DefaultSearchApplied) { + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"!*.obj;#build", /*bExcludeFileRegex=*/true)); // 解析は成功する + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // 検索対象は補完されて 1 件 + EXPECT_STREQ(L"*.*", keys.m_vecSearchFileKeys[0].c_str()); // 既定の検索対象 + ASSERT_EQ(1, keys.m_vecExceptFileRegexPatterns.size()); // 除外正規表現は 1 件 + EXPECT_STREQ(L"*.obj", keys.m_vecExceptFileRegexPatterns[0].c_str()); // 除外ファイルの中身 + ASSERT_EQ(1, keys.m_vecExceptFolderKeys.size()); // 除外フォルダーは 1 件 + EXPECT_STREQ(L"build", keys.m_vecExceptFolderKeys[0].c_str()); // 除外フォルダー名 +} + +/*! + * @brief 超長大パターンのパース (IRR-04) + * @remark 4096文字という極端に長いパターンが与えられても、バッファオーバーラン等を起こさずに正しく保持することを確認する。 + */ +TEST(Irregular, SetFileKeys_VeryLongPattern_4096Chars) { + CGrepEnumKeys keys; + std::wstring longPattern(4096, L'A'); + EXPECT_EQ(0, keys.SetFileKeys(longPattern.c_str())); // 長大入力でも解析は成功する + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // 1 件のまま + EXPECT_EQ(4096u, keys.m_vecSearchFileKeys[0].size()); // 長さをそのまま保持する +} + +/*! + * @brief 重複パターンの除外 (IRR-05) + * @remark 同一のパターンが多数指定された場合、重複を排除して内部リストに単一の要素として保持することを確認する。 + */ +TEST(Irregular, SetFileKeys_ManyDuplicates_DeduplicatedTo1) { + CGrepEnumKeys keys; + std::wstring pattern; + for (int i = 0; i < 100; ++i) pattern += L"*.cpp;"; + EXPECT_EQ(0, keys.SetFileKeys(pattern.c_str())); // 重複があっても解析は成功する + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // 重複を 1 件にまとめる + EXPECT_STREQ(L"*.cpp", keys.m_vecSearchFileKeys[0].c_str()); // 残る要素は *.cpp +} + +/*! + * @brief ヌルバイトを含むパターンの挙動 (IRR-06) + * @remark パターンの途中にヌル文字が含まれる場合、そこで文字列が終端したとみなして処理することを確認する。 + */ +TEST(Irregular, SetFileKeys_NullByteInMiddle_TruncatesAtNull) { + CGrepEnumKeys keys; + std::wstring pattern(L"*.cpp\0*.h", 11); + EXPECT_EQ(0, keys.SetFileKeys(pattern.c_str())); // c_str() なので途中の NUL で切れる + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // NUL 以降は見ない + EXPECT_STREQ(L"*.cpp", keys.m_vecSearchFileKeys[0].c_str()); // 先頭側だけ残る +} + +/*! + * @brief 不正な正規表現の除外パターン登録 (IRR-07) + * @remark パース処理は通るが、後の正規表現コンパイル処理にて適切にエラー(false)として扱われることを確認する。 + */ +TEST(Irregular, SetFileKeys_BangPrefixWithInvalidRegex_RegisteredButFailsLater) { + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"!(invalid(regex", /*bExcludeFileRegex=*/true)); // ひとまず登録は受理する + ASSERT_EQ(1, keys.m_vecExceptFileRegexPatterns.size()); // 除外正規表現は 1 件 + // InitRegexp は DLLSHAREDATA 必須のため TEST() 内では呼ばない。 + // コンパイル失敗の検証は GrepIrregularTest フィクスチャ側で行う。 + EXPECT_STREQ(L"(invalid(regex", keys.m_vecExceptFileRegexPatterns[0].c_str()); +} + +/*! + * @brief 不正な除外正規表現パターンのコンパイル失敗確認 (IRR-07b) + * @remark ! プレフィックス付きの不正なパターンは登録されるが、bregonig でのコンパイルは失敗することを確認する。 + */ +TEST_F(GrepIrregularTest, Regex_InvalidExcludePatternFailsToCompile) { + CBregexp regexp; + if (!InitRegexp(nullptr, regexp, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + EXPECT_FALSE(regexp.Compile(L"(invalid(regex", 0)); +} + +/*! + * @brief パス途中にドライブレターを含むパターンの扱い (IRR-08) + * @remark C:\ などの絶対パス表現が途中に含まれる場合でも、先頭の形式に基づいて相対パス扱いとして登録する仕様を確認する。 + */ +TEST(Irregular, SetFileKeys_PathWithDriveLetterInMiddle_TreatedAsRelative) { + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"foo\\C:\\bar")); // 解析は成功する + // 先頭が絶対パス形でないため、相対パス扱いのまま保持する。 + ASSERT_EQ(1, keys.m_vecSearchFileKeys.size()); // 1 要素のまま + EXPECT_STREQ(L"foo\\C:\\bar", keys.m_vecSearchFileKeys[0].c_str()); // 文字列をそのまま保持 +} + +// ============================================================================= +// CCommandLine 境界 (IRR-09 ~ 19) +// ============================================================================= + +/*! + * @brief GKEYの空値オプション (IRR-09) + * @remark "-GKEY=" のように値が空で指定された場合、無視する(または空として登録する)ことを確認する。 + */ +TEST(Irregular, CommandLine_GKEY_EmptyValue_Ignored) { + CCommandLine c; + c.ParseCommandLine(L"-GKEY=", false); + EXPECT_EQ(0, c.GetGrepInfoRef().cmGrepKey.GetStringLength()); // 空文字列のまま +} + +/*! + * @brief GREPRの空値オプション (IRR-10) + * @remark "-GREPR=" のように置換文字列が空で指定された場合、置換フラグがONになり空文字列で置換する設定になることを確認する。 + */ +TEST(Irregular, CommandLine_GREPR_EmptyValue_AcceptedAsEmpty) { + CCommandLine c; + c.ParseCommandLine(L"-GREPR=", false); + EXPECT_EQ(0, c.GetGrepInfoRef().cmGrepRep.GetStringLength()); // 置換文字列は空 + EXPECT_TRUE(c.GetGrepInfoRef().bGrepReplace); // 置換モードは有効 +} + +/*! + * @brief GFOLDERに存在しないパスを指定 (IRR-11) + * @remark 実在しないパスを指定した場合でもエラーとならず、そのまま文字列として保持することを確認する。 + */ +TEST(Irregular, CommandLine_GFOLDER_NonExistentPath_StoredAsIs) { + CCommandLine c; + c.ParseCommandLine(L"-GFOLDER=Z:\\does\\not\\exist", false); + EXPECT_STREQ(L"Z:\\does\\not\\exist", c.GetGrepInfoRef().cmGrepFolder.GetStringPtr()); // 文字列をそのまま保持 +} + +/*! + * @brief GCODEに範囲外の数値を指定 (IRR-12) + * @remark 定義された文字コードの範囲外の数値が指定された場合でも、その値をそのまま保持することを確認する。 + */ +TEST(Irregular, CommandLine_GCODE_OutOfRange_StoredAsIs) { + CCommandLine c; + c.ParseCommandLine(L"-GCODE=99999", false); + EXPECT_EQ(99999, c.GetGrepInfoRef().nGrepCharSet); // 範囲外でも値を保持 +} + +/*! + * @brief GCODEに負の数値を指定 (IRR-13) + * @remark 負の数値が指定された場合でも、その値をそのまま保持することを確認する。 + */ +TEST(Irregular, CommandLine_GCODE_Negative_StoredAsIs) { + CCommandLine c; + c.ParseCommandLine(L"-GCODE=-1", false); + EXPECT_EQ(-1, c.GetGrepInfoRef().nGrepCharSet); // 負値でも値を保持 +} + +/*! + * @brief GKEYに制御文字を含む指定 (IRR-14) + * @remark 制御文字が含まれる検索キーワードが指定された場合、それをそのまま正しく保持することを確認する。 + */ +TEST(Irregular, CommandLine_GKEY_WithControlChars_Stored) { + CCommandLine c; + std::wstring cmd(L"-GKEY=foo\x01" L"bar"); + c.ParseCommandLine(cmd.c_str(), false); + EXPECT_STREQ(L"foo\x01" L"bar", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); // 制御文字も保持 +} + +/*! + * @brief GKEYにサロゲートペア文字を含む指定 (IRR-15) + * @remark UTF-16 サロゲートペア(U+1F600)を含むキーワードが正しく保持されることを確認する。 + * コードページ 932 では直接記述できないため \xD83D\xDE00 で指定する。 + */ +TEST(Irregular, CommandLine_GKEY_WithSurrogatePair) { + CCommandLine c; + c.ParseCommandLine(L"-GKEY=foo\xD83D\xDE00" L"bar", false); + EXPECT_STREQ(L"foo\xD83D\xDE00" L"bar", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); // サロゲートペアも保持 +} + +/*! + * @brief 長大なコマンドライン引数のパース (IRR-16) + * @remark Windowsの上限に近い32000文字超の引数が指定されても、クラッシュせずに解析することを確認する。 + */ +// DEBUG実行時に、DEBUG_TRACE は最終的に DebugOutW(バッファサイズ16,000文字)を呼び出すため、 +// -GKEY 以外のオプション(-GREPR、-GFILE、-GFOLDER など)であっても、16,000文字を超える値を設定すると、 +// DebugOutW でバッファあふれを起こし、デバッグブレーク (::DebugBreak()) でクラッシュする。 +// その為、CCommandLine::CheckCommandLine で解析されるすべてのオプション引数に対し文字制限をかけた後に +// ここを復活させる。 +// +// TEST(Irregular, CommandLine_TotalLength_NearWindowsLimit_32767Chars) { +// CCommandLine c; +// std::wstring cmd = L"-GKEY=" + std::wstring(32000, L'A'); +// c.ParseCommandLine(cmd.c_str(), false); +// EXPECT_EQ(32000, c.GetGrepInfoRef().cmGrepKey.GetStringLength()); // 長大入力でも長さが崩れない +// } + + +/*! + * @brief レスポンスファイル経由の Grep 引数解析 (IRR-17) + * @remark ParseCommandLine の第 2 引数を true にしてレスポンスファイルを展開し、 + * -GKEY と -GFILE が正しくパースされることを確認する。 + * レスポンスファイルの区切り文字はスペース(改行ではない)。 + */ +TEST_F(GrepIrregularTest, CommandLine_ResponseFileWithGrepArgs_Parsed) { + auto respPath = m_temp->Sub(L"resp.txt"); + std::ofstream os(respPath, std::ios::trunc); + os << "-GKEY=foo -GFILE=*.cpp"; + os.close(); + + CCommandLine c; + std::wstring cmd = L"-@=\"" + respPath.wstring() + L"\""; + c.ParseCommandLine(cmd.c_str(), true); + EXPECT_STREQ(L"foo", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); + EXPECT_STREQ(L"*.cpp", c.GetGrepInfoRef().cmGrepFile.GetStringPtr()); +} + +/*! + * @brief ダブルダッシュによるオプション解析の停止 (IRR-18) + * @remark "--" 以降はオプションではなくファイル名として扱われることを確認する。 + */ +TEST(Irregular, CommandLine_DoubleDashStopsOptionParsing) { + CCommandLine c; + c.ParseCommandLine(L"-- -GKEY=foo", false); + EXPECT_EQ(0, c.GetGrepInfoRef().cmGrepKey.GetStringLength()); // オプションは設定しない + // GetFileName の挙動は実装依存のため、キーが空であることのみ保証する +} + +/*! + * @brief コロン区切りでのオプション指定 (IRR-19) + * @remark イコールの代わりにコロンを使用("-GKEY:foo")しても同等にパースすることを確認する。 + */ +TEST(Irregular, CommandLine_ColonSeparatorEquivalentToEqual) { + CCommandLine c; + c.ParseCommandLine(L"-GKEY:foo", false); + EXPECT_STREQ(L"foo", c.GetGrepInfoRef().cmGrepKey.GetStringPtr()); // コロン指定も同等 +} + +// ============================================================================= +// 除外ファイルパターンの正規表現コンパイル検証 (IRR-20 ~ IRR-25) +// ============================================================================= + +/*! + * @brief 有効な正規表現パターンがコンパイルに成功する (IRR-20) + * @remark GUI 除外ファイル欄に .*\.obj$ のような正規表現を入力した場合、 + * CBregexp::Compile が成功することを確認する。 + */ +TEST_F(GrepIrregularTest, ExcludeFile_ValidRegex_CompilesSuccessfully) { + CBregexp regexp; + if (!InitRegexp(nullptr, regexp, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + EXPECT_TRUE(regexp.Compile(L".*\\.obj$", 0)); +} + +/*! + * @brief グロブパターンが正規表現コンパイルに失敗する (IRR-21) + * @remark GUI 除外ファイル欄に *.msi のようなグロブを入力した場合、 + * CBregexp::Compile が失敗することを確認する。 + * これは Sakura の「無効な除外正規表現パターン」エラーに相当する。 + */ +TEST_F(GrepIrregularTest, ExcludeFile_GlobPattern_CompileFails) { + CBregexp regexp; + if (!InitRegexp(nullptr, regexp, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + EXPECT_FALSE(regexp.Compile(L"*.msi", 0)); +} + +/*! + * @brief 除外ファイルが空の場合はコンパイル対象なし (IRR-22) + * @remark 除外ファイル欄が空の場合、正規表現コンパイルは行われず、 + * エラーも発生しないことを確認する。 + */ +TEST_F(GrepIrregularTest, ExcludeFile_EmptyPattern_NoCompileNoError) { + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.AddExceptFile(L"")); + EXPECT_EQ(0, keys.m_vecExceptFileKeys.size()); +} + +/*! + * @brief 正規表現とグロブの混在:有効パターンが先、無効パターンが後 (IRR-23) + * @remark 除外ファイルに有効な正規表現と無効なグロブが混在した場合の挙動を確認する。 + * 1 件目(正規表現)はコンパイル成功、2 件目(グロブ)はコンパイル失敗。 + * 「最初のエラーで中断」か「エラーをスキップして続行」かの仕様を凍結する。 + */ +TEST_F(GrepIrregularTest, ExcludeFile_MixedRegexAndGlob_FirstSucceedsSecondFails) { + CBregexp regexp; + if (!InitRegexp(nullptr, regexp, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + + EXPECT_TRUE(regexp.Compile(L".*\\.obj$", 0)); + + CBregexp regexp2; + if (!InitRegexp(nullptr, regexp2, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + EXPECT_FALSE(regexp2.Compile(L"*.msi", 0)); +} + +/*! + * @brief 正規表現とグロブの混在:無効パターンが先、有効パターンが後 (IRR-24) + * @remark IRR-23 と逆順で、順序によって挙動が変わらないことを確認する。 + */ +TEST_F(GrepIrregularTest, ExcludeFile_MixedGlobAndRegex_OrderReversed) { + CBregexp regexp; + if (!InitRegexp(nullptr, regexp, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + + EXPECT_FALSE(regexp.Compile(L"*.msi", 0)); + + CBregexp regexp2; + if (!InitRegexp(nullptr, regexp2, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + EXPECT_TRUE(regexp2.Compile(L".*\\.obj$", 0)); +} + +/*! + * @brief 全件有効な正規表現パターンの一括コンパイル (IRR-25) + * @remark 除外ファイルに複数の有効な正規表現のみを指定した場合、 + * 全件がコンパイルに成功することを確認する。 + */ +TEST_F(GrepIrregularTest, ExcludeFile_AllValidRegex_AllCompileSuccessfully) { + const wchar_t* patterns[] = { + L".*\\.obj$", + L".*\\.exe$", + L".*\\.pdb$", + }; + + for (const auto& pat : patterns) { + CBregexp regexp; + if (!InitRegexp(nullptr, regexp, false)) { + GTEST_SKIP() << "bregonig.dll not available"; + } + EXPECT_TRUE(regexp.Compile(pat, 0)) + << "Failed to compile: " << pat; + } +} + +// ============================================================================= +// ファイル経路境界(DoGrepFileWorker) (IRR-26 ~ 34) +// ============================================================================= + +/*! + * @brief 0バイトファイルの探索 (IRR-26) + * @remark サイズ0のファイルに対して検索を実行してもクラッシュせず、ヒットなし(0)で返ることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_ZeroByteFile_NoHit) { + auto path = m_temp->WriteRawBytes(L"zero.txt", ""); + CGrepAgent agent; + EXPECT_EQ(0, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption())); // 0 バイトならヒットなし +} + +/*! + * @brief BOMのみのファイルの探索 (IRR-27) + * @remark 内容がなくBOMだけが存在するファイルに対して検索を実行してもクラッシュせず、ヒットなしで返ることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_OnlyBomFile_NoHit) { + auto path = m_temp->WriteRawBytes(L"bom.txt", "\xEF\xBB\xBF"); + CGrepAgent agent; + EXPECT_EQ(0, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption(CODE_UTF8))); // BOM だけならヒットなし +} + +/*! + * @brief 不正なUTF-8を含むファイルの探索 (IRR-28) + * @remark ファイル内に不正なUTF-8シーケンスが混入していても、クラッシュせずに正常な部分が検索できることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_InvalidUtf8Sequence_DoesNotCrash) { + auto path = m_temp->WriteRawBytes(L"invalid.txt", "\xEF\xBB\xBF" "abc\xFF\xFE" "def\n"); + CGrepAgent agent; + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"def", MakeSearchOption(false, false), MakeGrepOption(CODE_UTF8))); // 正常部分だけ検索する +} + +/*! + * @brief 末尾に改行がないファイルの探索 (IRR-29) + * @remark ファイルの最後の行が改行で終わっていない場合でも、最後の行から対象文字列が検索できることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_FileWithoutFinalNewline_LastLineSearched) { + auto path = m_temp->WriteRawBytes(L"nonl.txt", "line1\nline2\nfoo"); + CGrepAgent agent; + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption(CODE_SJIS))); // 最終行も検索する +} + +/*! + * @brief 複数種類の改行コードが混在するファイルの探索 (IRR-30) + * @remark CR, LF, CRLF が混在するテキストファイルにおいても、行番号のカウントや検索処理が決定的であることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_MixedLineEndings_LineNumbersConsistent) { + auto path = m_temp->WriteRawBytes(L"mixed.txt", "a\r\nb\nc\rd\r\ne\nfoo"); + CGrepAgent agent; + // foo should be on line 6 + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption(CODE_SJIS))); // 改行種別が混在しても 1 件 +} + +/*! + * @brief 極端に長い1行を含むファイルの探索 (IRR-31) + * @remark 改行のない64KBを超える行が含まれる場合でも、メモリを線形に扱い、正しく対象文字を検出することを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_VeryLongSingleLine_64KChars) { + std::string line(65536, 'a'); + line += "foo"; + auto path = m_temp->WriteRawBytes(L"longline.txt", line); + CGrepAgent agent; + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption(CODE_SJIS))); // 長大 1 行でも見つかる +} + +/*! + * @brief 排他ロックされたファイルの探索 (IRR-32) + * @remark 他のプロセスによって読み取り不可の共有モードでロックされている場合、 + * エラー扱いとしてクラッシュせずに処理を続行できることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_LockedFile_ReturnsError) { + auto path = m_temp->WriteRawBytes(L"locked.txt", "foo\n"); + struct HandleDeleter { void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } }; + std::unique_ptr hFile{ + ::CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL) + }; + ASSERT_NE(INVALID_HANDLE_VALUE, hFile.get()); // 排他ロック用のハンドルを確保する + + CGrepAgent agent; + // DoGrepFileWorker may return 0 or log error if file cannot be opened due to share violation + int hits = RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption()); + EXPECT_TRUE(hits == GREP_RESULT_NO_HIT || hits == GREP_RESULT_CANCELLED) + << "Expected GREP_RESULT_NO_HIT(0) or GREP_RESULT_CANCELLED(-1), got " << hits; +} + +/*! + * @brief 読み取り専用属性ファイルの探索 (IRR-33) + * @remark READONLY属性のファイルであっても、通常通りにテキストを読み込み検索対象となることを確認する。 + */ +TEST_F(GrepIrregularTest, FileWorker_ReadOnlyAttribute_Searched) { + auto path = m_temp->WriteRawBytes(L"readonly.txt", "foo\n"); + ::SetFileAttributesW(path.c_str(), FILE_ATTRIBUTE_READONLY); + // 属性を確実に元に戻す RAII ガード + auto attrGuard = std::unique_ptr>( + reinterpret_cast(1), + [&path](void*) { ::SetFileAttributesW(path.c_str(), FILE_ATTRIBUTE_NORMAL); } + ); + + CGrepAgent agent; + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption())); // 読み取り専用でも検索する +} + +/*! + * @brief 隠しファイル・システムファイルの探索 (IRR-34) + * @remark FileWorker自身はファイルの属性によらず処理できることを検証する。(列挙側でのスキップは別とする仕様凍結) + */ +TEST_F(GrepIrregularTest, FileWorker_HiddenSystemFile_Searched) { + auto path = m_temp->WriteRawBytes(L"hidden.txt", "foo\n"); + ::SetFileAttributesW(path.c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM); + + CGrepAgent agent; + // 仕様凍結: CGrepEnumFilterFiles 等でスキップするかは別の話で、FileWorker自体は検索できる + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption())); // 属性に関係なく検索する + + ::SetFileAttributesW(path.c_str(), FILE_ATTRIBUTE_NORMAL); +} + +// ============================================================================= +// 文字コード自動判定境界 (IRR-35 ~ 38) +// ============================================================================= + +/*! + * @brief 短いASCII文字列に対するフォールバック (IRR-35) + * @remark 特定の文字コード特有のバイト列が含まれない短い文字列の場合、既定の文字コードにフォールバックすることを確認する。 + */ +TEST(Irregular, CodeMediator_ShortFile_FallbackToDefault) { + SEncodingConfig config = {0}; + config.m_eDefaultCodetype = CODE_SJIS; + CCodeMediator mediator(config); + const char* ascii = "short string"; + EXPECT_EQ(CODE_SJIS, mediator.CheckKanjiCode(ascii, 12)); // 既定コードへ戻る +} + +/*! + * @brief SJISの第2バイトがASCII範囲にある場合の判定 (IRR-36) + * @remark 特定のSJIS文字が持つASCII領域の第2バイトで文字コードが誤判定されないことを確認する。 + */ +TEST(Irregular, CodeMediator_SjisSecondByteInAsciiRange_NotMisdetected) { + SEncodingConfig config = {0}; + config.m_eDefaultCodetype = CODE_UTF8; + CCodeMediator mediator(config); + // 噂 (0x92, 0x9A), SJIS + const char* sjis = "\x92\x9A\x92\x9A\x92\x9A"; + EXPECT_EQ(CODE_SJIS, mediator.CheckKanjiCode(sjis, 6)); // SJIS と判定する +} + +/*! + * @brief CESU-8優先フラグによる挙動の切り替え (IRR-37) + * @remark サロゲートペアを2つの3バイトシーケンスで表現するCESU-8特Uriのバイト列に対し、 + * フラグON/OFFで判定が変わることを確認する。 + */ +TEST(Irregular, CodeMediator_PriorCesu8Flag_TogglesDetection) { + SEncodingConfig config = {0}; + config.m_bPriorCesu8 = true; + config.m_eDefaultCodetype = CODE_SJIS; + CCodeMediator mediator_cesu(config); + // Surrogate pair in CESU-8 (6 bytes) + const char* cesu8 = "\xED\xA0\x80\xED\xB0\x80"; + EXPECT_EQ(CODE_CESU8, mediator_cesu.CheckKanjiCode(cesu8, 6)); // 優先フラグONでは CESU-8 と判定する + + config.m_bPriorCesu8 = false; + CCodeMediator mediator_no_cesu(config); + EXPECT_EQ(CODE_CESU8, mediator_no_cesu.CheckKanjiCode(cesu8, 6)); // フラグは禁止ではなく優先: OFF でも CESU-8 固有バイト列は CESU-8 と判定される +} + +/*! + * @brief UTF-32LE BOMがUTF-16として誤判定されないか (IRR-38) + * @remark SakuraエディタはUTF-32をネイティブサポートしていないが、判定処理において + * UTF-16LE (CODE_UNICODE) として誤判定しないことを保証する。 + * (UTF-32LE BOM = FF FE 00 00 は UTF-16LE BOM = FF FE のスーパーセット) + */ +TEST(Irregular, CodeMediator_Utf32LeBom_NotMisdetectedAsUtf16) { + SEncodingConfig config = {0}; + config.m_eDefaultCodetype = CODE_SJIS; + CCodeMediator mediator(config); + const char* utf32le = "\xFF\xFE\x00\x00" "A\x00\x00\x00"; + ECodeType code = mediator.CheckKanjiCode(utf32le, 8); + // 仕様凍結: Sakura は UTF-32 非対応。FF FE 00 00 は先頭 FF FE を UTF-16LE BOM として判定する + EXPECT_EQ(CODE_UNICODE, code); +} + +// ============================================================================= +// 正規表現・同時実行境界 (IRR-39 ~ 46) +// ============================================================================= + +/*! + * @brief 空の正規表現パターンのコンパイル (IRR-39) + * @remark 空文字列の正規表現パターンに対して bregonig エンジンが + * (a) コンパイル失敗を返す、または + * (b) コンパイル成功後 Match() が安全に動作する + * のいずれかを満たすことを確認する(クラッシュ抑止の保証)。 + */ +TEST_F(GrepIrregularTest, Regex_EmptyPattern_CompileFailsOrEmptyMatch) { + CBregexp regexp; + ASSERT_TRUE(InitRegexp(nullptr, regexp, false)); // 正規表現エンジンを初期化する + + const bool bCompiled = regexp.Compile(L"", 0); + if (bCompiled) { + // コンパイル成功時: Match 呼び出しがクラッシュしないことを確認 + EXPECT_NO_FATAL_FAILURE({ // 空パターンでも安全に Match できる + (void)regexp.Match(L"abc", 3, 0); + }); + } + // bCompiled == false でクラッシュしなければ OK(テスト通過) +} + +/*! + * @brief 長大な行に対する .* 正規表現マッチ (IRR-40) + * @remark 行が長大な場合でも ".*" などの広範囲マッチパターンがタイムアウト(約10秒)の + * 保護を受けてハングアップせずに完了することを確認する。 + */ +TEST_F(GrepIrregularTest, Regex_DotStar_DoesNotHang) { + std::string line(10000, 'A'); + auto path = m_temp->WriteRawBytes(L"dotstar.txt", line); + CGrepAgent agent; + int hits = RunGrepFileWorker(agent, path, L".*", MakeSearchOption(true, false), MakeGrepOption()); + EXPECT_GE(hits, 0); // Should finish within 10 seconds. +} + +/*! + * @brief カタストロフィック・バックトラッキングを起こす正規表現への保護 (IRR-41) + * @remark `(a+)+$` のような非効率な正規表現に対しても、エンジン側でハングアップが + * 回避され処理が中断または失敗に終わることを確認する。 + */ +TEST_F(GrepIrregularTest, Regex_CatastrophicBacktracking_TimeoutGuarded) { + std::string line(30, 'a'); + line += "b"; + auto path = m_temp->WriteRawBytes(L"backtrack.txt", line); + + std::atomic result{-999}; + std::thread worker([&]() { + CGrepAgent agent; + result = RunGrepFileWorker( + agent, path, L"(a+)+$", + MakeSearchOption(true, false), MakeGrepOption()); + }); + + bool finished = false; + auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) { + if (result.load() != -999) { finished = true; break; } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + if (!finished) { + worker.detach(); + GTEST_SKIP() + << "bregonig (Onigmo 6.2.0) does not have a timeout mechanism " + "for catastrophic backtracking patterns like (a+)+$. " + "This is a known limitation."; + } else { + worker.join(); + EXPECT_EQ(0, result.load()); + } +} + +/*! + * @brief スレッド開始直後のキャンセル割り込み処理 (IRR-42) + * @remark マルチスレッド環境でワーカー開始直後にキャンセルシグナルが送出された場合、 + * すべての処理が直ちに中断し-1を返すことを確認する。 + */ +TEST_F(GrepIrregularTest, MultiThread_CancelImmediatelyAfterStart) { + std::vector files; + for (int i = 0; i < 50; ++i) { + files.push_back(m_temp->WriteRawBytes(L"f" + std::to_wstring(i) + L".txt", "foo\n")); + } + CGrepAgent agent; + std::atomic cancel{ true }; // Already cancelled! + int hits = RunGrepFileWorker(agent, files[0], L"foo", MakeSearchOption(false, false), MakeGrepOption(), cancel); + EXPECT_EQ(GREP_RESULT_CANCELLED, hits); // 開始直後キャンセルは -1 +} + +/*! + * @brief 走査中にファイルが削除された際の挙動 (IRR-43) + * @remark テストの走査直前にファイルが削除された場合、開けないため該当ファイルは + * 0件扱いとしてクラッシュせずに終了することを確認する。 + */ +TEST_F(GrepIrregularTest, MultiThread_FileDeletedMidScan) { + auto path = m_temp->WriteRawBytes(L"deleted.txt", "foo\n"); + CGrepAgent agent; + std::atomic cancel{ false }; + + // Delete file before starting worker + ::DeleteFileW(path.c_str()); + int hits = RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption(), cancel); + EXPECT_EQ(0, hits); // Can't open, 0 hits +} + +/*! + * @brief 除外正規表現が存在しない場合 (IRR-44) + * @remark 除外正規表現リストが空の場合でも、フィルタリング処理がスキップされて正しく通常の検索結果が返ることを確認する。 + */ +TEST_F(GrepIrregularTest, MultiThread_ZeroExcludeRegexes_Equivalent) { + auto path = m_temp->WriteRawBytes(L"zero_exc.txt", "foo\n"); + CGrepAgent agent; + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"foo", MakeSearchOption(false, false), MakeGrepOption())); // 除外がなくても通常検索は 1 件 +} + +/*! + * @brief 除外正規表現が大量(100個)に存在する場合 (IRR-45) + * @remark 大量の除外正規表現パターンを与えても適切にロードし、件数が正確に一致して適用することを確認する。 + */ +TEST_F(GrepIrregularTest, MultiThread_100ExcludeRegexes_AllApplied) { + CGrepEnumKeys keys; + std::wstring pattern = L"*.txt"; + for (int i = 0; i < 100; ++i) { + pattern += L";!.*exclude" + std::to_wstring(i) + L"\\.txt$"; + } + EXPECT_EQ(0, keys.SetFileKeys(pattern.c_str(), /*bExcludeFileRegex=*/true)); // 100 件の除外正規表現を受理する + EXPECT_EQ(100, keys.m_vecExceptFileRegexPatterns.size()); // 除外正規表現は 100 件 +} + +/*! + * @brief 何度も開始とキャンセルを繰り返す耐久テスト (IRR-46) + * @remark ワーカー処理の開始と即時キャンセルを50回繰り返し、 + * (1) クラッシュ・例外が発生しないこと、 + * (2) 大多数の呼び出しがキャンセル戻り値 -1 を返すこと + * を確認する。 + * @note タイミング依存で開始前に処理が完了する場合があるため + * 50回中45回以上 -1 を返せば PASS とする。 + */ +TEST_F(GrepIrregularTest, MultiThread_RepeatedStartCancel_50Iterations) { + auto path = m_temp->WriteRawBytes(L"repeat.txt", "foo\n"); + CGrepAgent agent; + + int cancelledCount = 0; + for (int i = 0; i < 50; ++i) { + std::atomic cancel{ true }; + const int result = RunGrepFileWorker( + agent, path, L"foo", + MakeSearchOption(false, false), MakeGrepOption(), cancel); + if (result == -1) { + ++cancelledCount; + } + } + EXPECT_GE(cancelledCount, 45) // 50回中45回以上キャンセルなら合格 + << "Expected most iterations to be cancelled, got " << cancelledCount << "/50"; +} diff --git a/src/test/cpp/tests1/test-grep.cpp b/src/test/cpp/tests1/test-grep.cpp new file mode 100644 index 0000000000..4a8977725c --- /dev/null +++ b/src/test/cpp/tests1/test-grep.cpp @@ -0,0 +1,4063 @@ +/*! @file */ +/* + Copyright (C) 2026, Sakura Editor Organization + + SPDX-License-Identifier: Zlib +*/ + +/*! + * @file test-grep.cpp + * @brief Grep 機能の回帰テスト + * + * Issue #1689 のテスト基盤と PR #2459 の拡張(マルチスレッド・除外ファイルの正規表現)を + * 対象として、実ファイルシステムを経由した動作確認を行う。 + * + * 1. 検索プリミティブ(文字列検索・正規表現) + * 2. CGrepEnumKeys のキー解釈 + * 3. CCodeMediator の文字コード自動判定 + * 4. 実ファイルに対する CGrepEnumFilterFiles / CGrepEnumFilterFolders の列挙 + * 5. CGrepAgent::DoGrepFileWorker による実ファイル内検索 + * 6. PR #2459 のマルチスレッド経路(シングルスレッドとの結果一致) + * 7. PR #2459 の除外ファイルの正規表現適用 + * 8. サブフォルダー再帰、32000 行ファイル、キャンセル伝播 等 + * + * 一時ファイル群は RAII クラス GrepTempDir で生成・破棄を行う。 + */ + +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "agent/CGrepAgent.h" +#include "agent/CSearchAgent.h" +#include "charset/CCodeFactory.h" +#include "charset/CCodeMediator.h" +#include "extmodule/CBregexp.h" +#include "grep/CGrepEnumFileBase.h" +#include "grep/CGrepEnumFiles.h" +#include "grep/CGrepEnumFolders.h" +#include "grep/CGrepEnumFilterFiles.h" +#include "grep/CGrepEnumFilterFolders.h" +#include "grep/CGrepEnumKeys.h" +#include "io/CFileLoad.h" +#include "util/file.h" + +#include "dlg/CDlgCancel.h" +#include "view/CEditView.h" + +#include "window/EditorTestSuite.hpp" + +namespace { + +// ============================================================================= +// 一時ディレクトリ RAII +// ============================================================================= + +/*! + * テスト用の一時ディレクトリを生成・自動削除する RAII クラス + * + * - コンストラクタで一意な一時ディレクトリを作る + * - デストラクタで再帰削除する + * - WriteEncodedTextFile / WriteRawBytes 等のヘルパで配下にファイルを書き込む + */ +class GrepTempDir +{ +public: + explicit GrepTempDir(std::wstring_view prefix = L"grp") + { + // GetTempFilePath は一時ファイルを生成するので、それを消してから + // 同名のディレクトリを掘り直すことで一意性を担保する。 + auto candidate = GetTempFilePath(prefix); + std::filesystem::remove(candidate); + std::filesystem::create_directories(candidate); + m_root = candidate; + } + + GrepTempDir(const GrepTempDir&) = delete; + GrepTempDir& operator=(const GrepTempDir&) = delete; + + ~GrepTempDir() + { + std::error_code ec; + std::filesystem::remove_all(m_root, ec); + } + + const std::filesystem::path& Root() const noexcept { return m_root; } + + std::filesystem::path Sub(std::wstring_view relative) const + { + return m_root / std::filesystem::path(relative); + } + + void EnsureDir(std::wstring_view relative) const + { + std::filesystem::create_directories(Sub(relative)); + } + + /*! + * 任意の文字コードでテキストファイルを書き出す + * + * BOM 付き UTF-8 / UTF-16LE / UTF-16BE に対応するために + * withBom を指定可能にしている(自動判定経路のテストで使用)。 + */ + std::filesystem::path WriteEncodedTextFile( + std::wstring_view relative, + ECodeType codeType, + std::wstring_view text, + bool withBom = false) const + { + const auto path = Sub(relative); + std::filesystem::create_directories(path.parent_path()); + + const auto encoded = CCodeFactory::ConvertToCode(codeType, std::wstring(text)); + if (encoded.result != RESULT_COMPLETE) { + ADD_FAILURE() << "CCodeFactory::ConvertToCode failed for codeType=" + << static_cast(codeType); + return path; + } + + std::ofstream os(path, std::ios::binary | std::ios::trunc); + if (!os) { + ADD_FAILURE() << "failed to open file for write: " << path.string(); + return path; + } + + if (withBom) { + switch (codeType) { + case CODE_UTF8: + os.write("\xEF\xBB\xBF", 3); + break; + case CODE_UNICODE: + os.write("\xFF\xFE", 2); + break; + case CODE_UNICODEBE: + os.write("\xFE\xFF", 2); + break; + default: + break; + } + } + os.write(encoded.destination.data(), + static_cast(encoded.destination.size())); + return path; + } + + std::filesystem::path WriteRawBytes( + std::wstring_view relative, std::string_view bytes) const + { + const auto path = Sub(relative); + std::filesystem::create_directories(path.parent_path()); + std::ofstream os(path, std::ios::binary | std::ios::trunc); + os.write(bytes.data(), static_cast(bytes.size())); + return path; + } + +private: + std::filesystem::path m_root; +}; + +// ============================================================================= +// 補助ヘルパ +// ============================================================================= + +SSearchOption MakeSearchOption(bool regex, bool caseSensitive, bool wordOnly = false) +{ + SSearchOption opt; + opt.Reset(); + opt.bRegularExp = regex; + opt.bLoHiCase = caseSensitive; + opt.bWordOnly = wordOnly; + return opt; +} + +SGrepOption MakeGrepOption(ECodeType charSet = CODE_AUTODETECT) +{ + // テストで共通に使う Grep 条件をまとめ、個別ケースでは必要な項目だけ上書きする。 + SGrepOption gopt; + gopt.nGrepCharSet = charSet; + gopt.nGrepOutputStyle = 1; // Normal + gopt.nGrepOutputLineType = 1; // ヒット行を出力 + gopt.bGrepHeader = false; + return gopt; +} + +/*! + * 1 ファイルに対する Grep ワーカーを呼び出してヒット数を返す + * + * CGrepAgent::DoGrepFileWorker は PR #2459 で並列 Grep の単位として + * 切り出された関数。ここで直接呼ぶことで、ファイル列挙以降の検索処理を + * 実ファイルに対して検証する。 + * + * @retval -1 キャンセル + * @retval >=0 ヒット数 + */ +int RunGrepFileWorker( + CGrepAgent& agent, + const std::filesystem::path& path, + std::wstring_view key, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption, + std::atomic& cancel) +{ + const std::wstring keyStr(key); + const std::wstring fullPath = path.wstring(); + const std::wstring fileName = path.filename().wstring(); + const std::wstring baseFolder = path.parent_path().wstring(); + + SGrepFileTask task; + task.fullPath = fullPath; + task.fileName = fileName; + task.baseFolder = baseFolder; + task.folder = baseFolder; + task.relPath = fileName; + + CBregexp regexp; + if (sSearchOption.bRegularExp) { + if (!InitRegexp(nullptr, regexp, false)) { + ADD_FAILURE() << "InitRegexp failed (bregonig.dll missing?)"; + return -1; + } + const DWORD flags = sSearchOption.bLoHiCase + ? CBregexp::optCaseSensitive + : 0; + if (!regexp.Compile(keyStr.c_str(), flags)) { + ADD_FAILURE() << "regexp.Compile failed: " << path.string(); + return -1; + } + } + + CSearchStringPattern pattern; + if (!pattern.SetPattern(nullptr, keyStr.c_str(), keyStr.size(), + sSearchOption, sSearchOption.bRegularExp ? ®exp : nullptr)) { + ADD_FAILURE() << "SetPattern failed"; + return -1; + } + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + cmemMessage.AllocStringBuffer(4000); + cUnicodeBuffer.AllocStringBuffer(4000); + + return agent.DoGrepFileWorker( + SGrepSearchParams{ keyStr.c_str(), sSearchOption, sGrepOption }, + task, + sSearchOption.bRegularExp ? ®exp : nullptr, + pattern, + cancel, + cmemMessage, cUnicodeBuffer); +} + +// 同上だが、外側でキャンセル管理しない単純版 +int RunGrepFileWorker( + CGrepAgent& agent, + const std::filesystem::path& path, + std::wstring_view key, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption) +{ + std::atomic cancel{ false }; + return RunGrepFileWorker(agent, path, key, sSearchOption, sGrepOption, cancel); +} + +/*! + * 1 行分のテキストを EOL 付きで反復した文字列を作る + * + * 大規模ファイルやヒット間隔を制御したテストデータを作るためのもの。 + * + * @param lineCount 行数 + * @param hitInterval 1 以上ならその倍数行に key を挿入し、それ以外は dummy を挿入する。0 以下なら常に dummy。 + */ +std::wstring BuildLineSequence( + std::wstring_view key, + std::wstring_view dummy, + int lineCount, + int hitInterval) +{ + std::wstring out; + out.reserve(static_cast(lineCount) * 16); + for (int i = 1; i <= lineCount; ++i) { + if (hitInterval > 0 && (i % hitInterval) == 0) { + out.append(key); + } else { + out.append(dummy); + } + out.push_back(L'\n'); + } + return out; +} + +} // namespace + +// ============================================================================= +// テストフィクスチャ +// ============================================================================= + +namespace { + +/*! + * 実ファイル系 Grep テストの共通フィクスチャ + * + * - SetUpTestSuite で DLLSHAREDATA を一度だけ初期化(CDocTypeManager 経由で + * DoGrepFileWorker が参照する) + * - 各テストの SetUp で一意な一時ディレクトリを掘り、TearDown で破棄 + */ +struct GrepRealFileTest + : public ::testing::Test + , public window::EditorTestSuite +{ + static void SetUpTestSuite() + { + SetUpEditor(); + } + + static void TearDownTestSuite() + { + TearDownEditor(); + } + + void SetUp() override + { + m_temp = std::make_unique(); + } + + void TearDown() override + { + m_temp.reset(); + } + + std::unique_ptr m_temp; +}; + +} // namespace + +// ============================================================================= +// 1. 検索プリミティブ(in-memory) +// ============================================================================= + +/*! + * @brief 大小文字区別 ON/OFF の文字列検索 + * @remark 大小文字区別なし(OFF)では小文字パターンで大文字混じりにヒットし、区別あり(ON)ではヒットしないことを確認する。 + */ +TEST(GrepSearchEngine, LiteralSearchCaseSensitivity) +{ + const std::wstring line = L"Needle in a haystack"; + + { + const auto opt = MakeSearchOption(false, /*caseSensitive=*/false); + CSearchStringPattern pattern; + ASSERT_TRUE(pattern.SetPattern(nullptr, L"needle", 6, opt, nullptr)); + EXPECT_NE(CSearchAgent::SearchString( + line.c_str(), static_cast(line.size()), 0, pattern), nullptr); + } + { + const auto opt = MakeSearchOption(false, /*caseSensitive=*/true); + CSearchStringPattern pattern; + ASSERT_TRUE(pattern.SetPattern(nullptr, L"needle", 6, opt, nullptr)); + EXPECT_EQ(CSearchAgent::SearchString( + line.c_str(), static_cast(line.size()), 0, pattern), nullptr); + } +} + +/*! + * @brief 日本語検索語の複数文字コード in-memory 検索 + * @remark SJIS / JIS / EUC / UTF-8 / UTF-16(LE/BE) / UTF-7 / CESU-8 にエンコード・デコードした後でも + * in-memory 検索で漏れがないことを確認する。ファイル経由の検索は GrepRealFileTest 側で別途確認する。 + */ +TEST_F(GrepRealFileTest, JapaneseLiteralSearchAcrossMixedEncodings) +{ + const std::wstring key = L"検索キー"; + const std::vector codeTypes = { + CODE_SJIS, + CODE_JIS, + CODE_EUC, + CODE_UNICODE, + CODE_UNICODEBE, + CODE_UTF8, + CODE_UTF7, + CODE_CESU8, + }; + + const auto opt = MakeSearchOption(false, false); + CSearchStringPattern pattern; + ASSERT_TRUE(pattern.SetPattern(nullptr, key.c_str(), key.size(), opt, nullptr)); + + for (const auto codeType : codeTypes) { + const auto encoded = CCodeFactory::ConvertToCode(codeType, + std::wstring(L"先頭\n検索キー\n末尾\n")); + ASSERT_EQ(encoded.result, RESULT_COMPLETE) + << "codeType=" << static_cast(codeType); + + const auto decoded = CCodeFactory::LoadFromCode(codeType, encoded.destination); + ASSERT_EQ(decoded.result, RESULT_COMPLETE) + << "codeType=" << static_cast(codeType); + + const std::wstring& text = decoded.destination; + EXPECT_NE(CSearchAgent::SearchString( + text.c_str(), static_cast(text.size()), 0, pattern), nullptr) + << "codeType=" << static_cast(codeType); + } +} + +/*! + * @brief 正規表現エンジンの基本動作 + * @remark コンパイル・マッチ・大小文字オプションが正しく機能することを確認する。 + */ +TEST_F(GrepRealFileTest, RegexCompileAndMatch) +{ + CBregexp regexp; + ASSERT_TRUE(InitRegexp(nullptr, regexp, false)); + ASSERT_TRUE(regexp.Compile(L"^needle$", CBregexp::optCaseSensitive)); + EXPECT_TRUE(regexp.Match(L"needle", 6, 0)); // 一致する + EXPECT_FALSE(regexp.Match(L"Needle", 6, 0)); // 大文字小文字区別で不一致 +} + +// ============================================================================= +// 2. CGrepEnumKeys 解析 +// ============================================================================= + +/*! + * @brief `!` プレフィックスを正規表現モードで除外正規表現として保存 (PR #2459) + * @remark 正規表現モード(bExcludeFileRegex=true)で `!` を除外正規表現として保存し、 + * GetExcludeFiles にも反映されることを確認する。 + */ +TEST(CGrepEnumKeys, ParseRegexExcludePattern) +{ + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.txt;!.*skip.*\\.txt$", /*bExcludeFileRegex=*/true)); + + ASSERT_EQ(keys.m_vecExceptFileRegexPatterns.size(), 1u); + EXPECT_EQ(keys.m_vecExceptFileRegexPatterns[0], L".*skip.*\\.txt$"); // ! を除いた文字列で格納 + + const auto excludeFiles = keys.GetExcludeFiles(); + ASSERT_EQ(excludeFiles.size(), 1u); + EXPECT_EQ(excludeFiles[0], L".*skip.*\\.txt$"); // GetExcludeFiles にも同値が反映 +} + +/*! + * @brief ファイルパターンと除外フォルダーの同時解析 + * @remark 検索対象ファイルパターンと除外フォルダー指定が正しく振り分けられ、 + * フォルダー未指定時は *.* を補完することを確認する。 + */ +TEST(CGrepEnumKeys, ParseFileAndFolderKeysWithDefaults) +{ + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.cpp;#build")); + + ASSERT_EQ(keys.m_vecSearchFileKeys.size(), 1u); + EXPECT_STREQ(keys.m_vecSearchFileKeys[0].c_str(), L"*.cpp"); // *.cpp が検索対象ファイルに振り分けられる + + const auto excludeFolders = keys.GetExcludeFolders(); + ASSERT_EQ(excludeFolders.size(), 1u); + EXPECT_STREQ(excludeFolders[0].c_str(), L"build"); // # を除いたフォルダー名で除外に振り分けられる + + ASSERT_EQ(keys.m_vecSearchFolderKeys.size(), 1u); + EXPECT_STREQ(keys.m_vecSearchFolderKeys[0].c_str(), L"*.*"); // フォルダー未指定時は *.* を補完 +} + +/*! + * @brief 絶対パス混在の検索キーはエラー + * @remark 検索パターンに絶対パスが含まれる場合、SetFileKeys が戻り値 2 を返して拒否することを確認する。 + */ +TEST(CGrepEnumKeys, AbsolutePathInSearchKeyIsRejected) +{ + CGrepEnumKeys keys; + EXPECT_EQ(2, keys.SetFileKeys(L"C:\\foo\\*.cpp")); +} + +// ============================================================================= +// 3. CCodeMediator 自動判定 +// ============================================================================= + +/*! + * @brief BOM 検出と空バッファ時の既定コードフォールバック + * @remark 空バッファは既定コードを返し、BOM 付きの UTF-8 / UTF-16LE / UTF-16BE は + * それぞれ正しい文字コードを返すことを確認する。 + */ +TEST(CCodeMediator, AutoDetectBomAndDefaultCode) +{ + SEncodingConfig encodingConfig{}; + encodingConfig.m_bPriorCesu8 = false; + encodingConfig.m_eDefaultCodetype = CODE_UTF8; + + CCodeMediator mediator(encodingConfig); + + EXPECT_EQ(mediator.CheckKanjiCode(nullptr, 0), CODE_UTF8); // 空入力は既定コード + + const auto utf8Body = CCodeFactory::ConvertToCode(CODE_UTF8, std::wstring(L"検索キー")); + ASSERT_EQ(utf8Body.result, RESULT_COMPLETE); + const std::string utf8Bom = std::string("\xEF\xBB\xBF", 3) + utf8Body.destination; + EXPECT_EQ(mediator.CheckKanjiCode(utf8Bom.data(), utf8Bom.size()), CODE_UTF8); // UTF-8 BOM を検出 + + const auto utf16leBody = CCodeFactory::ConvertToCode(CODE_UNICODE, std::wstring(L"検索キー")); + ASSERT_EQ(utf16leBody.result, RESULT_COMPLETE); + const std::string utf16leBom = std::string("\xFF\xFE", 2) + utf16leBody.destination; + EXPECT_EQ(mediator.CheckKanjiCode(utf16leBom.data(), utf16leBom.size()), CODE_UNICODE); // UTF-16LE BOM を検出 + + const auto utf16beBody = CCodeFactory::ConvertToCode(CODE_UNICODEBE, std::wstring(L"検索キー")); + ASSERT_EQ(utf16beBody.result, RESULT_COMPLETE); + const std::string utf16beBom = std::string("\xFE\xFF", 2) + utf16beBody.destination; + EXPECT_EQ(mediator.CheckKanjiCode(utf16beBom.data(), utf16beBom.size()), CODE_UNICODEBE); // UTF-16BE BOM を検出 +} + +// ============================================================================= +// 4. 実ファイルに対する CGrepEnumFilterFiles / Folders の列挙 +// ============================================================================= + +/*! + * @brief 拡張子フィルターと除外キーによるファイル列挙 + * @remark `*.cpp` 指定で .cpp ファイルのみが列挙され、AddExceptFile で追加した .bak が除外されることを確認する。 + */ +TEST_F(GrepRealFileTest, EnumerateFiles_FiltersByExtensionAndExcludeKey) +{ + m_temp->WriteEncodedTextFile(L"main.cpp", CODE_UTF8, L"int main(){}\n"); + m_temp->WriteEncodedTextFile(L"util.cpp", CODE_UTF8, L"void util(){}\n"); + m_temp->WriteEncodedTextFile(L"readme.md", CODE_UTF8, L"# readme\n"); + m_temp->WriteEncodedTextFile(L"main.bak", CODE_UTF8, L"old\n"); + + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.cpp")); + ASSERT_EQ(0, keys.AddExceptFile(L"*.bak")); + + CGrepEnumFilterFiles enumFiles; + CGrepEnumOptions enumOpts; + CGrepEnumFiles exceptAbs; + enumFiles.Enumerates(m_temp->Root().wstring().c_str(), keys, enumOpts, exceptAbs); + + std::vector found; + for (int i = 0; i < enumFiles.GetCount(); ++i) { + found.emplace_back(enumFiles.GetFileName(i)); + } + std::sort(found.begin(), found.end()); + + ASSERT_EQ(found.size(), 2u); + EXPECT_EQ(found[0], L"main.cpp"); + EXPECT_EQ(found[1], L"util.cpp"); +} + +/*! + * @brief `#` プレフィックスによるサブフォルダー除外 + * @remark `#excluded` や `#node_modules` で指定したフォルダーが列挙結果から除外され、 + * 指定外のフォルダーのみ残ることを確認する。 + */ +TEST_F(GrepRealFileTest, EnumerateFolders_ExcludesByHashKey) +{ + m_temp->EnsureDir(L"keep"); + m_temp->EnsureDir(L"excluded"); + m_temp->EnsureDir(L"node_modules"); + + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.cpp;#excluded;#node_modules")); + + CGrepEnumFilterFolders enumFolders; + CGrepEnumOptions enumOpts; + CGrepEnumFolders exceptAbs; + enumFolders.Enumerates(m_temp->Root().wstring().c_str(), keys, enumOpts, exceptAbs); + + std::vector found; + for (int i = 0; i < enumFolders.GetCount(); ++i) { + found.emplace_back(enumFolders.GetFileName(i)); + } + std::sort(found.begin(), found.end()); + + ASSERT_EQ(found.size(), 1u); + EXPECT_EQ(found[0], L"keep"); +} + +/*! + * @brief サブフォルダー再帰列挙 + * @remark サブフォルダーを再帰的に検索し、下位フォルダーのファイルも列挙されることを確認する。 + * 列挙順序はファイルシステム依存のため、順序ではなく存在のみを検証する。 + */ +TEST_F(GrepRealFileTest, EnumerateFiles_SubfolderRecursion) +{ + m_temp->WriteEncodedTextFile(L"root.txt", CODE_UTF8, L"a\n"); + m_temp->WriteEncodedTextFile(L"sub1/child.txt", CODE_UTF8, L"b\n"); + m_temp->WriteEncodedTextFile(L"sub1/sub2/grandchild.txt", CODE_UTF8, L"c\n"); + + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.txt")); + + std::vector visited; + + auto walk = [&](auto& self, const std::filesystem::path& folder) -> void { + CGrepEnumFilterFiles enumFiles; + CGrepEnumOptions enumOpts; + CGrepEnumFiles exceptAbs; + enumFiles.Enumerates(folder.wstring().c_str(), keys, enumOpts, exceptAbs); + for (int i = 0; i < enumFiles.GetCount(); ++i) { + visited.emplace_back((folder / enumFiles.GetFileName(i)).wstring()); + } + + CGrepEnumFilterFolders enumFolders; + CGrepEnumFolders exceptAbsFolders; + enumFolders.Enumerates(folder.wstring().c_str(), keys, enumOpts, exceptAbsFolders); + for (int i = 0; i < enumFolders.GetCount(); ++i) { + self(self, folder / enumFolders.GetFileName(i)); + } + }; + walk(walk, m_temp->Root()); + + ASSERT_EQ(visited.size(), 3u); + // ファイルシステムの列挙順序は保証されないため、順序に依存しない検証をする + bool foundGrandchild = false, foundChild = false, foundRoot = false; + for (const auto& v : visited) { + if (v.find(L"grandchild.txt") != std::wstring::npos) foundGrandchild = true; + if (v.find(L"child.txt") != std::wstring::npos) foundChild = true; + if (v.find(L"root.txt") != std::wstring::npos) foundRoot = true; + } + EXPECT_TRUE(foundGrandchild); + EXPECT_TRUE(foundChild); + EXPECT_TRUE(foundRoot); +} + +// ============================================================================= +// 5. CGrepAgent::DoGrepFileWorker による実ファイル内検索 +// ============================================================================= + +/*! + * @brief 単一 UTF-8 ファイルへの基本ヒット数 + * @remark 50 行のファイルに 10 行ごと key を埋め込み、DoGrepFileWorker が 5 件を返すことを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_BasicLiteralHitsInUtf8File) +{ + const auto path = m_temp->WriteEncodedTextFile(L"basic.txt", CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 50, 10)); + + CGrepAgent agent; + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + EXPECT_EQ(5, RunGrepFileWorker(agent, path, L"needle", sOpt, gOpt)); +} + +/*! + * @brief 大小文字オプションの実ファイル経路での反映 + * @remark 大小文字区別なしで 3 件、区別ありで 1 件ヒットすることを実ファイル経由で確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_CaseSensitivityIsRespected) +{ + const auto path = m_temp->WriteEncodedTextFile(L"case.txt", CODE_UTF8, + std::wstring(L"Needle\nneedle\nNEEDLE\n")); + + CGrepAgent agent; + const auto gOpt = MakeGrepOption(); + + EXPECT_EQ(3, RunGrepFileWorker(agent, path, L"needle", + MakeSearchOption(false, /*caseSensitive=*/false), gOpt)); // 大小文字区別なし: 3 件 + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"needle", + MakeSearchOption(false, /*caseSensitive=*/true), gOpt)); // 大小文字区別あり: 1 件 +} + +/*! + * @brief 正規表現検索の実ファイル経路での動作 + * @remark `^id=\d+$` パターンで数字行のみ 3 件ヒットすることを実ファイル経由で確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_RegexHitsInUtf8File) +{ + const auto path = m_temp->WriteEncodedTextFile(L"regex.txt", CODE_UTF8, + std::wstring(L"id=001\nid=042\nname=alice\nid=999\n")); + + CGrepAgent agent; + const auto sOpt = MakeSearchOption(/*regex=*/true, /*caseSensitive=*/true); + const auto gOpt = MakeGrepOption(); + + EXPECT_EQ(3, RunGrepFileWorker(agent, path, L"^id=\\d+$", sOpt, gOpt)); +} + +/*! + * @brief 日本語検索の複数エンコード対応 + * @remark SJIS / BOM 付き UTF-8 / BOM 付き UTF-16LE のファイルを自動判定経由で検索し、 + * いずれも 2 件を返すことを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_JapaneseAcrossEncodings) +{ + const std::wstring body = L"先頭\n検索キー\n中間\n検索キー\n末尾\n"; + + struct Entry { const wchar_t* name; ECodeType code; bool bom; }; + const std::vector entries = { + { L"jp_sjis.txt", CODE_SJIS, false }, + { L"jp_utf8.txt", CODE_UTF8, true }, + { L"jp_utf16.txt", CODE_UNICODE, true }, + }; + + CGrepAgent agent; + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + for (const auto& e : entries) { + const auto path = m_temp->WriteEncodedTextFile(e.name, e.code, body, e.bom); + EXPECT_EQ(2, RunGrepFileWorker(agent, path, L"検索キー", sOpt, gOpt)) + << "encoding=" << static_cast(e.code); + } +} + +/*! + * @brief 32000 行ファイルの全件走査 + * @remark 1000 行ごとに 1 件 key を埋め込んだ 32000 行ファイルを走査し、 + * ヒット数が 32 件で壊れないことを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_32000LinesIsSearchable) +{ + const auto path = m_temp->WriteEncodedTextFile(L"huge.txt", CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 32000, 1000)); + + CGrepAgent agent; + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + EXPECT_EQ(32, RunGrepFileWorker(agent, path, L"needle", sOpt, gOpt)); +} + +/*! + * @brief atomic キャンセルフラグによる走査の中断 + * @remark 50000 行ファイルの走査開始直後にキャンセルフラグを立て、ワーカーが + * -1 または全件未満で終了することを確認する。 + * 32 行ごとのチェックポイントで即時抜けることを期待する。 + */ +TEST_F(GrepRealFileTest, FileWorker_CancelTerminatesScan) +{ + const auto path = m_temp->WriteEncodedTextFile(L"long.txt", CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 50000, 1000)); + + CGrepAgent agent; + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + std::atomic cancel{ false }; + std::promise resultPromise; + auto resultFuture = resultPromise.get_future(); + + std::thread worker([&]() { + resultPromise.set_value( + RunGrepFileWorker(agent, path, L"needle", sOpt, gOpt, cancel)); + }); + + // ワーカーがファイルを開いて走り出すための猶予 + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + cancel.store(true, std::memory_order_release); + + // wait_for の結果を変数に受けてから join し、ASSERT でテスト中断しても + // スレッドがダングリング参照を踏まないようにする。 + const bool ready = resultFuture.wait_for(std::chrono::seconds(5)) + == std::future_status::ready; + worker.join(); + ASSERT_TRUE(ready) << "worker did not finish within 5s"; + const int hits = resultFuture.get(); + + // -1 (キャンセル) もしくは中途半端なヒット数で終わる。 + // いずれにせよ、全件 (50) が出ないこと、もしくは -1 であることを確認する。 + EXPECT_TRUE(hits == -1 || (hits >= 0 && hits < 50)) + << "hits=" << hits << " (50 means cancel was not honored)"; +} + +// ============================================================================= +// 6. PR #2459 マルチスレッド経路の検証 +// ============================================================================= + +namespace { + +/*! + * ファイル群をワーカー数 nThreads で並列走査して合計ヒット数を返す + * + * DoGrepFileWorker を直接呼ぶテスト専用ヘルパ。RunParallelGrep とは独立した実装で、 + * production のスレッドプール経路(RunParallelGrep 本体)はカバーしない。 + * production 本体の検証は RunParallelGrep_* テストを参照。 + */ +int RunWorkersParallel( + const std::vector& files, + std::wstring_view key, + const SSearchOption& sSearchOption, + const SGrepOption& sGrepOption, + unsigned int nThreads) +{ + std::atomic total{ 0 }; + std::atomic next{ 0 }; + std::atomic cancel{ false }; + + CGrepAgent agent; // ワーカー本体はステートレスにふるまうので共有してよい + + auto worker = [&]() { + while (!cancel.load(std::memory_order_acquire)) { + const size_t idx = next.fetch_add(1, std::memory_order_acq_rel); + if (idx >= files.size()) break; + const int hits = RunGrepFileWorker( + agent, files[idx], key, sSearchOption, sGrepOption, cancel); + if (hits < 0) { + cancel.store(true, std::memory_order_release); + break; + } + total.fetch_add(hits, std::memory_order_relaxed); + } + }; + + std::vector workers; + workers.reserve(nThreads); + for (unsigned int i = 0; i < nThreads; ++i) { + workers.emplace_back(worker); + } + for (auto& t : workers) t.join(); + return total.load(); +} + +} // namespace + +/*! + * @brief マルチスレッドとシングルスレッドの合計ヒット数一致 (PR #2459) + * @remark スレッド数 2 / 4 / 8 の並列走査が、シングルスレッドと同じ合計ヒット数を返すことを確認する。 + */ +TEST_F(GrepRealFileTest, MultiThread_HitCountMatchesSingleThread) +{ + std::vector files; + for (int i = 0; i < 12; ++i) { + const std::wstring name = L"file_" + std::to_wstring(i) + L".txt"; + files.push_back(m_temp->WriteEncodedTextFile(name, CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 100, 10))); + } + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + const int singleThreaded = RunWorkersParallel(files, L"needle", sOpt, gOpt, 1); + EXPECT_EQ(singleThreaded, 12 * 10); // 100 行に 10 件 × 12 ファイル + + for (unsigned int n : { 2u, 4u, 8u }) { + const int parallel = RunWorkersParallel(files, L"needle", sOpt, gOpt, n); + EXPECT_EQ(parallel, singleThreaded) << "threads=" << n; + } +} + +/*! + * @brief 並列走査の繰り返しによるデッドロック・件数ぶれ検証 (PR #2459) + * @remark 4 スレッドで同一入力を 5 回繰り返し、デッドロックが発生せず件数が毎回一致することを確認する。 + */ +TEST_F(GrepRealFileTest, MultiThread_StressNoDeadlockAcrossRepeats) +{ + std::vector files; + for (int i = 0; i < 8; ++i) { + const std::wstring name = L"stress_" + std::to_wstring(i) + L".txt"; + files.push_back(m_temp->WriteEncodedTextFile(name, CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 200, 20))); + } + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + const int expected = 8 * (200 / 20); + + for (int round = 0; round < 5; ++round) { + const int hits = RunWorkersParallel(files, L"needle", sOpt, gOpt, 4); + EXPECT_EQ(hits, expected) << "round=" << round; + } +} + +// ============================================================================= +// 7. PR #2459 除外正規表現の適用 +// ============================================================================= + +/*! + * @brief 正規表現モードの除外を列挙結果へ適用 (PR #2459) + * @remark 正規表現モード(bExcludeFileRegex=true)で `!.*\.obj$` / `!.*\.exe$` を SetFileKeys に与え、 + * production と同じ経路(CBregexp.Compile → Match)で列挙結果をフィルタリングした結果、 + * .cpp ファイルのみが残ることを確認する。 + */ +TEST_F(GrepRealFileTest, RegexExclude_AppliedToEnumeratedFiles) +{ + m_temp->WriteEncodedTextFile(L"main.cpp", CODE_UTF8, L"int main(){}\n"); + m_temp->WriteEncodedTextFile(L"util.cpp", CODE_UTF8, L"void util(){}\n"); + m_temp->WriteEncodedTextFile(L"main.obj", CODE_UTF8, L"binary-ish\n"); + m_temp->WriteEncodedTextFile(L"app.exe", CODE_UTF8, L"binary-ish\n"); + + CGrepEnumKeys keys; + // 探索対象は *.* だが、.obj と .exe は正規表現で除外 + ASSERT_EQ(0, keys.SetFileKeys(L"*.*;!.*\\.obj$;!.*\\.exe$", /*bExcludeFileRegex=*/true)); + ASSERT_EQ(keys.m_vecExceptFileRegexPatterns.size(), 2u); + + // production と同じく、テストドライバが各パターンを CBregexp でコンパイル + std::vector excludeRegexps(keys.m_vecExceptFileRegexPatterns.size()); + for (size_t i = 0; i < keys.m_vecExceptFileRegexPatterns.size(); ++i) { + ASSERT_TRUE(InitRegexp(nullptr, excludeRegexps[i], false)); + ASSERT_TRUE(excludeRegexps[i].Compile( + keys.m_vecExceptFileRegexPatterns[i].c_str(), + CBregexp::optCaseSensitive)); + } + + // 列挙 + CGrepEnumFilterFiles enumFiles; + CGrepEnumOptions enumOpts; + CGrepEnumFiles exceptAbs; + enumFiles.Enumerates(m_temp->Root().wstring().c_str(), keys, enumOpts, exceptAbs); + + // 列挙結果に対して production と同じく除外正規表現を当てる + std::vector survivors; + for (int i = 0; i < enumFiles.GetCount(); ++i) { + const std::wstring fullPath = (m_temp->Root() / enumFiles.GetFileName(i)).wstring(); + bool excluded = false; + for (auto& re : excludeRegexps) { + if (re.Match(fullPath.c_str(), static_cast(fullPath.size()), 0)) { + excluded = true; + break; + } + } + if (!excluded) { + survivors.emplace_back(enumFiles.GetFileName(i)); + } + } + std::sort(survivors.begin(), survivors.end()); + + ASSERT_EQ(survivors.size(), 2u); + EXPECT_EQ(survivors[0], L"main.cpp"); + EXPECT_EQ(survivors[1], L"util.cpp"); +} + +/*! + * @brief 正規表現モードで不正なパターンはコンパイル失敗 + * @remark 正規表現モード(bExcludeFileRegex=true)で不正な構文の除外パターンを与えると + * CBregexp::Compile が false を返し、production での上流バリデーション(DoGrep 側)で + * 弾けることを確認する。 + */ +TEST_F(GrepRealFileTest, RegexExclude_InvalidPatternFailsToCompile) +{ + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.cpp;!*invalid(", /*bExcludeFileRegex=*/true)); + ASSERT_EQ(keys.m_vecExceptFileRegexPatterns.size(), 1u); + + CBregexp regexp; + ASSERT_TRUE(InitRegexp(nullptr, regexp, false)); + EXPECT_FALSE(regexp.Compile( + keys.m_vecExceptFileRegexPatterns[0].c_str(), + CBregexp::optCaseSensitive)); +} + +// ============================================================================= +// Phase 2-B: CGrepEnumKeys coverage for SplitPattern / AddExcept* (PR #2459) +// ============================================================================= + +// ----- CGrepEnumKeys::SplitPattern 区切り 3 種 ----- + +/*! + * @brief 文字列分割(SplitPattern)の仕様:セミコロンによる区切り + * @remark セミコロン(;)で区切った場合、正しく複数の要素に分割することを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_SemicolonSeparator) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;*.h"); + EXPECT_EQ(2, keys.m_vecSearchFileKeys.size()); + if (keys.m_vecSearchFileKeys.size() == 2) { + EXPECT_STREQ(L"*.cpp", keys.m_vecSearchFileKeys[0].c_str()); + EXPECT_STREQ(L"*.h", keys.m_vecSearchFileKeys[1].c_str()); + } +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:スペースによる区切り + * @remark スペースで区切った場合も、正しく複数の要素に分割することを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_SpaceSeparator) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp *.h"); + EXPECT_EQ(2, keys.m_vecSearchFileKeys.size()); +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:カンマによる区切り + * @remark カンマ(,)で区切った場合も、正しく複数の要素に分割することを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_CommaSeparator) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp,*.h"); + EXPECT_EQ(2, keys.m_vecSearchFileKeys.size()); +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:複数種類の区切り文字の混在 + * @remark セミコロン、スペース、カンマが混在している場合でも、すべての区切り文字が有効に機能することを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_MixedSeparators) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;*.h *.txt,*.md"); + EXPECT_EQ(4, keys.m_vecSearchFileKeys.size()); +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:連続する区切り文字 + * @remark 区切り文字が連続している場合、空の要素を生成せず無視する(my_strtokの仕様)ことを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_ConsecutiveSeparators) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;;;*.h"); + EXPECT_EQ(2, keys.m_vecSearchFileKeys.size()); +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:末尾の区切り文字 + * @remark 文字列の末尾に区切り文字がある場合でも、余分な空要素を生成しないことを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_TrailingSeparator) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;"); + EXPECT_EQ(1, keys.m_vecSearchFileKeys.size()); +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:ダブルクォートの除去 + * @remark 要素がダブルクォートで囲まれている場合、分割後にダブルクォートが取り除かれることを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_QuotesRemoved) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"\"*.cpp\";\"*.h\""); + EXPECT_EQ(2, keys.m_vecSearchFileKeys.size()); + if (keys.m_vecSearchFileKeys.size() == 2) { + EXPECT_STREQ(L"*.cpp", keys.m_vecSearchFileKeys[0].c_str()); + EXPECT_STREQ(L"*.h", keys.m_vecSearchFileKeys[1].c_str()); + } +} + +/*! + * @brief 文字列分割(SplitPattern)の仕様:要素途中のダブルクォート + * @remark 要素の途中にダブルクォートが含まれている場合、文字列から + * そのダブルクォートを除去して連結することを確認する。 + */ +TEST(CGrepEnumKeys, SplitPattern_QuotesInMiddle) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"a\"b\"c"); + EXPECT_EQ(1, keys.m_vecSearchFileKeys.size()); + if (keys.m_vecSearchFileKeys.size() == 1) { + EXPECT_STREQ(L"abc", keys.m_vecSearchFileKeys[0].c_str()); + } +} + +// ----- CGrepEnumKeys::SetFileKeys 戻り値・分岐 ----- + +/*! + * @brief 検索対象キー設定(SetFileKeys)の仕様:空入力のフォールバック + * @remark 空文字列が入力された場合、デフォルトの "*.*" が検索対象・除外対象の双方に設定され、 + * 戻り値が0になることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_EmptyInputFallsBackToWildcard) +{ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"")); // 受理する + EXPECT_EQ(1, keys.m_vecSearchFileKeys.size()); // *.* が 1 件補完される + if (keys.m_vecSearchFileKeys.size() == 1) { + EXPECT_STREQ(L"*.*", keys.m_vecSearchFileKeys[0].c_str()); + } +} + +/*! + * @brief 検索対象キー設定(SetFileKeys)の仕様:パス中のワイルドカード + * @remark パス文字列内にワイルドカードが含まれる場合、戻り値が1になることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_WildcardInPathReturns1) +{ + CGrepEnumKeys keys; + EXPECT_EQ(1, keys.SetFileKeys(L"*\\file.exe")); +} + +/*! + * @brief 検索対象キー設定(SetFileKeys)の仕様:絶対パス指定 + * @remark 検索対象として絶対パスが指定された場合、戻り値が2になることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_AbsolutePathInSearchReturns2) +{ + CGrepEnumKeys keys; + EXPECT_EQ(2, keys.SetFileKeys(L"C:\\foo\\*.cpp")); +} + +/*! + * @brief 検索対象キー設定(SetFileKeys)の仕様:除外フォルダー(相対パス) + * @remark `#` から始まる相対パスが、除外フォルダーのリスト(m_vecExceptFolderKeys)に正しく追加され、 + * 戻り値が0になることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_ExcludeFolderHashRelative) +{ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"#build")); // 受理する + EXPECT_EQ(1, keys.m_vecExceptFolderKeys.size()); // 相対除外フォルダーに 1 件追加 + if (keys.m_vecExceptFolderKeys.size() == 1) { + EXPECT_STREQ(L"build", keys.m_vecExceptFolderKeys[0].c_str()); + } +} + +/*! + * @brief 検索対象キー設定(SetFileKeys)の仕様:除外フォルダー(絶対パス) + * @remark `#` から始まる絶対パスが、絶対パス用の除外フォルダーリスト(m_vecExceptAbsFolderKeys)に追加され、 + * 戻り値が0になることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_ExcludeFolderHashAbsolute) +{ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"#C:\\build")); // 受理する + EXPECT_EQ(1, keys.m_vecExceptAbsFolderKeys.size()); // 絶対除外フォルダーに 1 件追加 + if (keys.m_vecExceptAbsFolderKeys.size() == 1) { + EXPECT_STREQ(L"C:\\build", keys.m_vecExceptAbsFolderKeys[0].c_str()); + } +} + +/*! + * @brief 正規表現モードで `!` パターンは ValidateKey をスキップして受理される + * @remark 正規表現モード(bExcludeFileRegex=true)では `!` から始まるパターンが + * `ValidateKey` の事前チェックをスキップするため、不正な文法でも戻り値0(受理)になることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_RegexExcludeNoValidation) +{ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.SetFileKeys(L"!*invalid\\(", /*bExcludeFileRegex=*/true)); +} + +/*! + * @brief 検索対象キー設定(SetFileKeys)の仕様:重複キーの排除 + * @remark まったく同じ検索対象キーが複数指定された場合、内部で重複が排除(Deduplication)され、 + * 1つにまとめられることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_DuplicateKeyDeduplicated) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;*.cpp;*.cpp"); + EXPECT_EQ(1, keys.m_vecSearchFileKeys.size()); +} + +/*! + * @brief `!` はデフォルト (bExcludeFileRegex=false) ではワイルドカード除外に入る (回帰) + * @remark 第2引数を省略した場合、`!` プレフィックスはワイルドカード除外リストに格納され、 + * 正規表現除外リストは空のままであることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_ExcludeFileWildcardByDefault) +{ + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.cpp;!*.obj")); // 第2引数省略=false + ASSERT_EQ(1u, keys.m_vecExceptFileKeys.size()); + EXPECT_STREQ(L"*.obj", keys.m_vecExceptFileKeys[0].c_str()); + EXPECT_TRUE(keys.m_vecExceptFileRegexPatterns.empty()); +} + +/*! + * @brief `~` は特殊文字ではなく通常の検索対象ファイル名として扱われる (回帰) + * @remark `~` プレフィックスは除外でも正規表現でもなく、 + * 通常の検索対象ファイルキーとして格納されることを確認する。 + */ +TEST(CGrepEnumKeys, SetFileKeys_TildeIsNormalSearchTarget) +{ + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"~temp*")); + ASSERT_EQ(1u, keys.m_vecSearchFileKeys.size()); + EXPECT_STREQ(L"~temp*", keys.m_vecSearchFileKeys[0].c_str()); + EXPECT_TRUE(keys.m_vecExceptFileKeys.empty()); // 除外(ワイルドカード)にも入らない + EXPECT_TRUE(keys.m_vecExceptFileRegexPatterns.empty()); // 除外(正規表現)にも入らない +} + +// ----- AddExceptFile / AddExceptFolder ----- + +/*! + * @brief 個別除外指定(AddExceptFile)の仕様:相対パスの複数追加 + * @remark AddExceptFileを使用して相対パスを渡した際、区切り文字で分割して + * 除外ファイルリスト(相対)に追加することを確認する。 + */ +TEST(CGrepEnumKeys, AddExceptFile_RelativePath) +{ + CGrepEnumKeys keys; + keys.AddExceptFile(L"*.obj;*.tmp"); + EXPECT_EQ(2, keys.m_vecExceptFileKeys.size()); // セミコロンで 2 件に分割して相対リストに追加 +} + +/*! + * @brief 個別除外指定(AddExceptFile)の仕様:絶対パスの追加 + * @remark 絶対パスが渡された場合は、絶対パス用の除外ファイルリスト(m_vecExceptAbsFileKeys)に追加することを確認する。 + */ +TEST(CGrepEnumKeys, AddExceptFile_AbsolutePath) +{ + CGrepEnumKeys keys; + keys.AddExceptFile(L"C:\\tmp\\foo.obj"); + EXPECT_EQ(1, keys.m_vecExceptAbsFileKeys.size()); + if (keys.m_vecExceptAbsFileKeys.size() == 1) { + EXPECT_STREQ(L"C:\\tmp\\foo.obj", keys.m_vecExceptAbsFileKeys[0].c_str()); + } +} + +/*! + * @brief デフォルト除外フォルダの追加(AddExceptFolder)の仕様 + * @remark デフォルトで用意されている複数の相対除外パターン(.git, .svn, .vs等)を一括で正しく追加することを確認する。 + */ +TEST(CGrepEnumKeys, AddExceptFolder_DefaultPattern) +{ + CGrepEnumKeys keys; + EXPECT_EQ(0, keys.AddExceptFolder(L".git;.svn;.vs")); // 追加を受け付ける + EXPECT_EQ(3, keys.m_vecExceptFolderKeys.size()); // 相対フォルダーが 3 件 + EXPECT_EQ(0, keys.m_vecExceptAbsFolderKeys.size()); // 絶対側は増えない +} + +/*! + * @brief 除外ファイルの追加(AddExceptFile)の仕様:#記号の扱い + * @remark AddExceptFileでは `#` が特別な意味(フォルダー指定)を持たず、通常の文字列としてファイル名に処理することを確認する。 + */ +TEST(CGrepEnumKeys, AddExceptFile_HashIsNotSpecial) +{ + CGrepEnumKeys keys; + keys.AddExceptFile(L"#foo"); + EXPECT_EQ(1, keys.m_vecExceptFileKeys.size()); + if (keys.m_vecExceptFileKeys.size() == 1) { + EXPECT_STREQ(L"#foo", keys.m_vecExceptFileKeys[0].c_str()); + } +} + +// ----- GetExcludeFiles / GetExcludeFolders 集約 ----- + +/*! + * @brief 除外ファイルの集約取得(GetExcludeFiles)の仕様 + * @remark 正規表現モード(bExcludeFileRegex=true)の SetFileKeys と AddExceptFile の両方で登録された + * 除外ファイルが GetExcludeFiles で集約されることを確認する。 + * 集約対象は m_vecExceptFileKeys, m_vecExceptAbsFileKeys, + * m_vecExceptFileRegexPatterns の 3 配列。 + * ただし実装によっては一部配列が含まれない場合がある(件数は実装に合わせて凍結)。 + */ +TEST(CGrepEnumKeys, GetExcludeFiles_MergesAllThreeArrays) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;!.*\\.obj$", /*bExcludeFileRegex=*/true); + keys.AddExceptFile(L"*.tmp;C:\\foo.bak"); + auto files = keys.GetExcludeFiles(); + EXPECT_EQ(3, files.size()); // 正規表現・相対・絶対の 3 系統を集約した合計 +} + +/*! + * @brief 除外フォルダーの集約取得(GetExcludeFolders)の仕様 + * @remark SetFileKeysで設定された相対パス(`#`)および絶対パス(`#C:\...`)の除外フォルダーを + * 正しく1つに集約することを確認する。 + */ +TEST(CGrepEnumKeys, GetExcludeFolders_MergesRelAndAbs) +{ + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.cpp;#build;#C:\\sys"); + auto folders = keys.GetExcludeFolders(); + EXPECT_EQ(2, folders.size()); // 相対 1 件 + 絶対 1 件の合計 +} + +// ============================================================================= +// 9. RunParallelGrep 経路(UI非依存でテスト可能) +// ============================================================================= + +/*! + * @brief RunParallelGrep が pcDlgCancel/pcViewDst=nullptr で動作し、ヒット数が正しい (PR #2459) + * @remark UI 非依存(nullptr 渡し)でスレッドプール経路を走らせ、直列経路と同じ件数になることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_HitCountMatchesSingleThread) +{ + for (int i = 0; i < 8; ++i) { + m_temp->WriteEncodedTextFile( + L"rp_" + std::to_wstring(i) + L".txt", + CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 100, 10)); + } + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + const std::wstring rootPath = m_temp->Root().wstring(); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CGrepEnumFiles cGrepExceptAbsFiles; + CGrepEnumFolders cGrepExceptAbsFolders; + CNativeW cmemMessage; + int nHitCount = 0; + + const std::vector vPaths = { rootPath }; + const int result = agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ L"needle", sOpt, gOpt }, + SGrepEnumContext{ keys, cGrepExceptAbsFiles, cGrepExceptAbsFolders }, + vPaths, + cmemMessage, nHitCount); + + EXPECT_EQ(0, result); // 0 = 完了(-1 はキャンセル) + EXPECT_EQ(8 * 10, nHitCount); // 8 ファイル × 10 件 +} + +/*! + * @brief RunParallelGrep がサブフォルダーを再帰して合計ヒット数を返す (1-1: サブフォルダー分岐経路) + * @remark ルート + sub1/ + sub1/sub2/ に配置したファイルを bGrepSubFolder=true で検索し、 + * RunParallelGrep のバッチ1..N(cTopFolders 列挙 → サブフォルダーバッチ)経路を踏む。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_SubfolderRecursion) +{ + // ルート直下: 50行・10行おきにヒット → 5件 + m_temp->WriteEncodedTextFile(L"root.txt", CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 50, 10)); + // sub1/: 30行・10行おきにヒット → 3件 + m_temp->WriteEncodedTextFile(L"sub1/s1.txt", CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 30, 10)); + // sub1/sub2/: 20行・10行おきにヒット → 2件 + m_temp->WriteEncodedTextFile(L"sub1/sub2/s2.txt", CODE_UTF8, + BuildLineSequence(L"needle", L"abc", 20, 10)); + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepSubFolder = true; + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CGrepEnumFiles cExAbsFiles; + CGrepEnumFolders cExAbsFolders; + CNativeW cmemMessage; + int nHit = 0; + + const std::vector vPaths = { m_temp->Root().wstring() }; + const int rc = agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ L"needle", sOpt, gOpt }, + SGrepEnumContext{ keys, cExAbsFiles, cExAbsFolders }, + vPaths, + cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(10, nHit); // 5 + 3 + 2 +} + +/*! + * @brief RunParallelGrep のワーカー内除外正規表現が適用される (1-2: excludeRegexps Compile/Match 経路) + * @remark .cpp/.obj/.exe の4ファイルを配置し、.obj/.exe を除外正規表現で除外した結果 + * .cpp 2ファイル分のヒットのみが返ることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_ExcludeRegexFiltersWorker) +{ + // 4ファイルに各 2 件の needle を含む + m_temp->WriteEncodedTextFile(L"main.cpp", CODE_UTF8, L"needle\nneedle\n"); + m_temp->WriteEncodedTextFile(L"util.cpp", CODE_UTF8, L"needle\nneedle\n"); + m_temp->WriteEncodedTextFile(L"main.obj", CODE_UTF8, L"needle\nneedle\n"); + m_temp->WriteEncodedTextFile(L"app.exe", CODE_UTF8, L"needle\nneedle\n"); + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + CGrepAgent agent; + CGrepEnumKeys keys; + // *.*;!.*\.obj$;!.*\.exe$ → 正規表現モードで .obj/.exe を除外 + keys.SetFileKeys(L"*.*;!.*\\.obj$;!.*\\.exe$", /*bExcludeFileRegex=*/true); + CGrepEnumFiles cExAbsFiles; + CGrepEnumFolders cExAbsFolders; + CNativeW cmemMessage; + int nHit = 0; + + const std::vector vPaths = { m_temp->Root().wstring() }; + const int rc = agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ L"needle", sOpt, gOpt }, + SGrepEnumContext{ keys, cExAbsFiles, cExAbsFolders }, + vPaths, + cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(4, nHit); // .cpp 2 ファイル × 2 件(.obj/.exe は除外) +} + +/*! + * @brief RunParallelGrep のベースフォルダーヘッダーが重複しない (2: ヘッダー出力一致検証) + * @remark bGrepOutputBaseFolder=true, bGrepSeparateFolder=false で複数ファイルを検索したとき、 + * ベースフォルダーヘッダー行(■"...")が出力に 1 回だけ現れることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_BaseFolderHeader_NoDuplicate) +{ + m_temp->WriteEncodedTextFile(L"a.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"b.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"c.txt", CODE_UTF8, L"needle\n"); + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepOutputBaseFolder = true; + gOpt.bGrepSeparateFolder = false; + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CGrepEnumFiles cExAbsFiles; + CGrepEnumFolders cExAbsFolders; + CNativeW cmemMessage; + int nHit = 0; + + const std::wstring rootPath = m_temp->Root().wstring(); + const std::vector vPaths = { rootPath }; + const int rc = agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ L"needle", sOpt, gOpt }, + SGrepEnumContext{ keys, cExAbsFiles, cExAbsFolders }, + vPaths, + cmemMessage, nHit); + + ASSERT_EQ(0, rc); + EXPECT_EQ(3, nHit); + + // ベースフォルダーヘッダー(■"")は 1 回だけ出力されること + const std::wstring output(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + const std::wstring baseFolderHeader = L"■\"" + rootPath + L"\""; + int headerCount = 0; + size_t pos = 0; + while ((pos = output.find(baseFolderHeader, pos)) != std::wstring::npos) { + ++headerCount; + pos += baseFolderHeader.size(); + } + EXPECT_EQ(1, headerCount) << "ベースフォルダーヘッダーが重複して出力されている"; +} + +/*! + * @brief RunParallelGrep のベース/フォルダーヘッダーが重複しない (2: bGrepSeparateFolder=true) + * @remark bGrepOutputBaseFolder=true, bGrepSeparateFolder=true で複数ファイルを検索したとき、 + * ◎"..."(ベース)と ■\r\n(ルート直下フォルダー)が各 1 回だけ出ることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_SeparateFolderHeader_NoDuplicate) +{ + m_temp->WriteEncodedTextFile(L"x.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"y.txt", CODE_UTF8, L"needle\n"); + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepOutputBaseFolder = true; + gOpt.bGrepSeparateFolder = true; + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CGrepEnumFiles cExAbsFiles; + CGrepEnumFolders cExAbsFolders; + CNativeW cmemMessage; + int nHit = 0; + + const std::wstring rootPath = m_temp->Root().wstring(); + const std::vector vPaths = { rootPath }; + const int rc = agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ L"needle", sOpt, gOpt }, + SGrepEnumContext{ keys, cExAbsFiles, cExAbsFolders }, + vPaths, + cmemMessage, nHit); + + ASSERT_EQ(0, rc); + EXPECT_EQ(2, nHit); + + const std::wstring output(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + + // ◎"" は 1 回だけ(ベースフォルダーヘッダー、bGrepSeparateFolder=true 時) + const std::wstring baseHeader = L"◎\"" + rootPath + L"\""; + int baseCount = 0; + size_t pos = 0; + while ((pos = output.find(baseHeader, pos)) != std::wstring::npos) { + ++baseCount; + pos += baseHeader.size(); + } + EXPECT_EQ(1, baseCount) << "ベースフォルダーヘッダー(◎)が重複して出力されている"; + + // ■\r\n(ルート直下 = 空フォルダー名)は 1 回だけ + const std::wstring folderHeader = L"■\r\n"; + int folderCount = 0; + pos = 0; + while ((pos = output.find(folderHeader, pos)) != std::wstring::npos) { + ++folderCount; + pos += folderHeader.size(); + } + EXPECT_EQ(1, folderCount) << "フォルダーヘッダー(■\\r\\n)が重複して出力されている"; +} + +// ============================================================================= +// 補助ヘルパー(セクション 10–12 共通) +// ============================================================================= + +namespace { + +/*! 複数パスへの RunParallelGrep を最小限の引数で呼ぶラッパー */ +int RunParallelGrepOnPaths( + CGrepAgent& agent, + CGrepEnumKeys& keys, + const std::vector& vPaths, + std::wstring_view key, + const SSearchOption& sOpt, + const SGrepOption& gOpt, + CNativeW& cmemMessage, + int& nHit) +{ + CGrepEnumFiles cExAbsFiles; + CGrepEnumFolders cExAbsFolders; + return agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ std::wstring(key).c_str(), sOpt, gOpt }, + SGrepEnumContext{ keys, cExAbsFiles, cExAbsFolders }, + vPaths, + cmemMessage, nHit); +} + +/*! str 中に sub が何回現れるかを数える */ +int CountOccurrences(const std::wstring& str, const std::wstring& sub) +{ + int count = 0; + size_t pos = 0; + while ((pos = str.find(sub, pos)) != std::wstring::npos) { + ++count; + pos += sub.size(); + } + return count; +} + +} // namespace + +// ============================================================================= +// 10. BuildGrepHeader / BuildGrepFooter の分岐網羅 +// ============================================================================= + +/*! + * @brief BuildGrepHeader: リテラルキーのヘッダー基本形式 + * @remark キーが引用符で囲まれ、ファイルパターン・フォルダーが出力に含まれ、末尾が \r\n\r\n で終わることを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_LiteralKeyBasic) +{ + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepSubFolder = false; + + const auto result = CGrepAgent::BuildGrepHeader( + L"needle", L"*.txt", L"C:\\test", sOpt, gOpt); + const std::wstring out(result.GetStringPtr(), result.GetStringLength()); + + EXPECT_NE(out.find(L"\"needle\""), std::wstring::npos) << "キーが引用符で囲まれて出力されること"; + EXPECT_NE(out.find(L"*.txt"), std::wstring::npos) << "ファイルパターンが出力に含まれること"; + EXPECT_NE(out.find(L"C:\\test"), std::wstring::npos) << "フォルダーが出力に含まれること"; + ASSERT_GE(out.size(), 4u); + EXPECT_EQ(out.substr(out.size() - 4), std::wstring(L"\r\n\r\n")) << "末尾が \\r\\n\\r\\n で終わること"; +} + +/*! + * @brief BuildGrepHeader: 空キーはファイル一覧モード(キー引用符なし) + * @remark 空キーの場合はキー行が引用符なしで出力され、ファイル・フォルダーは通常通り出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_EmptyKeyIsFileListMode) +{ + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepSubFolder = false; + + const auto result = CGrepAgent::BuildGrepHeader( + L"", L"*.cpp", L"C:\\src", sOpt, gOpt); + const std::wstring out(result.GetStringPtr(), result.GetStringLength()); + + EXPECT_EQ(out.find(L"\"\""), std::wstring::npos) << "空キーは引用符で囲まれない"; + EXPECT_NE(out.find(L"*.cpp"), std::wstring::npos) << "ファイルパターンは出力される"; + EXPECT_NE(out.find(L"C:\\src"), std::wstring::npos) << "フォルダーは出力される"; +} + +/*! + * @brief BuildGrepHeader: 置換文字列・クリップボード貼り付けの分岐 + * @remark bGrepReplace=true 時に置換文字列が出力に含まれ、bGrepPaste=true 時は置換文字列が出力されないことを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_ReplaceAndPasteVariants) +{ + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepReplace = true; + gOpt.bGrepPaste = false; + + // 置換文字列あり + { + const auto result = CGrepAgent::BuildGrepHeader( + L"needle", L"*.txt", L"C:\\t", sOpt, gOpt, L"replacement"); + const std::wstring out(result.GetStringPtr(), result.GetStringLength()); + EXPECT_NE(out.find(L"\"replacement\""), std::wstring::npos) + << "置換文字列が引用符で囲まれて出力される"; + } + + // クリップボードから貼り付け(置換文字列は出力されない) + { + gOpt.bGrepPaste = true; + const auto result = CGrepAgent::BuildGrepHeader( + L"needle", L"*.txt", L"C:\\t", sOpt, gOpt, L"replacement"); + const std::wstring out(result.GetStringPtr(), result.GetStringLength()); + EXPECT_EQ(out.find(L"\"replacement\""), std::wstring::npos) + << "bGrepPaste=true のとき置換文字列は出力されない"; + } +} + +/*! + * @brief BuildGrepHeader: 各検索・出力オプションの分岐が出力に影響すること + * @remark 単語単位・文字コード固定・lineType・FileOnly・正規表現の各 ON/OFF で出力が変化することを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_SearchAndOutputOptionBranches) +{ + // 単語単位 ON: 追加文言で出力が長くなること + { + const auto base = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", + MakeSearchOption(false, false, false), MakeGrepOption()); + const auto word = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", + MakeSearchOption(false, false, true), MakeGrepOption()); + EXPECT_GT(word.GetStringLength(), base.GetStringLength()) + << "単語単位 ON のとき bWordOnly 文言が追加される"; + } + + // 文字コード固定(SJIS): 自動検出と異なる文字コード表記が出力されること + { + auto gAuto = MakeGrepOption(); gAuto.nGrepCharSet = CODE_AUTODETECT; + auto gSjis = MakeGrepOption(); gSjis.nGrepCharSet = CODE_SJIS; + const auto rAuto = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), gAuto); + const auto rSjis = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), gSjis); + const std::wstring sAuto(rAuto.GetStringPtr(), rAuto.GetStringLength()); + const std::wstring sSjis(rSjis.GetStringPtr(), rSjis.GetStringLength()); + EXPECT_NE(sAuto, sSjis) + << "自動検出と文字コード固定で文字コード表記が異なる"; + } + + // lineType=2(否ヒット行): lineType=1 と出力が異なること + { + auto g1 = MakeGrepOption(); g1.nGrepOutputLineType = 1; + auto g2 = MakeGrepOption(); g2.nGrepOutputLineType = 2; + const auto r1 = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), g1); + const auto r2 = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), g2); + const std::wstring s1(r1.GetStringPtr(), r1.GetStringLength()); + const std::wstring s2(r2.GetStringPtr(), r2.GetStringLength()); + EXPECT_NE(s1, s2) + << "lineType=1 と lineType=2 で異なる文言が出力される"; + } + + // bGrepOutputFileOnly=true: 文言追加で出力が長くなること + { + auto gBase = MakeGrepOption(); gBase.bGrepOutputFileOnly = false; + auto gFile = MakeGrepOption(); gFile.bGrepOutputFileOnly = true; + const auto rBase = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), gBase); + const auto rFile = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), gFile); + EXPECT_GT(rFile.GetStringLength(), rBase.GetStringLength()) + << "bGrepOutputFileOnly=true のとき ファイル毎最初のみ 文言が追加される"; + } + + // 正規表現 ON: 正規表現DLL文言で出力が長くなること + { + const auto rBase = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), MakeGrepOption()); + const auto rRegex = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(true, false), MakeGrepOption()); + EXPECT_GT(rRegex.GetStringLength(), rBase.GetStringLength()) + << "正規表現 ON のとき正規表現DLL文言が追加される"; + + // 置換・正規表現・lineType=0・貼り付けなし → 1行目のみ表示の分岐 + auto gRep = MakeGrepOption(); + gRep.bGrepReplace = true; + gRep.bGrepPaste = false; + gRep.nGrepOutputLineType = 0; + const auto rFirstLine = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(true, false), gRep, L"rep"); + EXPECT_GT(rFirstLine.GetStringLength(), 0); + } +} + +/*! + * @brief BuildGrepFooter: 通常検索と置換で書式が異なり、どちらもヒット数を含む + * @remark BuildGrepFooter(42, false) と BuildGrepFooter(42, true) がそれぞれ "42" を含み、 + * かつ互いに異なる文字列になることを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepFooter_SearchAndReplaceFormats) +{ + const auto rSearch = CGrepAgent::BuildGrepFooter(42, false); + const auto rReplace = CGrepAgent::BuildGrepFooter(42, true); + const std::wstring sSearch (rSearch .GetStringPtr(), rSearch .GetStringLength()); + const std::wstring sReplace(rReplace.GetStringPtr(), rReplace.GetStringLength()); + + EXPECT_NE(sSearch.find(L"42"), std::wstring::npos) << "通常フッターにヒット数が含まれる"; + EXPECT_NE(sReplace.find(L"42"), std::wstring::npos) << "置換フッターにヒット数が含まれる"; + EXPECT_NE(sSearch, sReplace) << "通常と置換でフッター文字列が異なる"; +} + +// ============================================================================= +// 11. FormatGrepResultLine の出力形式 +// ============================================================================= + +/*! + * @brief FormatGrepResultLine: style=1 + bGrepOutputBaseFolder=true で行頭に ・ が付き CRLF 終端 + * @remark ノーマルスタイルでベースフォルダー表示が ON のとき、先頭文字が ・ になり CRLF で終わることを確認する。 + */ +TEST_F(GrepRealFileTest, FormatGrepResultLine_NormalStyleWithFolderBullet) +{ + CNativeW out; + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputStyle = 1; + gOpt.bGrepOutputBaseFolder = true; + gOpt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + out, + L"foo.cpp", L"", + SGrepMatchInfo{ 1, 1, L"needle in a haystack", 20, 0, L"needle", 6 }, + gOpt); + + const std::wstring result(out.GetStringPtr(), out.GetStringLength()); + ASSERT_FALSE(result.empty()); + EXPECT_EQ(result[0], L'・') << "ベースフォルダー表示 ON のとき行頭は ・ (U+30FB)"; + ASSERT_GE(result.size(), 2u); + EXPECT_EQ(result.substr(result.size() - 2), std::wstring(L"\r\n")) << "CRLF 終端"; +} + +/*! + * @brief FormatGrepResultLine: style=2(WZ風)で行頭 ・(行番号,桁) 形式、ファイルパス非表示 + * @remark WZ風スタイルは ・(<行>,<桁>): の行頭形式になり、ファイルパスは含まれないことを確認する。 + */ +TEST_F(GrepRealFileTest, FormatGrepResultLine_WZStyle) +{ + CNativeW out; + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputStyle = 2; + gOpt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + out, + L"bar.cpp", L"", + SGrepMatchInfo{ 5, 3, L"line content", 12, 0, L"line", 4 }, + gOpt); + + const std::wstring result(out.GetStringPtr(), out.GetStringLength()); + ASSERT_FALSE(result.empty()); + EXPECT_EQ(result[0], L'・') << "WZ風は行頭 ・"; + EXPECT_NE(result.find(L'('), std::wstring::npos) << "行番号を括弧付きで出力する"; + EXPECT_EQ(result.find(L"bar.cpp"), std::wstring::npos) + << "ファイルパスは FormatGrepResultLine では出力しない"; +} + +/*! + * @brief FormatGrepResultLine: style=3 + lineType=0 でマッチ部分のみ出力され EOL 付加を制御 + * @remark 結果のみモード・該当部分出力のとき、マッチ末尾が EOL でなければ \r\n を付加し、 + * EOL で終わる場合は余分な \r\n を付加しないことを確認する。 + */ +TEST_F(GrepRealFileTest, FormatGrepResultLine_ResultOnlyMatchPartEol) +{ + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputStyle = 3; + gOpt.nGrepOutputLineType = 0; + + // Case 1: マッチ末尾が EOL でない → \r\n が追加される + { + CNativeW out; + CGrepAgent::FormatGrepResultLine( + out, L"f.cpp", L"", + SGrepMatchInfo{ 1, 1, L"needle in haystack\r\n", 20, 2, L"needle", 6 }, + gOpt); + const std::wstring result(out.GetStringPtr(), out.GetStringLength()); + EXPECT_EQ(result, std::wstring(L"needle\r\n")) + << "マッチ末尾が EOL でない場合 \\r\\n を追加する"; + } + + // Case 2: マッチ末尾が \n → bEOL=false → 余分な \r\n を追加しない + { + CNativeW out; + const wchar_t matchData[] = L"needle\n"; + CGrepAgent::FormatGrepResultLine( + out, L"f.cpp", L"", + SGrepMatchInfo{ 1, 1, matchData, 7, 1, matchData, 7 }, + gOpt); + const std::wstring result(out.GetStringPtr(), out.GetStringLength()); + EXPECT_EQ(result, std::wstring(L"needle\n")) + << "マッチ末尾が \\n の場合 \\r\\n を追加しない"; + } +} + +// ============================================================================= +// 12. RunParallelGrep / DoGrepFileWorker 分岐追加 +// ============================================================================= + +/*! + * @brief RunParallelGrep: ワーカースレッドローカルの正規表現コンパイル経路 + * @remark 正規表現キーを渡したとき、ワーカースレッド内で正規表現がコンパイルされ正しく検索されることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_RegexKeySearch) +{ + m_temp->WriteEncodedTextFile(L"regex.txt", CODE_UTF8, + L"id=001\nid=042\nname=alice\nid=999\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CNativeW cmemMessage; + int nHit = 0; + + const auto sOpt = MakeSearchOption(/*regex=*/true, /*caseSensitive=*/true); + const auto gOpt = MakeGrepOption(); + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"^id=\\d+$", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(3, nHit); +} + +/*! + * @brief RunParallelGrep: 単語単位検索分岐(CreateWordList / SearchStringWord) + * @remark bWordOnly=true で "word" を検索したとき、単独の "word" のみヒットし + * "words" 内の "word" はマッチしないことを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_WordOnlySearch) +{ + m_temp->WriteEncodedTextFile(L"word.txt", CODE_UTF8, L"word\nwords\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CNativeW cmemMessage; + int nHit = 0; + + const auto sOpt = MakeSearchOption(false, false, /*wordOnly=*/true); + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputLineType = 1; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"word", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(1, nHit) << "\"words\" 内の \"word\" は単語境界なしでマッチしない"; +} + +/*! + * @brief RunParallelGrep: lineType=0(該当部分)で 1 行内の複数ヒットをカウント + * @remark nGrepOutputLineType=0 のとき 1 行内の全マッチを数えるため、 + * 2 回 needle が現れる行は 2 件となることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_MatchPartCountsAllHitsInLine) +{ + m_temp->WriteEncodedTextFile(L"multi.txt", CODE_UTF8, L"needle needle\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CNativeW cmemMessage; + int nHit = 0; + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputLineType = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(2, nHit) << "1 行に 2 件のマッチがあれば lineType=0 では 2 件カウント"; +} + +/*! + * @brief RunParallelGrep: lineType=2(否ヒット行)の件数と出力内容 + * @remark nGrepOutputLineType=2 のとき、ヒットしなかった行のみカウント・出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_NoHitLineOutput) +{ + m_temp->WriteEncodedTextFile(L"nohit.txt", CODE_UTF8, + L"target\nother\nelse\ntarget\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CNativeW cmemMessage; + int nHit = 0; + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputLineType = 2; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"target", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(2, nHit) << "否ヒット行は 2 行(other・else)"; + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_NE(out.find(L"other"), std::wstring::npos) << "否ヒット行 other が出力される"; + EXPECT_NE(out.find(L"else"), std::wstring::npos) << "否ヒット行 else が出力される"; +} + +/*! + * @brief RunParallelGrep: bGrepOutputFileOnly=true でファイル毎最初の 1 件のみ + * @remark 5 行すべてヒットするファイルを検索したとき、bGrepOutputFileOnly=true では 1 件のみ返ることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_OutputFileOnly) +{ + m_temp->WriteEncodedTextFile(L"fileonly.txt", CODE_UTF8, + L"needle\nneedle\nneedle\nneedle\nneedle\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + CNativeW cmemMessage; + int nHit = 0; + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepOutputFileOnly = true; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(1, nHit) << "bGrepOutputFileOnly=true ではファイル毎最初の 1 件のみ"; +} + +/*! + * @brief RunParallelGrep: 複数検索パスでパスごとにヘッダー履歴がリセットされる + * @remark 2 つの独立ディレクトリを vPaths に渡したとき、各パスのベースフォルダーヘッダーが + * それぞれ 1 回ずつ出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_MultiplePathsHeaderResetPerPath) +{ + GrepTempDir dir1, dir2; + dir1.WriteEncodedTextFile(L"a.txt", CODE_UTF8, L"needle\n"); + dir2.WriteEncodedTextFile(L"b.txt", CODE_UTF8, L"needle\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + auto gOpt = MakeGrepOption(); + gOpt.bGrepOutputBaseFolder = true; + gOpt.bGrepSeparateFolder = false; + + const auto sOpt = MakeSearchOption(false, false); + const std::vector vPaths = { + dir1.Root().wstring(), + dir2.Root().wstring() + }; + CNativeW cmemMessage; + int nHit = 0; + CGrepEnumFiles cExAbsFiles; + CGrepEnumFolders cExAbsFolders; + + const int rc = agent.RunParallelGrep( + nullptr, nullptr, + SGrepSearchParams{ L"needle", sOpt, gOpt }, + SGrepEnumContext{ keys, cExAbsFiles, cExAbsFolders }, + vPaths, + cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(2, nHit); + + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_EQ(1, CountOccurrences(out, L"■\"" + dir1.Root().wstring() + L"\"")) + << "dir1 のヘッダーが 1 回のみ出力される"; + EXPECT_EQ(1, CountOccurrences(out, L"■\"" + dir2.Root().wstring() + L"\"")) + << "dir2 のヘッダーが 1 回のみ出力される"; +} + +/*! + * @brief RunParallelGrep: ファイルのない空ディレクトリで 0 件・正常終了しハングしない + * @remark ファイルが存在しない一時ディレクトリを検索したとき、タスク 0 件バッチの早期リターンしハングしないことを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_EmptyDirectoryNoHang) +{ + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + CNativeW cmemMessage; + int nHit = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(0, nHit); +} + +/*! + * @brief RunParallelGrep: # 除外フォルダーにより DoGrepTreeEnumerate でフォルダーが事前除外される + * @remark #excluded キーで excluded/ フォルダーを除外したとき、keep/ のファイルのみがヒットすることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_ExcludeFolderHashKey) +{ + m_temp->WriteEncodedTextFile(L"excluded/a.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"keep/b.txt", CODE_UTF8, L"needle\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt;#excluded"); + auto gOpt = MakeGrepOption(); + gOpt.bGrepSubFolder = true; + + const auto sOpt = MakeSearchOption(false, false); + CNativeW cmemMessage; + int nHit = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(1, nHit) << "excluded/ は #excluded で除外されるため keep/ の 1 件のみ"; +} + +/*! + * @brief RunParallelGrep: bGrepSeparateFolder=true でサブフォルダー名ヘッダーが 1 回のみ出力される + * @remark sub/ に 3 件ヒットがあるとき、フォルダーヘッダー ■"" が 1 回だけ出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_SeparateFolderSubfolderNameHeader) +{ + m_temp->WriteEncodedTextFile(L"sub/c.txt", CODE_UTF8, + L"needle\nneedle\nneedle\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + auto gOpt = MakeGrepOption(); + gOpt.bGrepSubFolder = true; + gOpt.bGrepSeparateFolder = true; + + const auto sOpt = MakeSearchOption(false, false); + CNativeW cmemMessage; + int nHit = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(3, nHit); + + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + const std::wstring subPath = (m_temp->Root() / L"sub").wstring(); + EXPECT_EQ(1, CountOccurrences(out, L"■\"" + subPath + L"\"")) + << "sub フォルダーヘッダーが 1 回のみ出力される"; +} + +/*! + * @brief RunParallelGrep: 空キーはファイル一覧モードで 1 ファイル 1 件・ファイル名出力 + * @remark 空キーの場合、ファイル内容を検索せず 1 ファイルあたり 1 件としてファイル名を出力することを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_EmptyKeyListsFiles) +{ + m_temp->WriteEncodedTextFile(L"list.txt", CODE_UTF8, L"content\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + CNativeW cmemMessage; + int nHit = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(1, nHit) << "空キーはファイル 1 件につき 1 カウント"; + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_NE(out.find(L"list.txt"), std::wstring::npos) << "ファイル名が出力に含まれる"; +} + +/*! + * @brief RunParallelGrep: ヒット 0 件で rc=0・正常終了 + * @remark マッチしないキーで検索したとき、戻り値 0・ヒット数 0 で正常終了することを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_NoHitsReturnsZero) +{ + m_temp->WriteEncodedTextFile(L"empty.txt", CODE_UTF8, L"no match here\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + CNativeW cmemMessage; + int nHit = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(0, nHit); +} + +/*! + * @brief RunParallelGrep: WZ風スタイル(style=2)でファイルヘッダーが 1 回のみ出力される + * @remark 3 件ヒットするファイルを WZ風スタイルで検索したとき、■"" ヘッダーが 1 回だけ出ることを確認する。 + */ +TEST_F(GrepRealFileTest, RunParallelGrep_WZStyleFileHeaderOnce) +{ + m_temp->WriteEncodedTextFile(L"wz.txt", CODE_UTF8, + L"needle\nneedle\nneedle\n"); + + CGrepAgent agent; + CGrepEnumKeys keys; + keys.SetFileKeys(L"*.txt"); + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputStyle = 2; + gOpt.bGrepOutputBaseFolder = false; + gOpt.bGrepSeparateFolder = false; + + const auto sOpt = MakeSearchOption(false, false); + CNativeW cmemMessage; + int nHit = 0; + const std::vector vPaths = { m_temp->Root().wstring() }; + + const int rc = RunParallelGrepOnPaths(agent, keys, vPaths, L"needle", sOpt, gOpt, cmemMessage, nHit); + + EXPECT_EQ(0, rc); + EXPECT_EQ(3, nHit); + + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_EQ(1, CountOccurrences(out, L"■\"")) + << "WZ風ファイルヘッダーが 1 回のみ出力される"; +} + +// ============================================================================= +// 13. DoGrepFileWorker / FormatGrepResultLine / BuildGrepHeader 未カバー分割 +// ============================================================================= + +/*! + * @brief DoGrepFileWorker: 存在しないファイルは CError_FileOpen を捕捉し 0 件(エラーメッセージ + * @remark FileOpen 失敗時に catch(CError_FileOpen) 経路が開き、戻り値 0 と + * フルパスを含むエラーメッセージが結果バッファに追記されることを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_FileOpenErrorReturnsZeroWithMessage) +{ + const std::filesystem::path path = m_temp->Sub(L"not_exist.txt"); // 作成しない + + SGrepFileTask task; + task.fullPath = path.wstring(); + task.fileName = path.filename().wstring(); + task.baseFolder = m_temp->Root().wstring(); + task.folder = task.baseFolder; + task.relPath = task.fileName; + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(); + + CSearchStringPattern pattern; + ASSERT_TRUE(pattern.SetPattern(nullptr, L"needle", 6, sOpt, nullptr)); + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + std::atomic cancel{ false }; + + CGrepAgent agent; + const int hits = agent.DoGrepFileWorker( + SGrepSearchParams{ L"needle", sOpt, gOpt }, + task, + nullptr, pattern, cancel, cmemMessage, cUnicodeBuffer); + + EXPECT_EQ(0, hits) << "ファイルオープン失敗は 0 件で正常復帰する"; + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_NE(out.find(path.wstring()), std::wstring::npos) + << "エラーメッセージに対象フルパスが含まれる"; +} + +/*! + * @brief DoGrepFileWorker: 事前キャンセル済みフラグで走査前に -1 を返す + * @remark FileOpen 直後のキャンセルチェック(行ループ前)で即時 -1 を返すことを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_PreCancelledReturnsMinusOne) +{ + const auto path = m_temp->WriteEncodedTextFile(L"pre.txt", CODE_UTF8, L"needle\n"); + + CGrepAgent agent; + std::atomic cancel{ true }; // 走査開始前にキャンセル済み + + EXPECT_EQ(-1, RunGrepFileWorker(agent, path, L"needle", + MakeSearchOption(false, false), MakeGrepOption(), cancel)); +} + +/*! + * @brief DoGrepFileWorker: 空キー時の出力スタイル書式分割マトリクス + * @remark 空キー(ファイル一覧モード)で style=1/2/3 × ベースフォルダー/フォルダー毎表示の + * 組み合わせごとに正しい行頭書式(●/■/■ /装飾なし)が選択されることを確認する。 + * 文字コードは固定(CODE_UTF8)として自動判定分岐を踏まずに書式分岐のみを検証する。 + */ +TEST_F(GrepRealFileTest, FileWorker_EmptyKeyOutputStyleVariants) +{ + const auto path = m_temp->WriteEncodedTextFile(L"name.txt", CODE_UTF8, L"x\n"); + + const auto run = [&](int style, bool baseFolder, bool sepFolder) -> std::wstring { + SGrepFileTask task; + task.fullPath = path.wstring(); + task.fileName = path.filename().wstring(); + task.baseFolder = m_temp->Root().wstring(); + task.folder = task.baseFolder; + task.relPath = task.fileName; + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(CODE_UTF8); // 文字コード固定で自動判定を回避 + gOpt.nGrepOutputStyle = style; + gOpt.bGrepOutputBaseFolder = baseFolder; + gOpt.bGrepSeparateFolder = sepFolder; + + CSearchStringPattern pattern; + pattern.SetPattern(nullptr, L"", 0, sOpt, nullptr); // 空キーでは未使用 + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + std::atomic cancel{ false }; + CGrepAgent agent; + const int hits = agent.DoGrepFileWorker( + SGrepSearchParams{ L"", sOpt, gOpt }, + task, + nullptr, pattern, cancel, cmemMessage, cUnicodeBuffer); + EXPECT_EQ(1, hits) << "style=" << style + << " base=" << baseFolder << " sep=" << sepFolder; + return std::wstring(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + }; + + // style=1 + ベースフォルダー表示: 行頭 ・"(中黒 U+30FB) + { + const std::wstring out = run(1, true, false); + EXPECT_EQ(0u, out.find(L"・\"")) << "style=1/base のとき行頭は ・\""; + } + // style=2 + ベースフォルダー表示: 行頭 ■"(空白なし) + { + const std::wstring out = run(2, true, false); + EXPECT_EQ(0u, out.find(L"■\"")) << "style=2/base のとき行頭は ■\""; + } + // style=2 + フォルダー毎表示: 行頭 ◆" + { + const std::wstring out = run(2, false, true); + EXPECT_EQ(0u, out.find(L"◆\"")) << "style=2/sep のとき行頭は ◆\""; + } + // style=3 装飾なし: フルパス + CRLF のみ + { + const std::wstring out = run(3, false, false); + EXPECT_EQ(0u, out.find(path.wstring())) << "style=3 はフルパスから始まる"; + EXPECT_EQ(out.find(L"■"), std::wstring::npos); + EXPECT_EQ(out.find(L"・"), std::wstring::npos); + } +} + +/*! + * @brief FormatGrepResultLine: 該当行出力(lineType=1)の 2000 文字クランプ + * @remark style=1 で行本文が 2000 文字を超える場合、出力が 2000 文字に切り詰められることを確認する。 + */ +TEST_F(GrepRealFileTest, FormatGrepResultLine_LongLineIsTruncatedTo2000) +{ + const std::wstring longLine(3000, L'a'); + + CNativeW out; + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputStyle = 1; + gOpt.nGrepOutputLineType = 1; + + CGrepAgent::FormatGrepResultLine( + out, L"f.cpp", L"", + SGrepMatchInfo{ 1, 1, longLine.c_str(), static_cast(longLine.size()), 0, longLine.c_str(), 6 }, + gOpt); + + const std::wstring result(out.GetStringPtr(), out.GetStringLength()); + EXPECT_NE(result.find(std::wstring(2000, L'a')), std::wstring::npos) + << "2000 文字までは出力される"; + EXPECT_EQ(result.find(std::wstring(2001, L'a')), std::wstring::npos) + << "2001 文字以上は出力されない"; +} + +/*! + * @brief FormatGrepResultLine: 該当部分出力(lineType=0)の 2500 文字クランプと EOL 付加 + * @remark style=3 でもマッチ部分が 2500 文字を超える場合に切り詰められる。 + * 切り詰め後の末尾が改行でなく \r\n が付加されることを確認する。 + */ +TEST_F(GrepRealFileTest, FormatGrepResultLine_LongMatchIsTruncatedTo2500) +{ + const std::wstring longMatch(3000, L'b'); + + CNativeW out; + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputStyle = 3; + gOpt.nGrepOutputLineType = 0; + + CGrepAgent::FormatGrepResultLine( + out, L"f.cpp", L"", + SGrepMatchInfo{ 1, 1, longMatch.c_str(), static_cast(longMatch.size()), 0, longMatch.c_str(), static_cast(longMatch.size()) }, + gOpt); + + EXPECT_EQ(2502, out.GetStringLength()) + << "マッチ部分 2500 文字 + \\r\\n の合計 2502 文字"; + const std::wstring result(out.GetStringPtr(), out.GetStringLength()); + EXPECT_EQ(result.substr(result.size() - 2), std::wstring(L"\r\n")); +} + +/*! + * @brief BuildGrepHeader: nullptr 引数を空文字列として扱い、クラッシュしない + * @remark pszKey / pszFile / pszFolder に nullptr を渡しても既定値(空文字列)で + * 安全にヘッダーが生成されることを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_NullArgumentsAreHandled) +{ + const auto result = CGrepAgent::BuildGrepHeader( + nullptr, nullptr, nullptr, + MakeSearchOption(false, false), MakeGrepOption()); + + EXPECT_GT(result.GetStringLength(), 0) << "nullptr 引数でもヘッダーが生成される"; + ASSERT_GE(result.GetStringLength(), 4); + const std::wstring out(result.GetStringPtr(), result.GetStringLength()); + EXPECT_EQ(out.substr(out.size() - 4), std::wstring(L"\r\n\r\n")); +} + +// ============================================================================= +// 14. 未カバー else / catch 分岐の追加カバレッジ +// ============================================================================= + +/*! + * @brief DoGrepFileWorker: 空キー(自動判定)不存在ファイルで DetectError 分岐 + * @remark 空キー(ファイル一覧モード)かつ CODE_AUTODETECT のとき、存在しないファイルの + * 文字コード判定が CODE_ERROR となり、!IsValidCodeOrCPType 分岐で + * "(DetectError)" をコード名として出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_EmptyKeyDetectErrorOnMissingFile) +{ + const std::filesystem::path path = m_temp->Sub(L"missing.txt"); // 作成しない + + SGrepFileTask task; + task.fullPath = path.wstring(); + task.fileName = path.filename().wstring(); + task.baseFolder = m_temp->Root().wstring(); + task.folder = task.baseFolder; + task.relPath = task.fileName; + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(CODE_AUTODETECT); // 自動判定経路 + + CSearchStringPattern pattern; + pattern.SetPattern(nullptr, L"", 0, sOpt, nullptr); // 空キーでは未使用 + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + std::atomic cancel{ false }; + + CGrepAgent agent; + const int hits = agent.DoGrepFileWorker( + SGrepSearchParams{ L"", sOpt, gOpt }, + task, + nullptr, pattern, cancel, cmemMessage, cUnicodeBuffer); + + EXPECT_EQ(1, hits) << "空キーは判定エラーでもファイル 1 件としてカウントする"; + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_NE(out.find(L"(DetectError)"), std::wstring::npos) + << "判定エラー時はコード名に (DetectError) が出力される"; + EXPECT_NE(out.find(path.wstring()), std::wstring::npos) + << "ファイルパスが出力に含まれる"; +} + +/*! + * @brief DoGrepFileWorker: 正規表現のゼロ長マッチで matchlen 補正分岐を経て無限ループしない + * @remark lineType=0(該当部分出力)でゼロ長マッチを許す正規表現(b*)を与えると、 + * matchlen <= 0 の補正分岐(GetSizeOfChar による前進)が機能して + * 走査が正常終了することを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_RegexZeroLengthMatchAdvances) +{ + const auto path = m_temp->WriteEncodedTextFile(L"zero.txt", CODE_UTF8, L"xb\n"); + + CGrepAgent agent; + const auto sOpt = MakeSearchOption(/*regex=*/true, /*caseSensitive=*/true); + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputLineType = 0; // 該当部分出力(1 行内の複数マッチ継続ループ) + + const int hits = RunGrepFileWorker(agent, path, L"b*", sOpt, gOpt); + + // マッチ件数は正規表現エンジンのゼロ長マッチ仕様に依存するため、 + // 正常終了(ハングしない)と 1 件以上のマッチのみを検証する。 + EXPECT_GE(hits, 1); +} + +/*! + * @brief BuildGrepHeader: 英大文字小文字を区別する(bLoHiCase=true)分岐 + * @remark bLoHiCase の ON/OFF でヘッダー記述(区別する/区別しない)が切り替わることを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_CaseSensitiveBranch) +{ + const auto rIgnore = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", + MakeSearchOption(false, /*caseSensitive=*/false), MakeGrepOption()); + const auto rCase = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", + MakeSearchOption(false, /*caseSensitive=*/true), MakeGrepOption()); + + const std::wstring sIgnore(rIgnore.GetStringPtr(), rIgnore.GetStringLength()); + const std::wstring sCase (rCase.GetStringPtr(), rCase.GetStringLength()); + EXPECT_NE(sIgnore, sCase) + << "大文字小文字区別の ON/OFF でヘッダー記述が切り替わる"; +} + +/*! + * @brief BuildGrepHeader: 置換時(lineType=0 でも一致した箇所を出力)分岐 + * @remark bGrepReplace=false かつ nGrepOutputLineType=0 のとき、lineType=1 とは + * 異なる記述(STR_GREP_SHOW_MATCH_AREA)が出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, BuildGrepHeader_MatchAreaLineTypeZero) +{ + auto g0 = MakeGrepOption(); g0.nGrepOutputLineType = 0; + auto g1 = MakeGrepOption(); g1.nGrepOutputLineType = 1; + + const auto r0 = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), g0); + const auto r1 = CGrepAgent::BuildGrepHeader( + L"key", L"*.txt", L"C:\\t", MakeSearchOption(false, false), g1); + + const std::wstring s0(r0.GetStringPtr(), r0.GetStringLength()); + const std::wstring s1(r1.GetStringPtr(), r1.GetStringLength()); + EXPECT_NE(s0, s1) + << "lineType=0(一致した箇所)と lineType=1(一致した行)で記述が異なる"; +} + +// ============================================================================= +// 15. DoGrep 本体のスモークテスト(EditorTestSuite + stdout 経路) +// ============================================================================= + +namespace { + +/*! + * Windows 標準ハンドルの一時差し替え RAII ガード(test-cgrepagent-flow.cpp と同手法) + */ +class GrepStdHandleGuard { +public: + GrepStdHandleGuard(DWORD id, HANDLE replacement) + : m_id(id), m_saved(::GetStdHandle(id)) + { + ::SetStdHandle(m_id, replacement); + } + ~GrepStdHandleGuard() + { + ::SetStdHandle(m_id, m_saved); + } + GrepStdHandleGuard(const GrepStdHandleGuard&) = delete; + GrepStdHandleGuard& operator=(const GrepStdHandleGuard&) = delete; + +private: + DWORD m_id; + HANDLE m_saved; +}; + +/*! + * DoGrep を stdout モードで実行し、ヒット数と stdout 出力(ANSI文字列)を返す + * + * bGrepStdout=true にし AddTail は標準出力へ書き込む(GA-18 で実証済みの安全経路)。 + * 標準出力は一時ファイルへ差し替えて回収する。 + * bGrepCurFolder=true でカレントディレクトリ変更を抑止する。 + */ +DWORD RunDoGrepStdout( + const std::wstring& key, + const std::wstring& filePattern, + const std::wstring& folder, + std::string& capturedStdout, + bool bExcludeFileRegex = false) // → 追加 +{ + capturedStdout.clear(); + + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_stdout.txt"; + + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hTempFile == INVALID_HANDLE_VALUE) { + ADD_FAILURE() << "failed to create stdout capture file"; + return 0; + } + struct HandleDeleter { + void operator()(HANDLE h) const { + if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); + } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + + const CNativeW cmKey(key.c_str()); + const CNativeW cmRep(L""); + const CNativeW cmFile(filePattern.c_str()); + const CNativeW cmFolder(folder.c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepReplace = false; + gOpt.bGrepSubFolder = false; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder: カレントディレクトリを変更しない + bExcludeFileRegex // bGrepExcludeFileRegexp + ); + } // ← stdout 復帰 + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[8192] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + capturedStdout.assign(buf, dwRead); + + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + return hits; +} + +} // namespace + +/*! + * @brief DoGrep: stdout モードでの基本検索スモーク (PR #2459) + * @remark EditorTestSuite 環境で DoGrep 本体を実行し、ヘッダー組み立て・検索条件・除外表示・ + * サブフォルダー記述切り替え → RunParallelGrep → ヒット数出力の全経路。 + * headless で実走するため、ヒット数と stdout 出力が正しいことを確認する。 + */ +TEST_F(GrepRealFileTest, DoGrep_StdoutMode_BasicSearchSmoke) +{ + m_temp->WriteEncodedTextFile(L"hit.txt", CODE_UTF8, L"needle\nabc\nneedle\n"); + + std::string out; + const DWORD hits = RunDoGrepStdout( + L"needle", L"*.txt", m_temp->Root().wstring(), out); + + EXPECT_EQ(2u, hits) << "needle 2 件がヒットする"; + EXPECT_NE(out.find("needle"), std::string::npos) + << "stdout にヒット行が出力される"; + EXPECT_NE(out.find("hit.txt"), std::string::npos) + << "stdout にファイル名が出力される"; +} + +/*! + * @brief DoGrep: `!` 除外正規表現の事前検証と適用のスモーク (PR #2459) + * @remark 有効な `!` 除外パターンを与え、DoGrep 冒頭の事前検証ループ + * (InitRegexp → Compile 成功)を経た上で、キーのヒット・除外ファイル表示と + * ワーカーでの除外適用(skip 系 1 件除外)が行われることを確認する。 + */ +TEST_F(GrepRealFileTest, DoGrep_StdoutMode_ExcludeRegexSmoke) +{ + m_temp->WriteEncodedTextFile(L"keep.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"skip_me.txt", CODE_UTF8, L"needle\n"); + + std::string out; + const DWORD hits = RunDoGrepStdout( + L"needle", L"*.txt;!.*skip.*\\.txt$", m_temp->Root().wstring(), + out, /*bExcludeFileRegex=*/true); + + EXPECT_EQ(1u, hits) << "skip_me.txt は除外し keep.txt の 1 件のみ"; + EXPECT_NE(out.find("keep.txt"), std::string::npos) + << "keep.txt のヒットが出力される"; +} + +/*! + * @brief DoGrepFileWorker: 空キー(自動判定)実在ファイルで有効コードの Bracket 分岐 (GA-21) + * @remark 空キー(ファイル一覧モード)かつ CODE_AUTODETECT で、実在の UTF-8(BOM) ファイルの + * 文字コード判定が有効コードになり、IsValidCodeType 真の Bracket 表記が出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, FileWorker_EmptyKeyAutoDetectValidCodeBracket) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"detect.txt", CODE_UTF8, L"日本語テキスト\n", /*withBom=*/true); + + SGrepFileTask task; + task.fullPath = path.wstring(); + task.fileName = path.filename().wstring(); + task.baseFolder = m_temp->Root().wstring(); + task.folder = task.baseFolder; + task.relPath = task.fileName; + + const auto sOpt = MakeSearchOption(false, false); + const auto gOpt = MakeGrepOption(CODE_AUTODETECT); // 自動判定経路 + + CSearchStringPattern pattern; + pattern.SetPattern(nullptr, L"", 0, sOpt, nullptr); // 空キーでは未使用 + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + std::atomic cancel{ false }; + + CGrepAgent agent; + const int hits = agent.DoGrepFileWorker( + SGrepSearchParams{ L"", sOpt, gOpt }, + task, + nullptr, pattern, cancel, cmemMessage, cUnicodeBuffer); + + EXPECT_EQ(1, hits) << "空キーはファイル 1 件としてカウント"; + const std::wstring out(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()); + EXPECT_EQ(std::wstring::npos, out.find(L"(DetectError)")) + << "実在ファイルは判定エラーにならない"; + EXPECT_NE(std::wstring::npos, out.find(L"[")) + << "有効コードの Bracket 表記([UTF-8] 等)が出力される"; +} + +/*! + * @brief DoGrep: 置換モード(除外正規表現)サブフォルダーの直列パススモーク (GA-22) + * @remark bGrepReplace=true 経路(DoGrepTree → DoGrepReplaceFile)を実走し、 + * DoGrepTree の除外正規表現コンパイル・reExcl.Match フィルタ・サブフォルダー再帰を網羅する。 + * keep.txt と sub/child.txt は置換対象、skip_me.txt は ! 除外正規表現で除外される。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_ExcludeRegexAndSubfolderSmoke) +{ + m_temp->WriteEncodedTextFile(L"keep.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"skip_me.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"sub/child.txt", CODE_UTF8, L"needle\n"); + + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_replace_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + std::string out; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + + const CNativeW cmKey(L"needle"); + const CNativeW cmRep(L"REPLACED"); + const CNativeW cmFile(L"*.txt;!.*skip.*\\.txt$"); + const CNativeW cmFolder(m_temp->Root().wstring().c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepReplace = true; + gOpt.bGrepSubFolder = true; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder: カレントディレクトリを変更しない + true // bGrepExcludeFileRegexp=true(! 除外を正規表現扱い) + ); + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[8192] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + out.assign(buf, dwRead); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + + EXPECT_EQ(2u, hits) << "keep.txt と sub/child.txt の 2 件のみ置換(skip_me.txt は除外)"; + EXPECT_NE(out.find("keep.txt"), std::string::npos) << "keep.txt が出力される"; + EXPECT_NE(out.find("child.txt"), std::string::npos) << "サブフォルダーの child.txt が出力される(再帰)"; + EXPECT_EQ(out.find("skip_me"), std::string::npos) << "skip_me.txt は除外正規表現で除外され出力されない"; +} + +/*! + * @brief DoGrep 置換: 複数の除外正規表現が結合パターンとして適用される (GA-35) + * @remark 除外正規表現を 2 本与え、DoGrepTree の P4 結合("(?:p1)|(?:p2)")経路で + * 両パターンとも有効に除外されることを確認する(1 本のみの GA-22 では + * 結合の "|" 連結コードが実行されないため本テストで補完する)。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_MultipleExcludeRegexCombined) +{ + m_temp->WriteEncodedTextFile(L"keep.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"skip_me.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"app.log", CODE_UTF8, L"needle\n"); + + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_multiexcl_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + std::string out; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + + const CNativeW cmKey(L"needle"); + const CNativeW cmRep(L"REPLACED"); + const CNativeW cmFile(L"*.*;!.*skip.*\\.txt$;!.*\\.log$"); + const CNativeW cmFolder(m_temp->Root().wstring().c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepReplace = true; + gOpt.bGrepSubFolder = false; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder + true // bGrepExcludeFileRegexp=true(! 除外を正規表現扱い) + ); + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[8192] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + out.assign(buf, dwRead); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + + EXPECT_EQ(1u, hits) << "keep.txt の 1 件のみ置換(skip_me.txt / app.log は結合除外パターンで除外)"; + EXPECT_NE(out.find("keep.txt"), std::string::npos) << "keep.txt が出力される"; + EXPECT_EQ(out.find("skip_me"), std::string::npos) << "skip_me.txt は 1 本目の除外パターンで除外"; + EXPECT_EQ(out.find("app.log"), std::string::npos) << "app.log は 2 本目の除外パターンで除外"; +} + +// ============================================================================= +// Grep 置換経路の追加検証(GA-23 ~ GA-25) +// ============================================================================= + +namespace { + +//! ファイルの生バイトを読み出す(置換後内容の検証用)。 +//! BOM の有無や EOL に依存しないよう、バイト列としてそのまま返す。 +std::string ReadAllBytes(const std::filesystem::path& path) +{ + std::ifstream is(path, std::ios::binary); + if (!is) return {}; + is.seekg(0, std::ios::end); + const std::streamoff len = is.tellg(); + if (len <= 0) return {}; + std::string buf(static_cast(len), '\0'); + is.seekg(0, std::ios::beg); + is.read(&buf[0], len); + buf.resize(static_cast(is.gcount())); + return buf; +} + +//! DoGrep を置換モードで実行する。 +//! stdout はテンポラリへ迂回してコンソールを汚さない(GA-22 と同じ手法)。 +//! ファイル書き換え(CWriteData)は stdout 経路とは独立に実行されるため、 +//! 置換後のファイル内容はこの呼び出し後に各テストが読み出して検証できる。 +DWORD RunDoGrepReplace( + const std::wstring& key, + const std::wstring& replaceTo, + const std::wstring& filePattern, + const std::wstring& folder, + bool bRegex, + bool bBackup, + bool bSubFolder) +{ + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_replace_stdout.txt"; + + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hTempFile == INVALID_HANDLE_VALUE) { + ADD_FAILURE() << "failed to create stdout capture file"; + return 0; + } + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + + const CNativeW cmKey(key.c_str()); + const CNativeW cmRep(replaceTo.c_str()); + const CNativeW cmFile(filePattern.c_str()); + const CNativeW cmFolder(folder.c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + sOpt.bRegularExp = bRegex; + + SGrepOption gOpt; + gOpt.bGrepReplace = true; + gOpt.bGrepSubFolder = bSubFolder; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + gOpt.bGrepBackup = bBackup; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder: カレントディレクトリを変更しない + false // bGrepExcludeFileRegexp + ); + } // ← stdout 復帰 + + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + return hits; +} + +} // namespace + +/*! + * @brief DoGrep 置換: リテラル置換が実ファイルへ書き込まれる (GA-23) + * @remark bGrepReplace=true の直行経路(DoGrepTree → DoGrepReplaceFile → CWriteData)で、 + * 対象ファイルの内容が実際に needle → REPLACED へ書き換えられることを検証する。 + * GA-22 は stdout(ヒットファイル名)のみを見ていたため、書き込み結果を別途確認する。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_LiteralWritesReplacedContent) +{ + const auto path = m_temp->WriteEncodedTextFile(L"keep.txt", CODE_UTF8, L"needle\n"); + + const DWORD hits = RunDoGrepReplace( + L"needle", L"REPLACED", L"*.txt", m_temp->Root().wstring(), + /*bRegex=*/false, /*bBackup=*/false, /*bSubFolder=*/false); + + EXPECT_EQ(1u, hits) << "1 件が置換される"; + + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("REPLACED"), std::string::npos) + << "置換後の文字列がファイルへ書き込まれている"; + EXPECT_EQ(body.find("needle"), std::string::npos) + << "前の文字列は残らない"; +} + +/*! + * @brief DoGrep 置換: 正規表現置換が行内の全マッチを置換する (GA-24) + * @remark bRegularExp=true の置換経路(pRegexp->Replace)で、1 行内の複数マッチを + * すべて置換し、ヒット数が一致することを検証する(g 相当の行末まで一括置換)。 + * @note bregonig.dll に依存する(他の正規表現テストと同条件)。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_RegexReplacesAllInLine) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"multi.txt", CODE_UTF8, L"needle needle needle\n"); + + const DWORD hits = RunDoGrepReplace( + L"needle", L"XX", L"*.txt", m_temp->Root().wstring(), + /*bRegex=*/true, /*bBackup=*/false, /*bSubFolder=*/false); + + EXPECT_EQ(3u, hits) << "行内 3 件すべてがヒット・置換される"; + + const std::string body = ReadAllBytes(path); + EXPECT_EQ(body.find("needle"), std::string::npos) + << "行内の全マッチが置換される"; + EXPECT_NE(body.find("XX"), std::string::npos) + << "置換後の文字列が書き込まれている"; +} + +/*! + * @brief DoGrep 置換: バックアップ有効時に .skrold が生成される (GA-25) + * @remark bGrepBackup=true のとき、元ファイルを .skrold へ退避し(元内容を保持)、 + * 元パスには置換後内容が書き込まれることを検証する。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_BackupCreatesSkrold) +{ + const auto path = m_temp->WriteEncodedTextFile(L"data.txt", CODE_UTF8, L"needle\n"); + + const DWORD hits = RunDoGrepReplace( + L"needle", L"REPLACED", L"*.txt", m_temp->Root().wstring(), + /*bRegex=*/false, /*bBackup=*/true, /*bSubFolder=*/false); + + EXPECT_EQ(1u, hits) << "1 件が置換される"; + + // 元パスには置換後内容 + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("REPLACED"), std::string::npos) + << "元パスに置換後内容が書き込まれている"; + EXPECT_EQ(body.find("needle"), std::string::npos) + << "元パスに前の文字列は残らない"; + + // バックアップ(.skrold)には元内容 + const std::filesystem::path backup = path.wstring() + L".skrold"; + ASSERT_TRUE(std::filesystem::exists(backup)) + << ".skrold バックアップが生成される"; + const std::string backupBody = ReadAllBytes(backup); + EXPECT_NE(backupBody.find("needle"), std::string::npos) + << ".skrold には前の内容が保持される"; + EXPECT_EQ(backupBody.find("REPLACED"), std::string::npos) + << ".skrold は置換前の内容である"; +} + +// ============================================================================= +// Grep 置換経路の全ギャップ検証(GA-26 ~ GA-33) +// ============================================================================= + +namespace { + +//! 置換テスト拡張の実行オプション。既定は最小構成。 +struct ReplaceRunParams { + std::wstring key; + std::wstring replaceTo; + std::wstring folder; + std::wstring filePattern = L"*.txt"; + bool bRegex = false; + bool bWordOnly = false; + bool bBackup = false; + bool bSubFolder = false; + bool bOutputFileOnly = false; + int nOutputLineType = 1; + ECodeType charSet = CODE_AUTODETECT; +}; + +//! DoGrep を置換モードで実行する(オプション構造体版)。 +//! stdout はテンポラリへ迂回してコンソールを汚さない(GA-22 と同じ手法)。 +DWORD RunDoGrepReplaceEx(const ReplaceRunParams& p) +{ + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_replace_ex_stdout.txt"; + + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hTempFile == INVALID_HANDLE_VALUE) { + ADD_FAILURE() << "failed to create stdout capture file"; + return 0; + } + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + + const CNativeW cmKey(p.key.c_str()); + const CNativeW cmRep(p.replaceTo.c_str()); + const CNativeW cmFile(p.filePattern.c_str()); + const CNativeW cmFolder(p.folder.c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + sOpt.bRegularExp = p.bRegex; + sOpt.bWordOnly = p.bWordOnly; + + SGrepOption gOpt; + gOpt.bGrepReplace = true; + gOpt.bGrepSubFolder = p.bSubFolder; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = p.charSet; + gOpt.nGrepOutputLineType = p.nOutputLineType; + gOpt.nGrepOutputStyle = 1; + gOpt.bGrepBackup = p.bBackup; + gOpt.bGrepOutputFileOnly = p.bOutputFileOnly; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder + false // bGrepExcludeFileRegexp + ); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + return hits; +} + +//! 文字列中の部分文字列の出現回数を数える(置換件数の内容検証用)。 +size_t CountByteOccurrences(const std::string& hay, const std::string& needle) +{ + if (needle.empty()) return 0; + size_t n = 0; + for (size_t pos = hay.find(needle); pos != std::string::npos; pos = hay.find(needle, pos + needle.size())) { + ++n; + } + return n; +} + +} // namespace + +/*! + * @brief DoGrep 置換: 単語のみ検索は部分一致を置換しない (GA-26) + * @remark bWordOnly=true の置換経路(SearchStringWord)で、独立語 "cat" のみ置換され、 + * 部分一致 "category" は置換されないことを検証する。 + * @note 置換パターンのコンパイル(SetPattern)に bregonig.dll が要る。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_WordOnlySkipsSubstring) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"word.txt", CODE_UTF8, L"cat category cat\n"); + + ReplaceRunParams p; + p.key = L"cat"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + p.bWordOnly = true; + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(2u, hits) << "独立語 cat の 2 件のみヒット・置換される"; + + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("category"), std::string::npos) + << "部分一致 category は置換されない"; + EXPECT_EQ(2u, CountByteOccurrences(body, "REPLACED")) + << "置換されたのは独立語の 2 件のみ"; +} + +/*! + * @brief DoGrep 置換: UTF-8 BOM 付きファイルは BOM を保持して書き戻す (GA-27) + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_Utf8BomPreserved) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"bom8.txt", CODE_UTF8, L"needle\n", /*withBom=*/true); + + ReplaceRunParams p; + p.key = L"needle"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(1u, hits); + + const std::string body = ReadAllBytes(path); + ASSERT_GE(body.size(), 3u); + EXPECT_EQ(0xEF, static_cast(body[0])); // UTF-8 BOM が保持される + EXPECT_EQ(0xBB, static_cast(body[1])); + EXPECT_EQ(0xBF, static_cast(body[2])); + EXPECT_NE(body.find("REPLACED"), std::string::npos); + EXPECT_EQ(body.find("needle"), std::string::npos); +} + +/*! + * @brief DoGrep 置換: UTF-16LE ファイルは BOM・エンコードを保持して書き戻す (GA-28) + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_Utf16LePreserved) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"u16.txt", CODE_UNICODE, L"needle\n", /*withBom=*/true); + + ReplaceRunParams p; + p.key = L"needle"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(1u, hits); + + const std::string body = ReadAllBytes(path); + ASSERT_GE(body.size(), 2u); + EXPECT_EQ(0xFF, static_cast(body[0])); // UTF-16LE BOM が保持される + EXPECT_EQ(0xFE, static_cast(body[1])); + // "REPLACED" が UTF-16LE(各文字の後ろに 0x00)で書き込まれている + const char rep16[] = { 'R',0,'E',0,'P',0,'L',0,'A',0,'C',0,'E',0,'D',0 }; + EXPECT_NE(body.find(std::string(rep16, sizeof(rep16))), std::string::npos) + << "置換後文字列が UTF-16LE で書き込まれている"; +} + +/*! + * @brief DoGrep 置換: SJIS 日本語の置換往復(再 Grep で確認) (GA-29) + * @remark SJIS(BOM なし)の日本語ファイルを置換し、置換後ファイルを SJIS で再検索して + * 置換後語がヒット・置換前語が 0 件になることで、エンコード往復の健全性を確認する。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_SjisJapaneseRoundTrip) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"sjis.txt", CODE_SJIS, L"東京\n"); + + ReplaceRunParams p; + p.key = L"東京"; p.replaceTo = L"大阪"; p.folder = m_temp->Root().wstring(); + p.charSet = CODE_SJIS; + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(1u, hits); + + // 置換後ファイルを SJIS で再検索して往復を確認する。 + CGrepAgent agent; + EXPECT_EQ(1, RunGrepFileWorker(agent, path, L"大阪", + MakeSearchOption(false, false), MakeGrepOption(CODE_SJIS))) + << "置換後語が SJIS として正しく書き戻され再検索でヒットする"; + EXPECT_EQ(0, RunGrepFileWorker(agent, path, L"東京", + MakeSearchOption(false, false), MakeGrepOption(CODE_SJIS))) + << "置換前語は残らない"; +} + +/*! + * @brief DoGrep 置換: bGrepOutputFileOnly でも全行が置換される (GA-30) + * @remark 出力(メッセージ)はファイル毎 1 回に抑制されるが、置換自体は全該当行に及ぶこと。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_OutputFileOnlyStillReplacesAll) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"multi.txt", CODE_UTF8, L"needle\nneedle\n"); + + ReplaceRunParams p; + p.key = L"needle"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + p.bOutputFileOnly = true; + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(2u, hits) << "出力抑制されても 2 行ともヒット・置換される"; + + const std::string body = ReadAllBytes(path); + EXPECT_EQ(body.find("needle"), std::string::npos); + EXPECT_EQ(2u, CountByteOccurrences(body, "REPLACED")) + << "全該当行が置換される"; +} + +/*! + * @brief DoGrep 置換: 否ヒット行タイプ(2)は置換では通常置換に正規化される (GA-31) + * @remark nGrepOutputLineType=2(否ヒット行)で置換を指示しても、DoGrep が 1 に正規化し、 + * 通常どおりヒット行を置換すること。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_NoHitLineTypeNormalized) +{ + const auto path = m_temp->WriteEncodedTextFile( + L"nht.txt", CODE_UTF8, L"needle\n"); + + ReplaceRunParams p; + p.key = L"needle"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + p.nOutputLineType = 2; // 置換モードでは 1 に正規化される + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(1u, hits) << "否ヒット行指定でも通常のヒット行置換として動作する"; + + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("REPLACED"), std::string::npos); + EXPECT_EQ(body.find("needle"), std::string::npos); +} + +/*! + * @brief DoGrep 置換: 書き込みファイル(.skrnew)が開けない場合は元が壊れない (GA-32) + * @remark 出力先 ".skrnew" と同名のディレクトリを先に作って書き込みオープンを失敗させる。 + * CError_WriteFileOpen 経路(STR_GREP_ERR_FILEWRITE)が通り、元ファイルは無変更のまま。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_WriteOpenFailureKeepsOriginal) +{ + const auto path = m_temp->WriteEncodedTextFile(L"data.txt", CODE_UTF8, L"needle\n"); + + // 出力先 .skrnew をディレクトリとして先取りし、ファイル書き込みを失敗させる。 + const std::filesystem::path skrnew = path.wstring() + L".skrnew"; + std::filesystem::create_directories(skrnew); + + ReplaceRunParams p; + p.key = L"needle"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + const DWORD hits = RunDoGrepReplaceEx(p); + + EXPECT_EQ(0u, hits) << "書き込みオープン失敗時はヒット確定前に中断する"; + + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("needle"), std::string::npos) + << "書き込み失敗時に元ファイルは無変更"; + EXPECT_EQ(body.find("REPLACED"), std::string::npos) + << "置換後内容は元ファイルへ書き込まれない"; +} + +/*! + * @brief DoGrep 置換: 元ファイルロックされていても元が壊れない (GA-33) + * @remark 元ファイルを共有読み取りのみ(削除・書き込み共有なし)で開いたまま置換を実行する。 + * 読み取り open 失敗 or 置換確定(Delete/MoveFile)失敗のいずれの分岐でも、 + * 元ファイルが破壊されない不変条件を検証する。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_LockedOriginalNotCorrupted) +{ + const auto path = m_temp->WriteEncodedTextFile(L"locked.txt", CODE_UTF8, L"needle\n"); + + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + // 共有は読み取りのみ(削除・書き込み共有を与えない)→ 置換確定の Delete/MoveFile が失敗するはず。 + std::unique_ptr hLock{ + ::CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL) + }; + ASSERT_NE(INVALID_HANDLE_VALUE, hLock.get()) << "ロック用ハンドルの確保に失敗"; + + ReplaceRunParams p; + p.key = L"needle"; p.replaceTo = L"REPLACED"; p.folder = m_temp->Root().wstring(); + (void)RunDoGrepReplaceEx(p); // ヒット数は分岐により値が変わり得るため見ない + + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("needle"), std::string::npos) + << "ロック中の元ファイルは破壊されない"; + EXPECT_EQ(body.find("REPLACED"), std::string::npos) + << "ロック中は置換後内容で上書きされない"; +} + +// ============================================================================= +// Grep 置換「貼り付けモード」検証(GA-34) +// ============================================================================= + +namespace { + +//! クリップボードに CF_UNICODETEXT を積む(貼り付けモード検証用)。 +void SetClipboardUnicodeText(HWND hwnd, const std::wstring& s) +{ + if (!::OpenClipboard(hwnd)) { ADD_FAILURE() << "OpenClipboard failed"; return; } + ::EmptyClipboard(); + const size_t bytes = (s.size() + 1) * sizeof(wchar_t); + if (HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes)) { + if (void* p = ::GlobalLock(hMem)) { + memcpy(p, s.c_str(), bytes); + ::GlobalUnlock(hMem); + ::SetClipboardData(CF_UNICODETEXT, hMem); // 所有権はクリップボードへ移る + } + } + ::CloseClipboard(); +} + +//! クリップボードを空にする(後続テストへ影響させない)。 +void ClearClipboard(HWND hwnd) +{ + if (::OpenClipboard(hwnd)) { ::EmptyClipboard(); ::CloseClipboard(); } +} + +} // namespace + +/*! + * @brief DoGrep 置換: 貼り付けモードはクリップボード内容で置換する (GA-34) + * @remark bGrepPaste=true の経路(MyGetClipboardData → CClipboard::GetText(CF_UNICODETEXT))で、 + * 置換文字列フィールドではなくクリップボードの内容が使われることを検証する。 + * @note クリップボードはグローバル資源のため、テスト後に空にする。 + * クリップボードにアクセスできる環境(EditorTestSuite のウィンドウ)を前提とする。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_PasteUsesClipboard) +{ + const auto path = m_temp->WriteEncodedTextFile(L"paste.txt", CODE_UTF8, L"needle\n"); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + const HWND hwnd = pView->GetHwnd(); + + // クリップボードへ置換文字列を積む(置換フィールドには別値を入れ、そちらが使われないことを確認する) + SetClipboardUnicodeText(hwnd, L"REPLACED"); + + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_paste_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CGrepAgent agent; + const CNativeW cmKey(L"needle"); + const CNativeW cmRep(L"IGNORED"); // 貼り付けモードでは置換値として使われない + const CNativeW cmFile(L"*.txt"); + const CNativeW cmFolder(m_temp->Root().wstring().c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepReplace = true; + gOpt.bGrepPaste = true; // ← クリップボードから置換文字列を取得 + gOpt.bGrepSubFolder = false; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder + false // bGrepExcludeFileRegexp + ); + } // ← stdout 復帰 + + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + ClearClipboard(hwnd); + + EXPECT_EQ(1u, hits) << "1 件が置換される"; + + const std::string body = ReadAllBytes(path); + EXPECT_NE(body.find("REPLACED"), std::string::npos) + << "クリップボードの内容で置換される"; + EXPECT_EQ(body.find("IGNORED"), std::string::npos) + << "置換フィールドの値は貼り付けモードでは使われない"; + EXPECT_EQ(body.find("needle"), std::string::npos) + << "前の文字列は残らない"; +} + +// ============================================================================= +// 15. Grep 結果スナップショット(Phase 0-4) +// ============================================================================= + +namespace { + +/*! + * DoGrepFileWorker を 1 ファイルに対して実行し、ヒット数と出力メッセージ全文を返す + * + * ヒット数のみを検証する RunGrepFileWorker と異なり、出力文字列の完全一致 + * スナップショットを取るための派生ヘルパ。文字コードは CODE_UTF8 固定とし、 + * 自動判別によるコード名表記("[UTF-8]" 等)の揺れを排除する。 + */ +struct SWorkerSnapshot +{ + int hits; + std::wstring message; +}; + +SWorkerSnapshot RunWorkerSnapshot( + const std::filesystem::path& path, + std::wstring_view key, + int nOutputStyle, + int nOutputLineType) +{ + const std::wstring keyStr(key); + + SGrepFileTask task; + task.fullPath = path.wstring(); + task.fileName = path.filename().wstring(); + task.baseFolder = path.parent_path().wstring(); + task.folder = task.baseFolder; + task.relPath = task.fileName; + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(CODE_UTF8); // 文字コード固定で自動判別のコード名出力を回避 + gOpt.nGrepOutputStyle = nOutputStyle; + gOpt.nGrepOutputLineType = nOutputLineType; + + CSearchStringPattern pattern; + EXPECT_TRUE(pattern.SetPattern(nullptr, keyStr.c_str(), keyStr.size(), sOpt, nullptr)); + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + cmemMessage.AllocStringBuffer(4000); + cUnicodeBuffer.AllocStringBuffer(4000); + std::atomic cancel{ false }; + CGrepAgent agent; + + const int hits = agent.DoGrepFileWorker( + SGrepSearchParams{ keyStr.c_str(), sOpt, gOpt }, + task, + nullptr, pattern, cancel, cmemMessage, cUnicodeBuffer); + + return SWorkerSnapshot{ + hits, + std::wstring(cmemMessage.GetStringPtr(), cmemMessage.GetStringLength()) }; +} + +//! 2 行(1 行目のみヒット)の標準スナップショット入力を書き出す +std::filesystem::path WriteSnapshotInput(const GrepTempDir& temp) +{ + // 1 行目: 7 桁目に "needle"(1 ヒット) / 2 行目: ヒットなし + return temp.WriteEncodedTextFile(L"snap.txt", CODE_UTF8, L"alpha needle one\nbravo two\n"); +} + +} // namespace + +/*! + * @brief スナップショット: style=1(ノーマル) × lineType=1(行単位) + * @remark "フルパス(行,桁): 該当行" 形式の完全一致を確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style1_LineType1_MatchedLine) +{ + const auto path = WriteSnapshotInput(*m_temp); + const auto result = RunWorkerSnapshot(path, L"needle", 1, 1); + + EXPECT_EQ(1, result.hits); + EXPECT_EQ(path.wstring() + L"(1,7): alpha needle one\r\n", result.message); +} + +/*! + * @brief スナップショット: style=1(ノーマル) × lineType=0(該当部分) + * @remark 該当行全体ではなくマッチ部分のみが出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style1_LineType0_MatchedPart) +{ + const auto path = WriteSnapshotInput(*m_temp); + const auto result = RunWorkerSnapshot(path, L"needle", 1, 0); + + EXPECT_EQ(1, result.hits); + EXPECT_EQ(path.wstring() + L"(1,7): needle\r\n", result.message); +} + +/*! + * @brief スナップショット: style=1(ノーマル) × lineType=2(否マッチ行) + * @remark ヒットしなかった 2 行目のみが桁 1 で出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style1_LineType2_UnmatchedLine) +{ + const auto path = WriteSnapshotInput(*m_temp); + const auto result = RunWorkerSnapshot(path, L"needle", 1, 2); + + EXPECT_EQ(1, result.hits); + EXPECT_EQ(path.wstring() + L"(2,1): bravo two\r\n", result.message); +} + +/*! + * @brief スナップショット: style=2(WZ風) × lineType=1(行単位) + * @remark ファイルヘッダー行(■"フルパス")と "・(%6d,%-5d): " 書式の完全一致を確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style2_LineType1_MatchedLine) +{ + const auto path = WriteSnapshotInput(*m_temp); + const auto result = RunWorkerSnapshot(path, L"needle", 2, 1); + + EXPECT_EQ(1, result.hits); + EXPECT_EQ( + L"■\"" + path.wstring() + L"\"\r\n" + L"・( 1,7 ): alpha needle one\r\n", + result.message); +} + +/*! + * @brief スナップショット: style=3(結果のみ) × lineType=1(行単位) + * @remark パス・行番号などの装飾なしで該当行のみが出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style3_LineType1_MatchedLine) +{ + const auto path = WriteSnapshotInput(*m_temp); + const auto result = RunWorkerSnapshot(path, L"needle", 3, 1); + + EXPECT_EQ(1, result.hits); + EXPECT_EQ(L"alpha needle one\r\n", result.message); +} + +/*! + * @brief スナップショット: style=3(結果のみ) × lineType=0(該当部分) + * @remark マッチ部分のみが装飾なしで出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style3_LineType0_MatchedPart) +{ + const auto path = WriteSnapshotInput(*m_temp); + const auto result = RunWorkerSnapshot(path, L"needle", 3, 0); + + EXPECT_EQ(1, result.hits); + EXPECT_EQ(L"needle\r\n", result.message); +} + +/*! + * @brief スナップショット: style=1(ノーマル) × lineType=0 の同一行内複数ヒット + * @remark 同一行の 2 つのヒットがそれぞれの桁位置で 2 行出力されることを確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Style1_LineType0_MultipleHitsInLine) +{ + const auto path = m_temp->WriteEncodedTextFile(L"multi.txt", CODE_UTF8, L"needle x needle\n"); + const auto result = RunWorkerSnapshot(path, L"needle", 1, 0); + + EXPECT_EQ(2, result.hits); + EXPECT_EQ( + path.wstring() + L"(1,1): needle\r\n" + + path.wstring() + L"(1,10): needle\r\n", + result.message); +} + +/*! + * @brief スナップショット: 除外ファイル(!)・除外フォルダー(#)適用後の列挙結果 + * @remark "*.*;!*.log;#skip" 指定で .log ファイルと skip フォルダーが列挙から除外され、 + * 残る要素が期待どおりであることを確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Enumerate_ExcludePatternsApplied) +{ + m_temp->WriteEncodedTextFile(L"a.txt", CODE_UTF8, L"a\n"); + m_temp->WriteEncodedTextFile(L"b.log", CODE_UTF8, L"b\n"); + m_temp->WriteEncodedTextFile(L"c.txt", CODE_UTF8, L"c\n"); + m_temp->EnsureDir(L"keep"); + m_temp->EnsureDir(L"skip"); + + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.*;!*.log;#skip")); + + // ファイル列挙: b.log が除外される + { + CGrepEnumFilterFiles enumFiles; + CGrepEnumOptions enumOpts; + CGrepEnumFiles exceptAbs; + enumFiles.Enumerates(m_temp->Root().wstring().c_str(), keys, enumOpts, exceptAbs); + + std::vector found; + for (int i = 0; i < enumFiles.GetCount(); ++i) { + found.emplace_back(enumFiles.GetFileName(i)); + } + std::sort(found.begin(), found.end()); + + ASSERT_EQ(2u, found.size()); + EXPECT_EQ(L"a.txt", found[0]); + EXPECT_EQ(L"c.txt", found[1]); + } + + // フォルダー列挙: skip が除外される + { + CGrepEnumFilterFolders enumFolders; + CGrepEnumOptions enumOpts; + CGrepEnumFolders exceptAbs; + enumFolders.Enumerates(m_temp->Root().wstring().c_str(), keys, enumOpts, exceptAbs); + + std::vector found; + for (int i = 0; i < enumFolders.GetCount(); ++i) { + found.emplace_back(enumFolders.GetFileName(i)); + } + std::sort(found.begin(), found.end()); + + ASSERT_EQ(1u, found.size()); + EXPECT_EQ(L"keep", found[0]); + } +} + +/*! + * @brief スナップショット: 除外ファイルパターンが検索結果に反映される + * @remark 除外対象ファイルは列挙段階で落ちるため、残ったファイルの検索結果のみが + * 得られることを、列挙→ワーカー実行の結合で確認する。 + */ +TEST_F(GrepRealFileTest, Snapshot_Enumerate_ThenWorker_ExcludedFileNotSearched) +{ + const auto hitPath = m_temp->WriteEncodedTextFile(L"hit.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"ignored.log", CODE_UTF8, L"needle\n"); + + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.*;!*.log")); + + CGrepEnumFilterFiles enumFiles; + CGrepEnumOptions enumOpts; + CGrepEnumFiles exceptAbs; + enumFiles.Enumerates(m_temp->Root().wstring().c_str(), keys, enumOpts, exceptAbs); + + std::wstring combined; + int totalHits = 0; + for (int i = 0; i < enumFiles.GetCount(); ++i) { + const auto result = RunWorkerSnapshot( + m_temp->Root() / enumFiles.GetFileName(i), L"needle", 1, 1); + totalHits += result.hits; + combined += result.message; + } + + EXPECT_EQ(1, totalHits); + EXPECT_EQ(hitPath.wstring() + L"(1,1): needle\r\n", combined); +} + +// ============================================================================= +// 16. DoGrepFile 直列経路の直接検証(GA-36 ~ GA-40)・ハンドルGrep エラー経路(GA-41) +// ============================================================================= +// +// 補足: :HWND: 正常系(実ウィンドウ経由のハンドルGrep)は、テスト環境の CEditWnd が +// IsSakuraMainWindow のウィンドウクラス判定を満たさないため実行不可(CI で確認済み)。 +// 直列版 MatchAndEmitGrepLine(pnHitCount 非 null 分岐)は DoGrepFile を直接呼び出して +// 実ファイルに対し検証する。CDlgCancel はダイアログ未生成(NULL HWND)で渡し、 +// UI 系 API は no-op となる(GA-42 と同じ方式)。UI チェックは 32 行ごとのため +// 小さな入力では BlockingHook にも到達しない。 + +namespace { + +//! DoGrepFile(直列版)の実行結果 +struct SSerialGrepResult { + int ret; //!< DoGrepFile の戻り値(このファイルのヒット数) + int globalHits; //!< *pnHitCount(直列版合計ヒット数) + std::wstring message; //!< 結果メッセージ +}; + +//! DoGrepFile を実ファイルに対して直接呼び出す(直列版 MatchAndEmitGrepLine, pnHitCount 非 null 経路)。 +SSerialGrepResult RunDoGrepFileSerial( + const std::filesystem::path& path, + const std::wstring& key, + const SSearchOption& sOpt, + int nOutputLineType) +{ + SSerialGrepResult result{ 0, 0, L"" }; + + // DoGrep 本体と同じ手順で検索パターンを構築する(bRegularExp 時は SetPattern 内でコンパイルされる) + CBregexp regexp; + CSearchStringPattern pattern; + if (!pattern.SetPattern(nullptr, key.c_str(), (int)key.size(), sOpt, ®exp)) { + ADD_FAILURE() << "SetPattern failed"; + return result; + } + + auto gOpt = MakeGrepOption(); + gOpt.nGrepOutputLineType = nOutputLineType; + + CDlgCancel cDlgCancel; // ダイアログ未生成(NULL HWND・キャンセルされない) + + const std::wstring fullPath = path.wstring(); + const std::wstring fileName = path.filename().wstring(); + const std::wstring baseFolder = path.parent_path().wstring(); + + CNativeW cmemMessage; + CNativeW cUnicodeBuffer; + cmemMessage.AllocStringBuffer(4000); + cUnicodeBuffer.AllocStringBuffer(4000); + bool bOutputBaseFolder = false; + bool bOutputFolderName = false; + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + result.ret = agent.DoGrepFile( + pView, + &cDlgCancel, + NULL, // hWndTarget: NULL = 実ファイルを読み込む + key.c_str(), + fileName.c_str(), + sOpt, + gOpt, + pattern, + ®exp, + &result.globalHits, + fullPath.c_str(), + baseFolder.c_str(), + baseFolder.c_str(), + fileName.c_str(), + bOutputBaseFolder, + bOutputFolderName, + cmemMessage, + cUnicodeBuffer + ); + result.message.assign(cmemMessage.GetStringPtr(), (size_t)cmemMessage.GetStringLength()); + return result; +} + +} // namespace + +/*! + * @brief DoGrepFile 直列経路の基本ヒット(lineType=1) (GA-36) + * @remark 直列版 MatchAndEmitGrepLine(pnGlobalHitCount 非 null 分岐)で、戻り値と + * *pnHitCount の両方にヒット数が正しく加算されることを確認する。 + */ +TEST_F(GrepRealFileTest, SerialGrep_DoGrepFile_BasicHits) +{ + const auto path = m_temp->WriteEncodedTextFile(L"ga36.txt", CODE_UTF8, L"UNIQ0 alpha\nUNIQ0 beta\n"); + + SSearchOption sOpt; + sOpt.Reset(); + const auto r = RunDoGrepFileSerial(path, L"UNIQ0", sOpt, 1); + + EXPECT_EQ(2, r.ret) << "UNIQ0 を含む 2 行がヒットする"; + EXPECT_EQ(2, r.globalHits) << "直列版 *pnHitCount にも同数が加算される"; + EXPECT_NE(r.message.find(L"UNIQ0 alpha"), std::wstring::npos) << "ヒット行が出力される"; +} + +/*! + * @brief DoGrepFile 直列経路: lineType=0 で 1 行内複数ヒットの累積 (GA-37) + * @remark 該当部分出力モードで 1 行に 3 つのマッチがあるとき、直列版 MatchAndEmitGrepLine の + * ヒットごとの ++(*pnGlobalHitCount) が 3 回実行され合計 3 件となることを確認する。 + */ +TEST_F(GrepRealFileTest, SerialGrep_DoGrepFile_MultipleHitsInLine) +{ + const auto path = m_temp->WriteEncodedTextFile(L"ga37.txt", CODE_UTF8, L"UNIQ1 x UNIQ1 x UNIQ1\n"); + + SSearchOption sOpt; + sOpt.Reset(); + const auto r = RunDoGrepFileSerial(path, L"UNIQ1", sOpt, 0); // 該当部分(1 行内の複数マッチ継続) + + EXPECT_EQ(3, r.ret) << "1 行内の 3 マッチが戻り値に加算される"; + EXPECT_EQ(3, r.globalHits) << "1 行内の 3 マッチが直列版合計ヒット数へすべて加算される"; +} + +/*! + * @brief DoGrepFile 直列経路: lineType=2(否ヒット行)でヒット数が巻き戻される (GA-38) + * @remark 否ヒット行モードでは、ヒット行のマッチ分を *pnGlobalHitCount から行開始値へ + * 巻き戻し、否ヒット行のみ +1 する(C-1 共通化の直列版分岐)。 + * ファイルはヒット行 1(マッチ 3 個)+否ヒット行 2 の計 3 行。 + * 巻き戻しが壊れている場合、ヒット行の 3 マッチが混入して 5 以上になる。 + */ +TEST_F(GrepRealFileTest, SerialGrep_DoGrepFile_NoHitLineTypeRewindsCount) +{ + const auto path = m_temp->WriteEncodedTextFile(L"ga38.txt", CODE_UTF8, L"UNIQ2 UNIQ2 UNIQ2\nmissA\nmissB\n"); + + SSearchOption sOpt; + sOpt.Reset(); + const auto r = RunDoGrepFileSerial(path, L"UNIQ2", sOpt, 2); // 否ヒット行 + + EXPECT_EQ(2, r.ret) + << "否ヒット行 2 のみカウントされる(ヒット行の 3 マッチは巻き戻される)"; + EXPECT_EQ(2, r.globalHits) + << "直列版 *pnHitCount も行開始値へ巻き戻されて否ヒット行 2 のみとなる"; + EXPECT_EQ(r.message.find(L"UNIQ2"), std::wstring::npos) + << "ヒット行(UNIQ2 を含む行)は否ヒット行モードでは出力されない"; + EXPECT_NE(r.message.find(L"missA"), std::wstring::npos) << "否ヒット行 missA が出力される"; + EXPECT_NE(r.message.find(L"missB"), std::wstring::npos) << "否ヒット行 missB が出力される"; +} + +/*! + * @brief DoGrepFile 直列経路: 単語のみ検索分岐 (GA-39) + * @remark 直列版 MatchAndEmitGrepLine の bWordOnly 分岐で、独立語のみヒットし + * 部分一致(UNIQ3words)はヒットしないことを確認する。 + */ +TEST_F(GrepRealFileTest, SerialGrep_DoGrepFile_WordOnly) +{ + const auto path = m_temp->WriteEncodedTextFile(L"ga39.txt", CODE_UTF8, L"UNIQ3w UNIQ3words UNIQ3w\n"); + + SSearchOption sOpt; + sOpt.Reset(); + sOpt.bWordOnly = true; + const auto r = RunDoGrepFileSerial(path, L"UNIQ3w", sOpt, 0); // 1 行内の複数マッチを数える + + EXPECT_EQ(2, r.ret) << "独立語 UNIQ3w の 2 件のみ(UNIQ3words は単語境界不成立)"; + EXPECT_EQ(2, r.globalHits) << "直列版 *pnHitCount にも同数が加算される"; +} + +/*! + * @brief DoGrepFile 直列経路: 正規表現検索分岐 (GA-40) + * @remark 直列版 MatchAndEmitGrepLine の bRegularExp 分岐で、`^RXUNIQ\d+$` が + * 数字終端の行のみにヒットすることを確認する。 + * @note bregonig.dll に依存する(他の正規表現テストと同条件)。 + */ +TEST_F(GrepRealFileTest, SerialGrep_DoGrepFile_Regex) +{ + const auto path = m_temp->WriteEncodedTextFile(L"ga40.txt", CODE_UTF8, L"RXUNIQ1\nRXUNIQ22\nRXUNIQx\n"); + + SSearchOption sOpt; + sOpt.Reset(); + sOpt.bRegularExp = true; + const auto r = RunDoGrepFileSerial(path, L"^RXUNIQ\\d+$", sOpt, 1); + + EXPECT_EQ(2, r.ret) << "RXUNIQ1 / RXUNIQ22 の 2 行のみヒットする"; + EXPECT_EQ(2, r.globalHits) << "直列版 *pnHitCount にも同数が加算される"; +} + +/*! + * @brief ハンドルGrep: 無効 HWND トークンのエラー経路で SGrepRunningGuard がリセットする (GA-41) + * @remark 無効な :HWND: トークンで GetHwndTitle が -1 を返す経路("HWND handle error.")でも、 + * A-3 の RAII ガードにより m_bGrepRunning がリセットされ、同一 agent で + * 続けて実行する通常 Grep が再入エラー(0xffffffff)にならないことを確認する。 + * 本ガード導入前は本経路のみリセットが漏れ、以後 Grep が永続的に実行不能だった。 + */ +TEST_F(GrepRealFileTest, HwndGrep_InvalidTokenErrorPath_GuardResetsRunningFlag) +{ + m_temp->WriteEncodedTextFile(L"guard.txt", CODE_UTF8, L"needle\nneedle\n"); + + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + + CGrepAgent agent; // 2 回の DoGrep で同一インスタンスを使う(再入ガードは agent 単位) + + // 1 回目: 無効 HWND トークン → GetHwndTitle が -1 → エラー経路で return 0 + { + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_badhwnd_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + std::string out; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + const CNativeW cmKey(L"needle"); + const CNativeW cmRep(L""); + const CNativeW cmFile(L":HWND:0"); // 無効ハンドル(IsSakuraMainWindow が false) + const CNativeW cmFolder(L"C:\\"); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, gOpt, + true, // bGrepCurFolder + false // bGrepExcludeFileRegexp + ); + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[8192] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + out.assign(buf, dwRead); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + + EXPECT_EQ(0u, hits) << "無効 HWND トークンは 0 件で復帰する"; + EXPECT_NE(out.find("HWND handle error"), std::string::npos) + << "HWND ハンドルエラーのメッセージが出力される"; + } + + // 2 回目: 同一 agent で通常のファイル Grep。ガードが機能していれば正常実行できる。 + // ガードが機能していない場合は再入検知で 0xffffffff が返る。 + { + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_guard2_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits2 = 0; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + const CNativeW cmKey(L"needle"); + const CNativeW cmRep(L""); + const CNativeW cmFile(L"*.txt"); + const CNativeW cmFolder(m_temp->Root().wstring().c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits2 = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, gOpt, + true, // bGrepCurFolder + false // bGrepExcludeFileRegexp + ); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + + EXPECT_NE(0xffffffffu, hits2) + << "SGrepRunningGuard により m_bGrepRunning がリセットされ再入エラーにならない"; + EXPECT_EQ(2u, hits2) << "通常 Grep が正常に実行され needle 2 件がヒットする"; + } +} + +// ============================================================================= +// 17. DoGrepTree キャンセル経路・P4 結合安全化の検証(GA-42 ~ GA-44) +// ============================================================================= + +/*! + * @brief DoGrepTree: キャンセル済みダイアログで FlushAndCancel 経路を通る (GA-42) + * @remark ダイアログを生成せず m_bCANCEL=TRUE の CDlgCancel を注入し、ファイルループ先頭の + * 中断チェックで FlushAndCancel(B-1: goto cancel_return の代替ラムダ)が実行され、 + * (1) 戻り値 -1、(2) 残存メッセージの AddTail フラッシュ、(3) バッファ長 0 リセット、 + * の 3 点を確認する。フラッシュ先は stdout(bGrepStdout=true)で回収する。 + */ +TEST_F(GrepRealFileTest, DoGrepTree_PreCancelledFlushesAndReturnsMinusOne) +{ + // キャンセルチェックはファイルループ内にあるためファイルが 1 つ以上必要 + m_temp->WriteEncodedTextFile(L"c.txt", CODE_UTF8, L"needle\n"); + + CDlgCancel cDlgCancel; + cDlgCancel.m_bCANCEL = TRUE; // ダイアログ未生成のままキャンセル済み状態にする + + const auto sOpt = MakeSearchOption(false, false); + auto gOpt = MakeGrepOption(); + gOpt.bGrepStdout = true; + + CSearchStringPattern pattern; + ASSERT_TRUE(pattern.SetPattern(nullptr, L"needle", 6, sOpt, nullptr)); + CBregexp regexp; // 非正規表現検索のため未使用(キャンセル判定が先に走る) + + CGrepEnumKeys keys; + ASSERT_EQ(0, keys.SetFileKeys(L"*.txt")); + CGrepEnumFiles exceptAbsFiles; + CGrepEnumFolders exceptAbsFolders; + + CNativeW cmemMessage; + cmemMessage.AppendString(L"PRELOADEDMARK\r\n"); // FlushAndCancel の残存フラッシュ検証用 + CNativeW cUnicodeBuffer; + int nHitCount = 0; + bool bOutputBaseFolder = false; + const std::wstring rootPath = m_temp->Root().wstring(); + const CNativeW cmRep(L""); + + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogreptree_cancel_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + int ret = 0; + std::string out; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + ret = agent.DoGrepTree( + pView, + &cDlgCancel, + L"needle", + cmRep, + keys, + exceptAbsFiles, + exceptAbsFolders, + rootPath.c_str(), + rootPath.c_str(), + sOpt, + gOpt, + pattern, + ®exp, + bOutputBaseFolder, + &nHitCount, + cmemMessage, + cUnicodeBuffer + ); + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[8192] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + out.assign(buf, dwRead); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + + EXPECT_EQ(-1, ret) << "キャンセル時は -1 を返す"; + EXPECT_EQ(0, cmemMessage.GetStringLength()) + << "FlushAndCancel が残存メッセージをフラッシュして長さ 0 に戻す"; + EXPECT_NE(out.find("PRELOADEDMARK"), std::string::npos) + << "残存メッセージが AddTail(stdout)へフラッシュされる"; +} + +/*! + * @brief DoGrep 置換: 後方参照を含む除外正規表現は結合せず個別適用される (GA-43) + * @remark P4 結合はグループ番号が全パターン通しで振り直されるため、先行パターンに + * キャプチャグループがあると後続パターンの \1 の参照先が変わる。 + * CanCombineExcludePatterns により個別コンパイルへ迂回し、各パターンが + * 単独コンパイル時と同じ意味で適用されることを直列(置換)経路で確認する。 + * p1=`.*(zk)m\.log$`(グループあり)、p2=`.*(zq)\1.*`(\1 は (zq) を参照)。 + */ +TEST_F(GrepRealFileTest, DoGrep_ReplaceMode_BackRefExcludeNotCombined) +{ + m_temp->WriteEncodedTextFile(L"keep.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"zqzq.txt", CODE_UTF8, L"needle\n"); // p2 で除外(zqzq = (zq)\1) + m_temp->WriteEncodedTextFile(L"zkm.log", CODE_UTF8, L"needle\n"); // p1 で除外 + + WCHAR tempDir[MAX_PATH]; + ::GetTempPathW(MAX_PATH, tempDir); + const std::wstring tmpFile = std::wstring(tempDir) + L"sakura_dogrep_backref_stdout.txt"; + HANDLE hTempFile = ::CreateFileW(tmpFile.c_str(), + GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ASSERT_NE(INVALID_HANDLE_VALUE, hTempFile) << "stdout キャプチャファイル作成失敗"; + struct HandleDeleter { + void operator()(HANDLE h) const { if (h && h != INVALID_HANDLE_VALUE) ::CloseHandle(h); } + }; + std::unique_ptr hFileGuard{ hTempFile }; + + DWORD hits = 0; + std::string out; + { + GrepStdHandleGuard stdoutGuard(STD_OUTPUT_HANDLE, hTempFile); + + CEditView* pView = &CEditWnd::getInstance()->GetActiveView(); + CGrepAgent agent; + + const CNativeW cmKey(L"needle"); + const CNativeW cmRep(L"REPLACED"); + const CNativeW cmFile(L"*.*;!.*(zk)m\\.log$;!.*(zq)\\1.*"); + const CNativeW cmFolder(m_temp->Root().wstring().c_str()); + + SSearchOption sOpt; + sOpt.Reset(); + + SGrepOption gOpt; + gOpt.bGrepReplace = true; + gOpt.bGrepSubFolder = false; + gOpt.bGrepStdout = true; + gOpt.bGrepHeader = true; + gOpt.nGrepCharSet = CODE_AUTODETECT; + gOpt.nGrepOutputLineType = 1; + gOpt.nGrepOutputStyle = 1; + + hits = agent.DoGrep( + pView, + SGrepInput{ &cmKey, &cmRep, &cmFile, &cmFolder }, + sOpt, + gOpt, + true, // bGrepCurFolder + true // bGrepExcludeFileRegexp=true + ); + + ::SetFilePointer(hTempFile, 0, NULL, FILE_BEGIN); + char buf[8192] = {0}; + DWORD dwRead = 0; + ::ReadFile(hTempFile, buf, sizeof(buf) - 1, &dwRead, NULL); + out.assign(buf, dwRead); + } + hFileGuard.reset(); + ::DeleteFileW(tmpFile.c_str()); + + EXPECT_EQ(1u, hits) << "keep.txt の 1 件のみ置換(後方参照パターンは個別適用で正しく除外)"; + EXPECT_NE(out.find("keep.txt"), std::string::npos) << "keep.txt が出力される"; + EXPECT_EQ(out.find("zqzq"), std::string::npos) << "zqzq.txt は \\1 付きパターンで除外"; + EXPECT_EQ(out.find("zkm"), std::string::npos) << "zkm.log はグループ付きパターンで除外"; +} + +/*! + * @brief RunParallelGrep: 後方参照を含む除外正規表現は結合せず個別適用される (GA-44) + * @remark GA-43 と同じパターン構成を並列経路(ワーカー内 excludeRegexps)で検証する。 + */ +TEST_F(GrepRealFileTest, DoGrep_StdoutMode_BackRefExcludeNotCombined) +{ + m_temp->WriteEncodedTextFile(L"keep.txt", CODE_UTF8, L"needle\n"); + m_temp->WriteEncodedTextFile(L"zqzq.txt", CODE_UTF8, L"needle\n"); // p2 で除外(zqzq = (zq)\1) + m_temp->WriteEncodedTextFile(L"zkm.log", CODE_UTF8, L"needle\n"); // p1 で除外 + + std::string out; + const DWORD hits = RunDoGrepStdout( + L"needle", L"*.*;!.*(zk)m\\.log$;!.*(zq)\\1.*", m_temp->Root().wstring(), + out, /*bExcludeFileRegex=*/true); + + EXPECT_EQ(1u, hits) << "keep.txt の 1 件のみ(後方参照パターンは個別適用で正しく除外)"; + EXPECT_NE(out.find("keep.txt"), std::string::npos) << "keep.txt のヒットが出力される"; + EXPECT_EQ(out.find("zqzq"), std::string::npos) << "zqzq.txt は \\1 付きパターンで除外"; + EXPECT_EQ(out.find("zkm"), std::string::npos) << "zkm.log はグループ付きパターンで除外"; +}