diff --git a/spec/System/TestBuildListHelpers_spec.lua b/spec/System/TestBuildListHelpers_spec.lua new file mode 100644 index 0000000000..f390ff60d4 --- /dev/null +++ b/spec/System/TestBuildListHelpers_spec.lua @@ -0,0 +1,150 @@ +describe("BuildListHelpers", function() + local originalBuildPath + local originalCloudErrorPopup + local originalFileSearch + local originalFilterBuildList + local originalOpen + local buildListHelpers + local fileHeaders + local searches + local fileOpenCount + local searchCount + local cloudErrorPath + + before_each(function() + originalBuildPath = main.buildPath + originalCloudErrorPopup = main.OpenCloudErrorPopup + originalFileSearch = _G.NewFileSearch + originalFilterBuildList = main.filterBuildList + originalOpen = io.open + buildListHelpers = LoadModule("Modules/BuildListHelpers") + fileHeaders = { } + searches = { } + fileOpenCount = 0 + searchCount = 0 + cloudErrorPath = nil + main.buildPath = "Builds/" + main.filterBuildList = "" + main.OpenCloudErrorPopup = function(_, path) + cloudErrorPath = path + end + _G.NewFileSearch = function(pattern, foldersOnly) + searchCount = searchCount + 1 + local entries = searches[(foldersOnly and "folders:" or "files:")..pattern] + if not entries or not entries[1] then return end + local index = 1 + return { + GetFileName = function() return entries[index].name end, + GetFileModifiedTime = function() return entries[index].modified or 0 end, + NextFile = function() + index = index + 1 + return entries[index] ~= nil + end, + } + end + io.open = function(path) + fileOpenCount = fileOpenCount + 1 + if fileHeaders[path] == nil then return end + return { + read = function() return fileHeaders[path] or nil end, + close = function() end, + } + end + end) + + after_each(function() + main.buildPath = originalBuildPath + main.OpenCloudErrorPopup = originalCloudErrorPopup + _G.NewFileSearch = originalFileSearch + main.filterBuildList = originalFilterBuildList + io.open = originalOpen + end) + + it("filters a recursive index without rescanning files", function() + searches["files:Builds/*.xml"] = { { name = "Root.xml", modified = 1 } } + searches["folders:Builds/*"] = { { name = "League", modified = 2 } } + searches["files:Builds/League/*.xml"] = { { name = "Nested.xml", modified = 3 } } + fileHeaders["Builds/Root.xml"] = '' + fileHeaders["Builds/League/Nested.xml"] = '' + + local index = buildListHelpers.ScanFolder("") + local scansAfterIndex = searchCount + local opensAfterIndex = fileOpenCount + local directEntries = buildListHelpers.FilterList(index, "", "") + local classMatches = buildListHelpers.FilterList(index, "", "CLASS:invoker") + + assert.are.same(3, #index) + assert.are.same(2, #directEntries) + assert.are.same("Nested.xml", classMatches[1].fileName) + assert.are.same("League/", classMatches[1].subPath) + assert.are.same(scansAfterIndex, searchCount) + assert.are.same(opensAfterIndex, fileOpenCount) + end) + + it("filters the startup build list without rescanning files", function() + searches["files:Builds/*.xml"] = { { name = "Root.xml", modified = 1 } } + searches["folders:Builds/*"] = { { name = "League", modified = 2 } } + searches["files:Builds/League/*.xml"] = { { name = "Nested.xml", modified = 3 } } + fileHeaders["Builds/Root.xml"] = '' + fileHeaders["Builds/League/Nested.xml"] = '' + local listMode = LoadModule("Modules/BuildList") + + listMode:Init() + local scansAfterIndex = searchCount + listMode.controls.searchText.changeFunc("class:invoker") + + assert.are.same(scansAfterIndex, searchCount) + assert.are.same(1, #listMode.list) + assert.are.same("Nested.xml", listMode.list[1].fileName) + assert.are.same("(e.g. class:invoker myfilename)", listMode.controls.searchText.placeholder) + assert.are.same("Builds/League/Nested[2].xml", listMode:GetDestName("League/", "Nested.xml")) + local opensBeforeReturn = fileOpenCount + + searches["files:Builds/Root.xml"] = { { name = "Root.xml", modified = 4 } } + fileHeaders["Builds/Root.xml"] = '' + main.filterBuildList = "" + listMode:Init("Root", "") + + assert.are.same(scansAfterIndex + 1, searchCount) + assert.are.same(opensBeforeReturn + 1, fileOpenCount) + assert.are.same(81, listMode.controls.buildList.selValue.level) + assert.are.same("Invoker", listMode.controls.buildList.selValue.ascendClassName) + assert.are.same(4, listMode.controls.buildList.selValue.modified) + + local scansBeforeFolderChange = searchCount + listMode:Init("Nested", "League/") + assert.are.same(scansBeforeFolderChange + 2, searchCount) + assert.are.same("Builds/League/Nested.xml", listMode.controls.buildList.selValue.fullFileName) + end) + + it("reports cloud read failures", function() + searches["files:Builds/*.xml"] = { { name = "Cloud.xml" } } + fileHeaders["Builds/Cloud.xml"] = false + + local index = buildListHelpers.ScanFolder("") + + assert.are.same(0, #index) + assert.are.same("Builds/Cloud.xml", cloudErrorPath) + end) + + it("blocks descendant folder targets and selects duplicate filenames by path", function() + local rootFolder = { folderName = "Alpha", subPath = "" } + local childFolder = { folderName = "Beta", subPath = "Alpha/" } + local firstBuild = { fileName = "Same.xml", subPath = "Alpha/", fullFileName = "Builds/Alpha/Same.xml" } + local secondBuild = { fileName = "Same.xml", subPath = "Other/", fullFileName = "Builds/Other/Same.xml" } + local listMode = { + list = { firstBuild, secondBuild }, + subPath = "", + BuildList = function() end, + } + local control = new("BuildListControl"):BuildListControl(nil, { 0, 0, 500, 500 }, listMode) + + control:SelByFullFileName("Builds/Other/Same.xml") + + assert.are.same(secondBuild, control.selValue) + assert.is_false(control:CanDragToValue(1, childFolder, { selValue = rootFolder })) + assert.is_true(buildListHelpers.CanMoveToSubPath(rootFolder, "Other/")) + control:SelByFullFileName("Builds/Missing.xml") + assert.is_nil(control.selValue) + end) +end) diff --git a/src/Classes/BuildListControl.lua b/src/Classes/BuildListControl.lua index 65e2083c7d..7c3ec18b53 100644 --- a/src/Classes/BuildListControl.lua +++ b/src/Classes/BuildListControl.lua @@ -5,6 +5,7 @@ -- local ipairs = ipairs local s_format = string.format +local buildListHelpers = LoadModule("Modules/BuildListHelpers") ---@class BuildListControl: ListControl local BuildListClass = newClass("BuildListControl", "ListControl") @@ -35,13 +36,21 @@ function BuildListClass:BuildListControl(anchor, rect, listMode) function self.controls.path:ReceiveDrag(type, build, source) if type == "Build" then for index, folder in ipairs(self.folderList) do - if index < #self.folderList and folder.button:IsMouseOver() then - if build.folderName then - main:MoveFolder(build.folderName, main.buildPath..build.subPath, main.buildPath..folder.path) - else - os.rename(build.fullFileName, listMode:GetDestName(folder.path, build.fileName)) + if folder.button:IsMouseOver() then + if buildListHelpers.CanMoveToSubPath(build, folder.path) then + if build.folderName then + main:MoveFolder(build.folderName, main.buildPath..build.subPath, main.buildPath..folder.path) + else + local destPath = listMode:GetDestName(folder.path, build.fileName) + local res, msg = os.rename(build.fullFileName, destPath) + if not res then + main:OpenMessagePopup("Error", "Couldn't move '"..build.fullFileName.."' to '"..destPath.."': "..(msg or "")) + return + end + end + listMode:BuildList() end - listMode:BuildList() + break end end end @@ -53,18 +62,22 @@ function BuildListClass:BuildListControl(anchor, rect, listMode) return self end -function BuildListClass:SelByFileName(selFileName) - for index, build in ipairs(self.list) do - if build.fileName == selFileName then - self:SelectIndex(index) - break +function BuildListClass:SelByFullFileName(fullFileName) + if fullFileName then + for index, build in ipairs(self.list) do + if build.fullFileName == fullFileName then + self:SelectIndex(index) + return + end end end + self.selIndex = nil + self.selValue = nil end function BuildListClass:LoadBuild(build) if build.folderName then - self.controls.path:SetSubPath(self.listMode.subPath .. build.folderName .. "/") + self.controls.path:SetSubPath(build.subPath .. build.folderName .. "/") else main:SetMode("BUILD", build.fullFileName, build.buildName) end @@ -133,7 +146,7 @@ function BuildListClass:RenameBuild(build, copyOnName) end end self.listMode:BuildList() - self:SelByFileName(newFileName) + self:SelByFullFileName(main.buildPath..build.subPath..newFileName) end main:ClosePopup() self.listMode:SelectControl(self) @@ -178,12 +191,21 @@ end function BuildListClass:GetRowValue(column, index, build) if column == 1 then local label + local subPathPrefix = "" + if build.subPath and self.listMode and self.listMode.subPath and build.subPath ~= self.listMode.subPath then + local baseSub = self.listMode.subPath + if build.subPath:sub(1, #baseSub) == baseSub then + subPathPrefix = build.subPath:sub(#baseSub + 1) + else + subPathPrefix = build.subPath + end + end if build.folderName then - label = ">> " .. build.folderName + label = ">> " .. subPathPrefix .. build.folderName else - label = build.buildName or "?" + label = subPathPrefix .. (build.buildName or "?") end - if self.cutBuild and self.cutBuild.buildName == build.buildName and self.cutBuild.folderName == build.folderName then + if self.cutBuild and self.cutBuild.buildName == build.buildName and self.cutBuild.folderName == build.folderName and self.cutBuild.subPath == build.subPath then return "^xC0B0B0"..label else return label @@ -212,18 +234,26 @@ end function BuildListClass:ReceiveDrag(type, build, source) if type == "Build" then if self.hoverValue and self.hoverValue.folderName then - if build.folderName then - main:MoveFolder(build.folderName, main.buildPath..build.subPath, main.buildPath..self.hoverValue.subPath..self.hoverValue.folderName.."/") - else - os.rename(build.fullFileName, self.listMode:GetDestName(self.listMode.subPath..self.hoverValue.folderName.."/", build.fileName)) + local targetSubPath = self.hoverValue.subPath .. self.hoverValue.folderName .. "/" + if buildListHelpers.CanMoveToSubPath(build, targetSubPath) then + if build.folderName then + main:MoveFolder(build.folderName, main.buildPath..build.subPath, main.buildPath..targetSubPath) + else + local destPath = self.listMode:GetDestName(targetSubPath, build.fileName) + local res, msg = os.rename(build.fullFileName, destPath) + if not res then + main:OpenMessagePopup("Error", "Couldn't move '"..build.fullFileName.."' to '"..destPath.."': "..(msg or "")) + return + end + end + self.listMode:BuildList() end - self.listMode:BuildList() end end end function BuildListClass:CanDragToValue(index, build, source) - return build.folderName and source.selValue ~= build + return build.folderName and source.selValue ~= build and buildListHelpers.CanMoveToSubPath(source.selValue, build.subPath .. build.folderName .. "/") end function BuildListClass:OnSelClick(index, build, doubleClick) diff --git a/src/Classes/CompareTab.lua b/src/Classes/CompareTab.lua index 24ce2fca35..b9de34ec03 100644 --- a/src/Classes/CompareTab.lua +++ b/src/Classes/CompareTab.lua @@ -1593,12 +1593,22 @@ function CompareTabClass:OpenImportFolderPopup() controls = { }, } function listHost:BuildList() + self.buildIndex = buildListHelpers.ScanFolder(self.subPath) + self:FilterBuildList() + end + function listHost:FilterBuildList() wipeTable(self.list) - local scanned = buildListHelpers.ScanFolder(self.subPath, searchText) - for _, entry in ipairs(scanned) do + for _, entry in ipairs(buildListHelpers.FilterList(self.buildIndex, self.subPath, searchText)) do t_insert(self.list, entry) end + self:SortList() + end + function listHost:SortList() + local selectedFullFileName = controls.buildList and controls.buildList.selValue and controls.buildList.selValue.fullFileName buildListHelpers.SortList(self.list, sortMode) + if controls.buildList then + controls.buildList:SelByFullFileName(selectedFullFileName) + end end function listHost:SelectControl(control) -- Focus is managed by the popup's ControlHost; this is a no-op for the popup list. @@ -1627,12 +1637,13 @@ function CompareTabClass:OpenImportFolderPopup() -- Search box and sort dropdown sit above the build list. controls.searchText = new("EditControl"):EditControl({ "TOPLEFT", nil, "TOPLEFT" }, { 15, 25, 450, 20 }, "", "Search", "%c%(%)", 100, function(buf) searchText = buf - listHost:BuildList() + listHost:FilterBuildList() end, nil, nil, true) + controls.searchText:SetPlaceholder("(e.g. class:invoker myfilename)") controls.sort = new("DropDownControl"):DropDownControl({ "TOPLEFT", nil, "TOPLEFT" }, { 475, 25, 210, 20 }, buildListHelpers.buildSortDropList, function(index, value) sortMode = value.sortMode main.buildSortMode = value.sortMode - buildListHelpers.SortList(listHost.list, sortMode) + listHost:SortList() end) controls.sort:SelByValue(sortMode, "sortMode") @@ -1645,7 +1656,7 @@ function CompareTabClass:OpenImportFolderPopup() -- navigate folders, import builds, and suppress rename/delete/drag behaviors. function controls.buildList:LoadBuild(build) if build.folderName then - self.controls.path:SetSubPath(self.listMode.subPath .. build.folderName .. "/") + self.controls.path:SetSubPath(build.subPath .. build.folderName .. "/") else importBuildEntry(build) end diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index 22ddd51e3e..ff60d9dedd 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -405,6 +405,10 @@ function EditClass:Draw(viewPort, noTooltip) DrawImage(nil, caretX, textY, 1, textHeight) end else + if self.buf == '' and self.placeholder then + SetDrawColor(self.disableCol) + DrawString(textX, textY, "LEFT", textHeight, self.font, self.placeholder) + end local pre = self.textCol .. self.buf:sub(1, self.caret - 1) local post = self.buf:sub(self.caret) if self.protected then diff --git a/src/Modules/BuildList.lua b/src/Modules/BuildList.lua index 9162fdc076..176065891f 100644 --- a/src/Modules/BuildList.lua +++ b/src/Modules/BuildList.lua @@ -14,16 +14,25 @@ local listMode = new("ControlHost"):ControlHost() function listMode:Init(selBuildName, subPath) if self.initialised then + local subPathChanged = subPath and subPath ~= self.subPath + local reuseBuildIndex = not subPathChanged and self.indexedBuildPath == main.buildPath self.subPath = subPath or self.subPath self.controls.buildList.controls.path:SetSubPath(self.subPath) - self.controls.buildList:SelByFileName(selBuildName and selBuildName..".xml") --if main.showPublicBuilds then if false then self.controls.ExtBuildList = self:getPublicBuilds() else self.controls.ExtBuildList = nil end - self:BuildList() + if reuseBuildIndex then + if selBuildName then + buildListHelpers.RefreshBuild(self.buildIndex, self.subPath, selBuildName..".xml") + end + self:FilterBuildList() + elseif not subPathChanged then + self:BuildList() + end + self.controls.buildList:SelByFullFileName(selBuildName and main.buildPath..self.subPath..selBuildName..".xml") self:SelectControl(self.controls.buildList) return end @@ -95,13 +104,14 @@ function listMode:Init(selBuildName, subPath) self.controls.searchText = new("EditControl"):EditControl({ "TOP", self.anchor, "TOP" }, { 0, 25, 640, 20 }, self.filterBuildList, "Search", "%c%(%)", 100, function(buf) main.filterBuildList = buf - self:BuildList() + self:FilterBuildList() end, nil, nil, true) + self.controls.searchText:SetPlaceholder("(e.g. class:invoker myfilename)") self.controls.searchText.width = buildListWidth self.controls.searchText.x = buildListOffset self:BuildList() - self.controls.buildList:SelByFileName(selBuildName and selBuildName..".xml") + self.controls.buildList:SelByFullFileName(selBuildName and main.buildPath..self.subPath..selBuildName..".xml") self:SelectControl(self.controls.buildList) self.initialised = true @@ -138,8 +148,10 @@ function listMode:OnFrame(inputEvents) if self.controls.buildList.copyBuild then local build = self.controls.buildList.copyBuild if build.subPath ~= self.subPath then - if build.folderName then - main:CopyFolder(build.folderName, main.buildPath..build.subPath, main.buildPath..self.subPath) + if not buildListHelpers.CanMoveToSubPath(build, self.subPath) then + main:OpenMessagePopup("Error", "A folder cannot be copied into itself.") + elseif build.folderName then + main:CopyFolder(build.fullFileName, main.buildPath..self.subPath..build.folderName) else copyFile(build.fullFileName, self:GetDestName(self.subPath, build.fileName)) end @@ -151,7 +163,9 @@ function listMode:OnFrame(inputEvents) elseif self.controls.buildList.cutBuild then local build = self.controls.buildList.cutBuild if build.subPath ~= self.subPath then - if build.folderName then + if not buildListHelpers.CanMoveToSubPath(build, self.subPath) then + main:OpenMessagePopup("Error", "A folder cannot be moved into itself.") + elseif build.folderName then main:MoveFolder(build.folderName, main.buildPath..build.subPath, main.buildPath..self.subPath) else os.rename(build.fullFileName, self:GetDestName(self.subPath, build.fileName)) @@ -180,9 +194,11 @@ function listMode:GetDestName(subPath, fileName) local i = 2 local destName = fileName while true do - local test = io.open(destName, "r") + local test = io.open(main.buildPath..subPath..destName, "r") if test then - destName = fileName .. "[" .. i .. "]" + test:close() + local baseName = fileName:gsub("%.xml$", "") + destName = baseName .. "[" .. i .. "].xml" i = i + 1 else break @@ -192,20 +208,23 @@ function listMode:GetDestName(subPath, fileName) end function listMode:BuildList() + self.indexedBuildPath = main.buildPath + self.buildIndex = buildListHelpers.ScanFolder(self.subPath) + self:FilterBuildList() +end + +function listMode:FilterBuildList() wipeTable(self.list) - local scanned = buildListHelpers.ScanFolder(self.subPath, main.filterBuildList or "") - for _, entry in ipairs(scanned) do + for _, entry in ipairs(buildListHelpers.FilterList(self.buildIndex, self.subPath, main.filterBuildList)) do t_insert(self.list, entry) end self:SortList() end function listMode:SortList() - local oldSelFileName = self.controls.buildList.selValue and self.controls.buildList.selValue.fileName + local oldSelFullFileName = self.controls.buildList.selValue and self.controls.buildList.selValue.fullFileName buildListHelpers.SortList(self.list, main.buildSortMode) - if oldSelFileName then - self.controls.buildList:SelByFileName(oldSelFileName) - end + self.controls.buildList:SelByFullFileName(oldSelFullFileName) end return listMode diff --git a/src/Modules/BuildListHelpers.lua b/src/Modules/BuildListHelpers.lua index aefc34f174..b8e45114eb 100644 --- a/src/Modules/BuildListHelpers.lua +++ b/src/Modules/BuildListHelpers.lua @@ -14,68 +14,155 @@ local buildSortDropList = { { label = "Sort by Level", sortMode = "LEVEL"}, } --- Scan main.buildPath..subPath for .xml builds and sub-folders. --- filterText is an optional substring filter applied to build filenames. --- Returns a freshly allocated list of entries in the shape used by BuildListControl. --- On cloud-read failure opens main:OpenCloudErrorPopup and returns whatever has been --- collected so far (matching the prior in-module behavior in Modules/BuildList). -local function ScanFolder(subPath, filterText) +local function ReadBuildHeader(fullFileName) + local fileHnd = io.open(fullFileName, "r") + if not fileHnd then return { } end + local headerText = fileHnd:read(2048) + fileHnd:close() + if not headerText then + main:OpenCloudErrorPopup(fullFileName) + return + end + + local buildTag = headerText:match("]->") + if not buildTag then return { } end + + return { + level = tonumber(buildTag:match('level="([^"]+)"')), + className = buildTag:match('className="([^"]+)"'), + ascendClassName = buildTag:match('ascendClassName="([^"]+)"'), + } +end + +local function ReadBuild(subPath, fileName, modified) + local fullFileName = main.buildPath..subPath..fileName + local header = ReadBuildHeader(fullFileName) + if not header then return end + return { + fileName = fileName, + subPath = subPath, + fullFileName = fullFileName, + modified = modified, + buildName = fileName:gsub("%.xml$",""), + level = header.level, + className = header.className, + ascendClassName = header.ascendClassName, + } +end + +local function MatchEntry(entry, terms) + for _, term in ipairs(terms) do + local termLower = term:lower() + local val = termLower:match("^class:(.*)$") + if val then + if val ~= "" then + local match = (entry.className and entry.className:lower():find(val, 1, true)) or + (entry.ascendClassName and entry.ascendClassName:lower():find(val, 1, true)) or + (entry.folderName and entry.folderName:lower():find(val, 1, true)) + if not match then return false end + end + else + local match = (entry.buildName and entry.buildName:lower():find(termLower, 1, true)) or + (entry.folderName and entry.folderName:lower():find(termLower, 1, true)) or + (entry.className and entry.className:lower():find(termLower, 1, true)) or + (entry.ascendClassName and entry.ascendClassName:lower():find(termLower, 1, true)) or + (entry.subPath and entry.subPath:lower():find(termLower, 1, true)) + if not match then return false end + end + end + return true +end + +-- Recursively index builds and folders below main.buildPath..subPath. +local function ScanFolder(subPath) subPath = subPath or "" - filterText = filterText or "" local list = { } - local handle - if filterText ~= "" then - handle = NewFileSearch(main.buildPath..subPath.."*"..filterText.."*.xml") - else - handle = NewFileSearch(main.buildPath..subPath.."*.xml") + + local function scanDir(currentSubPath) + local handle = NewFileSearch(main.buildPath..currentSubPath.."*.xml") + while handle do + local fileName = handle:GetFileName() + local build = ReadBuild(currentSubPath, fileName, handle:GetFileModifiedTime()) + if not build then return false end + t_insert(list, build) + + if not handle:NextFile() then break end + end + + handle = NewFileSearch(main.buildPath..currentSubPath.."*", true) + local subFolders = { } + while handle do + local folderName = handle:GetFileName() + local modified = handle:GetFileModifiedTime() + t_insert(subFolders, { name = folderName, modified = modified }) + if not handle:NextFile() then break end + end + + for _, folder in ipairs(subFolders) do + local folderName = folder.name + local nextSubPath = currentSubPath .. folderName .. "/" + t_insert(list, { + folderName = folderName, + subPath = currentSubPath, + fullFileName = main.buildPath..currentSubPath..folderName, + modified = folder.modified + }) + if not scanDir(nextSubPath) then return false end + end + return true end - while handle do - local fileName = handle:GetFileName() - local build = { } - build.fileName = fileName - build.subPath = subPath - build.fullFileName = main.buildPath..subPath..fileName - build.modified = handle:GetFileModifiedTime() - build.buildName = fileName:gsub("%.xml$","") - local fileHnd = io.open(build.fullFileName, "r") - if fileHnd then - local fileText = fileHnd:read("*a") - fileHnd:close() - if not fileText then - main:OpenCloudErrorPopup(build.fullFileName) - return list - end - fileText = fileText:match("()") - if fileText then - local xml = common.xml.ParseXML(fileText.."") - if xml and xml[1] then - build.level = tonumber(xml[1].attrib.level) - build.className = xml[1].attrib.className - build.ascendClassName = xml[1].attrib.ascendClassName - end + + scanDir(subPath) + return list +end + +local function RefreshBuild(index, subPath, fileName) + local fullFileName = main.buildPath..subPath..fileName + local handle = NewFileSearch(fullFileName) + local build = handle and ReadBuild(subPath, fileName, handle:GetFileModifiedTime()) + if handle and not build then return end + + for buildIndex, entry in ipairs(index) do + if entry.fullFileName == fullFileName then + if build then + index[buildIndex] = build + else + table.remove(index, buildIndex) end - end - t_insert(list, build) - if not handle:NextFile() then - break + return end end - handle = NewFileSearch(main.buildPath..subPath.."*", true) - while handle do - local folderName = handle:GetFileName() - t_insert(list, { - folderName = folderName, - subPath = subPath, - fullFileName = main.buildPath..subPath..folderName, - modified = handle:GetFileModifiedTime() - }) - if not handle:NextFile() then - break + if build then + t_insert(index, build) + end +end + +-- Filtering the cached index avoids filesystem work on every keystroke. +local function FilterList(index, subPath, filterText) + local terms = { } + for term in (filterText or ""):gmatch("%S+") do + t_insert(terms, term) + end + + local list = { } + for _, entry in ipairs(index or { }) do + if (#terms == 0 and entry.subPath == subPath) or (#terms > 0 and MatchEntry(entry, terms)) then + t_insert(list, entry) end end return list end +local function CanMoveToSubPath(build, targetSubPath) + if build.subPath == targetSubPath then return false end + if build.folderName then + -- MoveFolder and CopyFolder recurse, so their destination cannot be inside the source. + local sourceSubPath = build.subPath .. build.folderName .. "/" + if targetSubPath:sub(1, #sourceSubPath) == sourceSubPath then return false end + end + return true +end + -- Sort the given list in place using the same rules as the startup build list. -- sortMode: "NAME" (default), "CLASS", "EDITED", or "LEVEL". local function SortList(list, sortMode) @@ -135,6 +222,9 @@ end return { buildSortDropList = buildSortDropList, + CanMoveToSubPath = CanMoveToSubPath, + FilterList = FilterList, + RefreshBuild = RefreshBuild, ScanFolder = ScanFolder, SortList = SortList, }