From ca07ac69c7816804601b8420c1f1c22dd7fc77aa Mon Sep 17 00:00:00 2001 From: jofon <70416966+jofon@users.noreply.github.com> Date: Sun, 31 Jan 2021 18:31:14 +0000 Subject: [PATCH] Enhance Folder as Workspace performance while adding/removing files in bulk Added batch processing of added and removed files in Folder as Workspace. Fix #9203,close #9651 --- .../WinControls/FileBrowser/fileBrowser.cpp | 401 +++++++++++++----- .../src/WinControls/FileBrowser/fileBrowser.h | 26 +- 2 files changed, 317 insertions(+), 110 deletions(-) diff --git a/PowerEditor/src/WinControls/FileBrowser/fileBrowser.cpp b/PowerEditor/src/WinControls/FileBrowser/fileBrowser.cpp index 4c52827d1..f480ee08c 100644 --- a/PowerEditor/src/WinControls/FileBrowser/fileBrowser.cpp +++ b/PowerEditor/src/WinControls/FileBrowser/fileBrowser.cpp @@ -244,27 +244,12 @@ INT_PTR CALLBACK FileBrowser::run_dlgProc(UINT message, WPARAM wParam, LPARAM lP case FB_ADDFILE: { - const std::vector file2Change = *(std::vector *)lParam; - generic_string separator = TEXT("\\\\"); - - size_t sepPos = file2Change[0].find(separator); - if (sepPos == generic_string::npos) - return false; - generic_string pathSuffix = file2Change[0].substr(sepPos + separator.length(), file2Change[0].length() - 1); - - // remove prefix of file/folder in changeInfo, splite the remained path - vector linarPathArray = split(pathSuffix, '\\'); - - generic_string rootPath = file2Change[0].substr(0, sepPos); - generic_string path = rootPath; + std::vector groupedFiles = getFilesFromParam(lParam); - generic_string addedFilePath = file2Change[0].substr(0, sepPos + 1); - addedFilePath += pathSuffix; - bool isAdded = addInTree(rootPath, addedFilePath, nullptr, linarPathArray); - if (!isAdded) + for (auto & group : groupedFiles) { - //MessageBox(NULL, addedFilePath.c_str(), TEXT("file/folder is not added"), MB_OK); + addToTree(group, nullptr); } break; @@ -272,26 +257,14 @@ INT_PTR CALLBACK FileBrowser::run_dlgProc(UINT message, WPARAM wParam, LPARAM lP case FB_RMFILE: { - const std::vector file2Change = *(std::vector *)lParam; - generic_string separator = TEXT("\\\\"); - - size_t sepPos = file2Change[0].find(separator); - if (sepPos == generic_string::npos) - return false; - generic_string pathSuffix = file2Change[0].substr(sepPos + separator.length(), file2Change[0].length() - 1); - - // remove prefix of file/folder in changeInfo, splite the remained path - vector linarPathArray = split(pathSuffix, '\\'); - - generic_string rootPath = file2Change[0].substr(0, sepPos); - // search recursively and modify the tree structure + std::vector groupedFiles = getFilesFromParam(lParam); - bool isRemoved = deleteFromTree(rootPath, nullptr, linarPathArray); - if (!isRemoved) + for (auto & group : groupedFiles) { - //MessageBox(NULL, file2Change[0].c_str(), TEXT("file/folder is not removed"), MB_OK); + deleteFromTree(group); } + break; } @@ -1138,10 +1111,8 @@ HTREEITEM FileBrowser::getRootFromFullPath(const generic_string & rootPath) cons HTREEITEM FileBrowser::findChildNodeFromName(HTREEITEM parent, const generic_string& label) const { - HTREEITEM childNodeFound = nullptr; - for (HTREEITEM hItemNode = _treeView.getChildFrom(parent); - hItemNode != NULL && childNodeFound == nullptr; + hItemNode != NULL; hItemNode = _treeView.getNextSibling(hItemNode)) { TCHAR textBuffer[MAX_PATH]; @@ -1154,10 +1125,10 @@ HTREEITEM FileBrowser::findChildNodeFromName(HTREEITEM parent, const generic_str if (label == tvItem.pszText) { - childNodeFound = hItemNode; + return hItemNode; } } - return childNodeFound; + return nullptr; } vector FileBrowser::getRoots() const @@ -1190,49 +1161,118 @@ generic_string FileBrowser::getSelectedItemPath() const return itemPath; } -bool FileBrowser::addInTree(const generic_string& rootPath, const generic_string& addItemFullPath, HTREEITEM node, vector linarPathArray) +std::vector FileBrowser::getFilesFromParam(LPARAM lParam) const +{ + const std::vector filesToChange = *(std::vector*)lParam; + const generic_string separator = TEXT("\\\\"); + const size_t separatorLength = separator.length(); + + std::vector groupedFiles; + for (size_t i = 0; i < filesToChange.size(); i++) + { + const size_t sepPos = filesToChange[i].find(separator); + if (sepPos == generic_string::npos) + continue; + + const generic_string pathSuffix = filesToChange[i].substr(sepPos + separatorLength, filesToChange[i].length() - 1); + + // remove prefix of file/folder in changeInfo, split the remained path + vector linarPathArray = split(pathSuffix, '\\'); + + const generic_string lastElement = linarPathArray.back(); + linarPathArray.pop_back(); + + const generic_string rootPath = filesToChange[i].substr(0, sepPos); + + const generic_string addedFilePath = filesToChange[i].substr(0, sepPos + 1) + pathSuffix; + + generic_string commonPath = rootPath; + + for (const auto & element : linarPathArray) + { + commonPath.append(TEXT("\\")); + commonPath.append(element); + } + + commonPath.append(TEXT("\\")); + + const auto it = std::find_if(groupedFiles.begin(), groupedFiles.end(), [&commonPath](const auto & group) { return group._commonPath == commonPath; }); + + if (it == groupedFiles.end()) + { + // Add a new file group + FilesToChange group; + group._commonPath = commonPath; + group._rootPath = rootPath; + group._linarWithoutLastPathElement = linarPathArray; + group._files.push_back(lastElement); + groupedFiles.push_back(group); + } + else + { + // Add to an existing file group + it->_files.push_back(lastElement); + } + } + + return groupedFiles; +} + +bool FileBrowser::addToTree(FilesToChange & group, HTREEITEM node) { if (node == nullptr) // it's a root. Search the right root with rootPath { // Search - if ((node = getRootFromFullPath(rootPath)) == nullptr) + if ((node = getRootFromFullPath(group._rootPath)) == nullptr) return false; } - if (linarPathArray.size() == 1) + if (group._linarWithoutLastPathElement.size() == 0) { - // Of course item to add should be exist on the disk - if (!::PathFileExists(addItemFullPath.c_str())) - return false; + // Items to add should exist on the disk + group._files.erase(std::remove_if(group._files.begin(), group._files.end(), + [&group](const auto & file) + { + return !::PathFileExists((group._commonPath + file).c_str()); + }), + group._files.end()); - // Search : if no found, add - HTREEITEM childNodeFound = findChildNodeFromName(node, linarPathArray[0]); - if (childNodeFound != nullptr) + if (group._files.empty()) + { return false; + } - // No found, good - Action - if (::PathIsDirectory(addItemFullPath.c_str())) + // Search: if not found, add + removeNamesAlreadyInNode(node, group._files); + if (group._files.empty()) { - SortingData4lParam* customData = new SortingData4lParam(TEXT(""), linarPathArray[0], true); - sortingDataArray.push_back(customData); - - _treeView.addItem(linarPathArray[0].c_str(), node, INDEX_CLOSE_NODE, reinterpret_cast(customData)); + return false; } - else - { - SortingData4lParam* customData = new SortingData4lParam(TEXT(""), linarPathArray[0], false); - sortingDataArray.push_back(customData); - _treeView.addItem(linarPathArray[0].c_str(), node, INDEX_LEAF, reinterpret_cast(customData)); - } + // Not found, good - Action + for (auto & file : group._files) { + if (::PathIsDirectory((group._commonPath + file).c_str())) + { + SortingData4lParam* customData = new SortingData4lParam(TEXT(""), file, true); + sortingDataArray.push_back(customData); + _treeView.addItem(file.c_str(), node, INDEX_CLOSE_NODE, reinterpret_cast(customData)); + } + else + { + SortingData4lParam* customData = new SortingData4lParam(TEXT(""), file, false); + sortingDataArray.push_back(customData); + + _treeView.addItem(file.c_str(), node, INDEX_LEAF, reinterpret_cast(customData)); + } + } _treeView.customSorting(node, categorySortFunc, 0); return true; } else { for (HTREEITEM hItemNode = _treeView.getChildFrom(node); - hItemNode != NULL ; + hItemNode != NULL; hItemNode = _treeView.getNextSibling(hItemNode)) { TCHAR textBuffer[MAX_PATH]; @@ -1243,15 +1283,33 @@ bool FileBrowser::addInTree(const generic_string& rootPath, const generic_string tvItem.hItem = hItemNode; SendMessage(_treeView.getHSelf(), TVM_GETITEM, 0, reinterpret_cast(&tvItem)); - if (linarPathArray[0] == tvItem.pszText) + if (group._linarWithoutLastPathElement[0] == tvItem.pszText) { // search recursively the node for an action - linarPathArray.erase(linarPathArray.begin()); - return addInTree(rootPath, addItemFullPath, hItemNode, linarPathArray); + group._linarWithoutLastPathElement.erase(group._linarWithoutLastPathElement.begin()); + return addToTree(group, hItemNode); } } return false; } + +} + +bool FileBrowser::deleteFromTree(FilesToChange & group) +{ + std::vector foundItems = findInTree(group, nullptr); + + if (foundItems.empty() == true) + { + return false; + } + + for (auto & item : foundItems) + { + _treeView.removeItem(item); + } + + return true; } HTREEITEM FileBrowser::findInTree(const generic_string& rootPath, HTREEITEM node, std::vector linarPathArray) const @@ -1297,15 +1355,96 @@ HTREEITEM FileBrowser::findInTree(const generic_string& rootPath, HTREEITEM node } } -bool FileBrowser::deleteFromTree(const generic_string& rootPath, HTREEITEM node, const std::vector& linarPathArray) +std::vector FileBrowser::findInTree(FilesToChange & group, HTREEITEM node) const { - HTREEITEM foundItem = findInTree(rootPath, node, linarPathArray); - if (foundItem == nullptr) - return false; + if (node == nullptr) // it's a root. Search the right root with rootPath + { + // Search + if ((node = getRootFromFullPath(group._rootPath)) == nullptr) + { + return {}; + } + } - // found it, delete it - _treeView.removeItem(foundItem); - return true; + if (group._linarWithoutLastPathElement.empty()) + { + // Search + return findChildNodesFromNames(node, group._files); + } + else + { + for (HTREEITEM hItemNode = _treeView.getChildFrom(node); + hItemNode != NULL; + hItemNode = _treeView.getNextSibling(hItemNode)) + { + TCHAR textBuffer[MAX_PATH]; + TVITEM tvItem; + tvItem.mask = TVIF_TEXT; + tvItem.pszText = textBuffer; + tvItem.cchTextMax = MAX_PATH; + tvItem.hItem = hItemNode; + SendMessage(_treeView.getHSelf(), TVM_GETITEM, 0, reinterpret_cast(&tvItem)); + + if (group._linarWithoutLastPathElement[0] == tvItem.pszText) + { + // search recursively the node for an action + group._linarWithoutLastPathElement.erase(group._linarWithoutLastPathElement.begin()); + return findInTree(group, hItemNode); + } + } + return {}; + } +} + +std::vector FileBrowser::findChildNodesFromNames(HTREEITEM parent, std::vector & labels) const +{ + std::vector itemNodes; + + for (HTREEITEM hItemNode = _treeView.getChildFrom(parent); + hItemNode != NULL && !labels.empty(); + hItemNode = _treeView.getNextSibling(hItemNode) + ) + { + TCHAR textBuffer[MAX_PATH]; + TVITEM tvItem; + tvItem.mask = TVIF_TEXT; + tvItem.pszText = textBuffer; + tvItem.cchTextMax = MAX_PATH; + tvItem.hItem = hItemNode; + SendMessage(_treeView.getHSelf(), TVM_GETITEM, 0, reinterpret_cast(&tvItem)); + + auto it = std::find(labels.begin(), labels.end(), tvItem.pszText); + if (it != labels.end()) + { + labels.erase(it); // remove, as it was already found + itemNodes.push_back(hItemNode); + } + } + return itemNodes; +} + +void FileBrowser::removeNamesAlreadyInNode(HTREEITEM parent, std::vector & labels) const +{ + // We have to search for the labels in the child nodes of parent, and remove the ones that already exist + for (HTREEITEM hItemNode = _treeView.getChildFrom(parent); + hItemNode != NULL && !labels.empty(); + hItemNode = _treeView.getNextSibling(hItemNode) + ) + { + TCHAR textBuffer[MAX_PATH]; + TVITEM tvItem; + tvItem.mask = TVIF_TEXT; + tvItem.pszText = textBuffer; + tvItem.cchTextMax = MAX_PATH; + tvItem.hItem = hItemNode; + SendMessage(_treeView.getHSelf(), TVM_GETITEM, 0, reinterpret_cast(&tvItem)); + + auto it = std::find(labels.begin(), labels.end(), tvItem.pszText); + if (it != labels.end()) + { + labels.erase(it); + } + } } bool FileBrowser::renameInTree(const generic_string& rootPath, HTREEITEM node, const std::vector& linarPathArrayFrom, const generic_string & renameTo) @@ -1527,53 +1666,57 @@ DWORD WINAPI FolderUpdater::watching(void *params) case WAIT_OBJECT_0 + 1: // We've received a notification in the queue. { + static const unsigned int MAX_BATCH_SIZE = 100; + + DWORD dwPreviousAction = 0; DWORD dwAction; generic_string wstrFilename; + + std::vector filesToChange; // Process all available changes, ignore User actions while (changes.Pop(dwAction, wstrFilename)) { - static generic_string oldName; - static std::vector file2Change; - file2Change.clear(); - switch (dwAction) + // FILE_ACTION_ADDED and FILE_ACTION_REMOVED are done in batches + if (dwAction != FILE_ACTION_ADDED && dwAction != FILE_ACTION_REMOVED) + { + processChange(dwAction, { wstrFilename }, thisFolderUpdater); + } + else { - case FILE_ACTION_ADDED: - file2Change.push_back(wstrFilename); - ::SendMessage((thisFolderUpdater->_pFileBrowser)->getHSelf(), FB_ADDFILE, reinterpret_cast(nullptr), reinterpret_cast(&file2Change)); - oldName = TEXT(""); - break; - - case FILE_ACTION_REMOVED: - file2Change.push_back(wstrFilename); - ::SendMessage((thisFolderUpdater->_pFileBrowser)->getHSelf(), FB_RMFILE, reinterpret_cast(nullptr), reinterpret_cast(&file2Change)); - oldName = TEXT(""); - break; - - case FILE_ACTION_MODIFIED: - oldName = TEXT(""); - break; - - case FILE_ACTION_RENAMED_OLD_NAME: - oldName = wstrFilename; - break; - - case FILE_ACTION_RENAMED_NEW_NAME: - if (!oldName.empty()) + // first iteration + if (dwPreviousAction == 0) + { + dwPreviousAction = dwAction; + } + + if (dwPreviousAction == dwAction) + { + filesToChange.push_back(wstrFilename); + + if (filesToChange.size() > MAX_BATCH_SIZE) // Process some so the editor doesn't block for too long { - file2Change.push_back(oldName); - file2Change.push_back(wstrFilename); - //thisFolderUpdater->updateTree(dwAction, file2Change); - ::SendMessage((thisFolderUpdater->_pFileBrowser)->getHSelf(), FB_RNFILE, reinterpret_cast(nullptr), reinterpret_cast(&file2Change)); + processChange(dwAction, filesToChange, thisFolderUpdater); + filesToChange.clear(); } - oldName = TEXT(""); - break; - - default: - oldName = TEXT(""); - break; + } + else + { + // Different action. Process the previous batch and start saving a new one + processChange(dwPreviousAction, filesToChange, thisFolderUpdater); + filesToChange.clear(); + + dwPreviousAction = dwAction; + filesToChange.push_back(wstrFilename); + } } } + + // process the last changes + if (dwAction == FILE_ACTION_ADDED || dwAction == FILE_ACTION_REMOVED) + { + processChange(dwAction, filesToChange, thisFolderUpdater); + } } break; @@ -1589,3 +1732,47 @@ DWORD WINAPI FolderUpdater::watching(void *params) //printStr(L"Quit watching thread"); return EXIT_SUCCESS; } + +void FolderUpdater::processChange(DWORD dwAction, std::vector filesToChange, FolderUpdater* thisFolderUpdater) +{ + static generic_string oldName; + + switch (dwAction) + { + case FILE_ACTION_ADDED: + + ::SendMessage((thisFolderUpdater->_pFileBrowser)->getHSelf(), FB_ADDFILE, reinterpret_cast(nullptr), reinterpret_cast(&filesToChange)); + oldName = TEXT(""); + break; + + case FILE_ACTION_REMOVED: + + ::SendMessage((thisFolderUpdater->_pFileBrowser)->getHSelf(), FB_RMFILE, reinterpret_cast(nullptr), reinterpret_cast(&filesToChange)); + oldName = TEXT(""); + break; + + case FILE_ACTION_MODIFIED: + oldName = TEXT(""); + break; + + case FILE_ACTION_RENAMED_OLD_NAME: + oldName = filesToChange.back(); + break; + + case FILE_ACTION_RENAMED_NEW_NAME: + if (!oldName.empty()) + { + std::vector fileRename; + fileRename.push_back(oldName); + fileRename.push_back(filesToChange.back()); + //thisFolderUpdater->updateTree(dwAction, fileRename); + ::SendMessage((thisFolderUpdater->_pFileBrowser)->getHSelf(), FB_RNFILE, reinterpret_cast(nullptr), reinterpret_cast(&fileRename)); + } + oldName = TEXT(""); + break; + + default: + oldName = TEXT(""); + break; + } +} diff --git a/PowerEditor/src/WinControls/FileBrowser/fileBrowser.h b/PowerEditor/src/WinControls/FileBrowser/fileBrowser.h index e65d3f616..f5c345148 100644 --- a/PowerEditor/src/WinControls/FileBrowser/fileBrowser.h +++ b/PowerEditor/src/WinControls/FileBrowser/fileBrowser.h @@ -103,6 +103,8 @@ private: HANDLE _watchThreadHandle = nullptr; HANDLE _EventHandle = nullptr; static DWORD WINAPI watching(void *param); + + static void processChange(DWORD dwAction, std::vector filesToChange, FolderUpdater* thisFolderUpdater); }; struct SortingData4lParam { @@ -143,11 +145,10 @@ public: void addRootFolder(generic_string rootFolderPath); HTREEITEM getRootFromFullPath(const generic_string & rootPath) const; - HTREEITEM findChildNodeFromName(HTREEITEM parent, const generic_string&) const; + HTREEITEM findChildNodeFromName(HTREEITEM parent, const generic_string& label) const; - bool addInTree(const generic_string& rootPath, const generic_string& addItemFullPath, HTREEITEM node, std::vector linarPathArray); HTREEITEM findInTree(const generic_string& rootPath, HTREEITEM node, std::vector linarPathArray) const; - bool deleteFromTree(const generic_string& rootPath, HTREEITEM node, const std::vector& linarPathArray); + void deleteAllFromTree() { popupMenuCmd(IDM_FILEBROWSER_REMOVEALLROOTS); }; @@ -188,6 +189,25 @@ protected: bool selectCurrentEditingFile() const; + struct FilesToChange { + generic_string _commonPath; // Common path between all the files. _rootPath + _linarWithoutLastPathElement + generic_string _rootPath; + std::vector _linarWithoutLastPathElement; + std::vector _files; // file/folder names + }; + + std::vector getFilesFromParam(LPARAM lParam) const; + + bool addToTree(FilesToChange & group, HTREEITEM node); + + bool deleteFromTree(FilesToChange & group); + + std::vector findInTree(FilesToChange & group, HTREEITEM node) const; + + std::vector findChildNodesFromNames(HTREEITEM parent, std::vector & labels) const; + + void removeNamesAlreadyInNode(HTREEITEM parent, std::vector & labels) const; + virtual INT_PTR CALLBACK run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam); void notified(LPNMHDR notification); void showContextMenu(int x, int y);