diff --git a/src/lib/openFolder.js b/src/lib/openFolder.js index ed177c6f7..1b3f28d37 100644 --- a/src/lib/openFolder.js +++ b/src/lib/openFolder.js @@ -429,6 +429,7 @@ async function handleContextmenu(type, url, name, $target) { */ function execOperation(type, action, url, $target, name) { const { clipBoard, $node, remove, url: rootUrl } = openFolder.find(url); + const parentUrl = normalizeFolderUrl(Url.dirname(url)); const startLoading = () => $node.$title.classList.add("loading"); const stopLoading = () => $node.$title.classList.remove("loading"); @@ -441,7 +442,7 @@ function execOperation(type, action, url, $target, name) { return deleteFile(); case "rename": - return renameFile(); + return renameFile(parentUrl); case "paste": return paste(); @@ -626,7 +627,7 @@ function execOperation(type, action, url, $target, name) { FileList.remove(url); } - async function renameFile() { + async function renameFile(parentUrl) { if (isTermuxSafUri(url) && !helpers.isFile(type)) { alert(strings.warning, strings["rename not supported"]); return; @@ -655,12 +656,7 @@ function execOperation(type, action, url, $target, name) { } newName = Url.basename(newUrl); - $target.querySelector(":scope>.text").textContent = newName; - $target.dataset.url = newUrl; - $target.dataset.name = newName; if (helpers.isFile(type)) { - $target.querySelector(":scope>span").className = - helpers.getIconForFile(newName); let file = editorManager.getFile(url, "uri"); if (file) { file.uri = newUrl; @@ -668,12 +664,16 @@ function execOperation(type, action, url, $target, name) { } } else { helpers.updateUriOfAllActiveFiles(url, newUrl); - //Reloading the folder by collapsing and expanding the folder - $target.click(); //collapse - $target.click(); //expand } - toast(strings.success); + FileList.rename(url, newUrl); + await refreshRenamedEntryInOpenFolders( + url, + newUrl, + parentUrl, + normalizeFolderUrl(Url.dirname(newUrl)), + ); + toast(strings.success); } async function createNew() { @@ -1063,11 +1063,27 @@ function appendEntryToOpenFolder(parentUrl, entryUrl, type) { }); } +/** + * Normalize folder URLs for DOM/state lookup. + * Keeps roots intact while removing a trailing slash from non-root folders. + * @param {string} url + * @returns {string} + */ +function normalizeFolderUrl(url) { + if (!url) return url; + const { url: parsedUrl, query } = Url.parse(url); + if (parsedUrl.endsWith("/") && Url.pathname(parsedUrl) !== "/") { + return parsedUrl.slice(0, -1) + query; + } + return parsedUrl + query; +} + /** * Refresh matching expanded folder views. * @param {string} folderUrl */ async function refreshOpenFolder(folderUrl) { + folderUrl = normalizeFolderUrl(folderUrl); const filesApp = sidebarApps.get("files"); const $els = filesApp.getAll(`[data-url="${folderUrl}"]`); @@ -1085,6 +1101,86 @@ async function refreshOpenFolder(folderUrl) { ); } +/** + * Move saved expanded-state keys after a rename. + * Supports both exact folder matches and expanded descendants. + * @param {string} oldUrl + * @param {string} newUrl + */ +function migrateOpenFolderStateUrls(oldUrl, newUrl) { + if (!oldUrl || !newUrl || oldUrl === newUrl) return; + + const getEntries = (listState) => { + if (!listState) return []; + if (listState instanceof Map) return Array.from(listState.entries()); + return Object.entries(listState); + }; + const setEntry = (listState, key, value) => { + if (listState instanceof Map) { + listState.set(key, value); + return; + } + listState[key] = value; + }; + const deleteEntry = (listState, key) => { + if (listState instanceof Map) { + listState.delete(key); + return; + } + delete listState[key]; + }; + + addedFolder.forEach(({ listState }) => { + const matchingEntries = getEntries(listState).filter(([folderUrl]) => { + return folderUrl === oldUrl || folderUrl.startsWith(`${oldUrl}/`); + }); + + if (!matchingEntries.length) return; + + matchingEntries.forEach(([folderUrl, isExpanded]) => { + deleteEntry(listState, folderUrl); + setEntry(listState, newUrl + folderUrl.slice(oldUrl.length), isExpanded); + }); + }); +} + +/** + * Refresh the minimal set of affected parent folders after a rename/move so FileTree state stays in sync with the filesystem. + * If an ancestor folder is already being refreshed, descendant folders are skipped because they will be rebuilt with the ancestor. + * @param {string} oldUrl + * @param {string} newUrl + */ +async function refreshRenamedEntryInOpenFolders( + oldUrl, + newUrl, + oldParentUrl = normalizeFolderUrl(Url.dirname(oldUrl)), + newParentUrl = normalizeFolderUrl(Url.dirname(newUrl)), +) { + if (!oldUrl || !newUrl || oldUrl === newUrl) return; + + migrateOpenFolderStateUrls( + normalizeFolderUrl(oldUrl), + normalizeFolderUrl(newUrl), + ); + + const isAncestorOrSame = (ancestor, descendant) => { + if (ancestor === descendant) return true; + return descendant.startsWith(`${ancestor}/`); + }; + + const parentUrls = Array.from( + new Set([oldParentUrl, newParentUrl].map(normalizeFolderUrl)), + ) + .filter(Boolean) + .filter((parentUrl, index, allParentUrls) => { + return !allParentUrls.some((otherUrl, otherIndex) => { + return otherIndex !== index && isAncestorOrSame(otherUrl, parentUrl); + }); + }); + + await Promise.all(parentUrls.map(refreshOpenFolder)); +} + /** * Create a folder tile * @param {string} name @@ -1134,27 +1230,8 @@ openFolder.add = async (url, type) => { openFolder.renameItem = (oldFile, newFile, newFilename) => { FileList.rename(oldFile, newFile); - helpers.updateUriOfAllActiveFiles(oldFile, newFile); - - const filesApp = sidebarApps.get("files"); - const $els = filesApp.getAll(`[data-url="${oldFile}"]`); - Array.from($els).forEach(($el) => { - if ($el.dataset.type === "dir") { - $el = $el.$title; - setTimeout(() => { - $el.collapse(); - $el.expand(); - }, 0); - } else { - $el.querySelector(":scope>span").className = - helpers.getIconForFile(newFilename); - } - - $el.dataset.url = newFile; - $el.dataset.name = newFilename; - $el.querySelector(":scope>.text").textContent = newFilename; - }); + refreshRenamedEntryInOpenFolders(oldFile, newFile).catch(helpers.error); }; openFolder.removeItem = (url) => {