/* Code Studio — explorer The project folder tree, its context menu and every file system operation reachable from it. */ /* ═══════════════════════════════════════════════════════════════════ Icons ═══════════════════════════════════════════════════════════════════ */ var CS_SPECIAL_FOLDERS = ['src', 'lib', 'test', 'tests', 'spec', 'config', 'public', 'static', 'assets', 'images', 'img', 'css', 'styles', 'js', 'scripts', 'components', 'views', 'pages', 'templates', 'models', 'controllers', 'api', 'routes', 'middleware', 'utils', 'helpers', 'vendor', 'node_modules', 'build', 'dist', 'out', 'bin', 'docs', 'documentation', 'backend', 'frontend', 'server', 'client', 'database', 'db', 'migrations', 'seeds']; var CS_COLOURED_EXT = ['js', 'ts', 'jsx', 'tsx', 'html', 'htm', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'md', 'py', 'go', 'java', 'c', 'cpp', 'cs', 'php', 'rb', 'rs', 'swift', 'sh', 'bash', 'sql', 'vue', 'agi', 'yaml', 'yml', 'toml', 'ini', 'env', 'gitignore', 'dockerfile']; function getFolderIconClass(folderName){ var name = String(folderName).toLowerCase(); return CS_SPECIAL_FOLDERS.indexOf(name) !== -1 ? name : ""; } function getFileIconClass(ext){ ext = String(ext).toLowerCase(); return CS_COLOURED_EXT.indexOf(ext) !== -1 ? ("file-icon-" + ext) : ""; } /* ═══════════════════════════════════════════════════════════════════ Tree rendering ═══════════════════════════════════════════════════════════════════ */ function renderTreeEntries(entries){ var folders = []; var files = []; (entries || []).forEach(function(entry){ var filename = entry.Filename; var filepath = entry.Filepath; var ext = filename.split(".").pop(); if (entry.IsDir){ folders.push( '
' + '
' + '' + '' + '' + escapeHTMLText(filename) + '' + '
' + '
'); } else { files.push( '
' + '' + '' + '' + escapeHTMLText(filename) + '' + '
'); } }); return folders.join("") + files.join(""); } function openProjectFolder(filedata, rewriteHash = true, restoreSession = true){ if (!filedata || filedata.length == 0) return; var folderpath = filedata[0].filepath; var foldername = filedata[0].filename; currentProjectFolder = folderpath; $("#openFolderTitle").text(foldername); $("#directoryExplorer").html('
Loading…
'); $.get("../system/file_system/listDir?dir=" + encodeURIComponent(folderpath), function(data){ $("#directoryExplorer").html(renderTreeEntries(data)); refreshGitStatus(); pushRecentFolder(folderpath, foldername); //Bring back the tabs this project had open the last time round if (restoreSession) restoreProjectSession(folderpath); }).fail(function(){ $("#directoryExplorer").html('
Unable to read this folder.
'); }); if (rewriteHash){ var currentState = getHashObject(); currentState["folder"] = folderpath; writeHashObject(currentState); } updateFileStatusDisplay(getCurrentFocusedFileData() ? getCurrentFocusedFileData().filepath : null); } function closeProjectFolder(){ currentProjectFolder = null; $("#openFolderTitle").text("No Folder Opened"); $("#directoryExplorer").html( '
No folder is open.' + '
'); var currentState = getHashObject(); delete currentState["folder"]; writeHashObject(currentState); $("#scmBadge").hide(); $("#sbBranch").hide(); $("#scmBody").html('
Open a project folder to see its repository.
'); } function listfolder(folderRow){ var wrapper = $(folderRow).parent(); var folderPath = $(folderRow).data("path"); var existingList = wrapper.children(".dirlist"); if (existingList.length == 0){ wrapper.append('
'); $(folderRow).find(".caret i").attr("class", "caret down icon"); $(folderRow).find(".folder.icon").addClass("open"); $.get("../system/file_system/listDir?dir=" + encodeURIComponent(folderPath), function(data){ wrapper.children(".dirlist").html(renderTreeEntries(data)); refreshGitDecorations(); }); return; } //Already loaded — collapse or expand it if ($(folderRow).find(".caret .down").length > 0 || $(folderRow).find("i.caret.down").length > 0){ existingList.slideUp("fast"); $(folderRow).find(".caret i").attr("class", "caret right icon"); $(folderRow).find(".folder.icon").removeClass("open"); } else { existingList.slideDown("fast"); $(folderRow).find(".caret i").attr("class", "caret down icon"); $(folderRow).find(".folder.icon").addClass("open"); } } function openFileViaDirectoryExplorer(object, event){ $("#directoryExplorer .row").removeClass("selected"); $(object).addClass("selected"); openFile($(object).data("path")); } function refreshFolderTree(){ if (!currentProjectFolder) return; openProjectFolder([{ filename: $("#openFolderTitle").text(), filepath: currentProjectFolder }], false); } /* ═══════════════════════════════════════════════════════════════════ Context menu ═══════════════════════════════════════════════════════════════════ */ function showFolderContextMenu(event, element){ event.preventDefault(); event.stopPropagation(); selectedFolderItem = { element: element, path: $(element).data("path"), name: $(element).data("name"), isDir: String($(element).data("isdir")) === "true" }; $("#directoryExplorer .row").removeClass("selected"); $(element).addClass("selected"); var items = [ { label: "New File", tip: "Ctrl+N", action: "newFileInFolder()" }, { label: "New Folder", action: "newFolderInFolder()" }, { divider: true }, { label: "Open", action: "openSelectedItem()", disabled: selectedFolderItem.isDir }, { label: "Open to the Side", action: "openSelectedItemToSide()", disabled: selectedFolderItem.isDir }, { divider: true }, { label: "Rename", tip: "F2", action: "renameSelectedItem()" }, { label: "Delete", tip: "Del", action: "deleteSelectedItem()" }, { divider: true }, { label: "Compare With…", action: "compareSelectedItem()" }, { divider: true }, { label: "Copy Path", action: "copyFilePath()" }, { label: "Reveal in File Manager", action: "revealInFileManager()" } ]; showFloatingMenu("#folderContextMenu", items, event.clientX, event.clientY); } function showDirectoryExplorerContextMenu(event){ if ($(event.target).attr("id") !== "directoryExplorer") return; if (!currentProjectFolder) return; event.preventDefault(); event.stopPropagation(); selectedFolderItem = { element: null, path: currentProjectFolder, name: $("#openFolderTitle").text(), isDir: true }; var items = [ { label: "New File", action: "newFileInFolder()" }, { label: "New Folder", action: "newFolderInFolder()" }, { divider: true }, { label: "Refresh Explorer", action: "refreshFolderTree()" }, { label: "Forget Saved Tabs", action: "forgetProjectSession()" }, { label: "Close Folder", action: "closeProjectFolder()" } ]; showFloatingMenu("#folderContextMenu", items, event.clientX, event.clientY); } function hideFolderContextMenu(){ $("#folderContextMenu").hide(); } function openSelectedItem(){ if (!selectedFolderItem || selectedFolderItem.isDir) return; openFile(selectedFolderItem.path); } function openSelectedItemToSide(){ if (!selectedFolderItem || selectedFolderItem.isDir) return; var path = selectedFolderItem.path; if (editors.length < 2){ splitEditor(); //The new group boots asynchronously — open once Monaco reports back setTimeout(function(){ openFile(path, true, editors[editors.length - 1]); }, 400); } else { openFile(path, true, editors[editors.length - 1]); } } function compareSelectedItem(){ if (!selectedFolderItem) return; openTool("compare", undefined, { type: selectedFolderItem.isDir ? "folder" : "pick", left: selectedFolderItem.path, right: "" }); } /* ═══════════════════════════════════════════════════════════════════ File operations ═══════════════════════════════════════════════════════════════════ */ //Folder that a new item should be created in, based on the current selection function targetFolderForNewItem(){ var targetPath = currentProjectFolder; if (selectedFolderItem){ if (selectedFolderItem.isDir){ targetPath = selectedFolderItem.path; } else { var parts = selectedFolderItem.path.split("/"); parts.pop(); targetPath = parts.join("/"); } } return targetPath; } function joinVirtualPath(folder, name){ if (!folder.endsWith("/")) folder += "/"; return folder + name; } function newFileInFolder(){ if (!currentProjectFolder){ newUntitledFile(); return; } var filename = prompt("Enter new file name:", "newfile.txt"); if (!filename || filename.trim() == "") return; var filepath = joinVirtualPath(targetFolderForNewItem(), filename.trim()); ao_module_agirun("Code Studio/backend/fileOps.agi", { opr: "newFile", filepath: filepath }, function(result){ if (result.error){ alert("Failed to create file: " + result.error); return; } refreshFolderTree(); openFile(filepath); setStatusMessage("checkmark", "File created"); }); } function newFolderInFolder(){ if (!currentProjectFolder){ alert("Open a project folder first."); return; } var foldername = prompt("Enter new folder name:", "NewFolder"); if (!foldername || foldername.trim() == "") return; var folderpath = joinVirtualPath(targetFolderForNewItem(), foldername.trim()); ao_module_agirun("Code Studio/backend/fileOps.agi", { opr: "newFolder", folderpath: folderpath }, function(result){ if (result.error){ alert("Failed to create folder: " + result.error); return; } refreshFolderTree(); setStatusMessage("checkmark", "Folder created"); }); } function renameSelectedItem(){ if (!selectedFolderItem) return; var newName = prompt("Enter new name:", selectedFolderItem.name); if (!newName || newName.trim() == "" || newName == selectedFolderItem.name) return; var oldPath = selectedFolderItem.path; var parts = oldPath.split("/"); parts.pop(); var newPath = parts.join("/") + "/" + newName.trim(); ao_module_agirun("Code Studio/backend/fileOps.agi", { opr: "rename", oldpath: oldPath, newpath: newPath }, function(result){ if (result.error){ alert("Failed to rename: " + result.error); return; } refreshFolderTree(); updateOpenTabsAfterRename(oldPath, newPath); setStatusMessage("checkmark", "Renamed"); }); } function deleteSelectedItem(){ if (!selectedFolderItem) return; var itemType = selectedFolderItem.isDir ? "folder" : "file"; if (!confirm("Are you sure you want to delete this " + itemType + "?\n" + selectedFolderItem.name)) return; ao_module_agirun("Code Studio/backend/fileOps.agi", { opr: "delete", filepath: selectedFolderItem.path }, function(result){ if (result.error){ alert("Failed to delete: " + result.error); return; } closeTabsWithPath(selectedFolderItem.path); refreshFolderTree(); setStatusMessage("checkmark", "Deleted"); }); } function copyFilePath(){ if (!selectedFolderItem) return; copyToClipboard(selectedFolderItem.path, function(copied){ if (copied){ setStatusMessage("copy", "Path copied to clipboard"); } else { //Nothing reached the clipboard — at least let the user read the path prompt("Copy the path below:", selectedFolderItem.path); } }); } function revealInFileManager(){ if (!selectedFolderItem) return; var parts = selectedFolderItem.path.split("/"); var filename = parts.pop(); ao_module_openPath(parts.join("/"), filename); } function updateOpenTabsAfterRename(oldPath, newPath){ editors.forEach(function(entry){ entry.tabs.forEach(function(tab){ if (!tab.filepath) return; if (tab.filepath === oldPath || tab.filepath.startsWith(oldPath + "/")){ tab.filepath = tab.filepath.replace(oldPath, newPath); tab.filename = tab.filepath.split("/").pop(); } }); }); renderAllTabs(); } function closeTabsWithPath(path){ editors.slice().forEach(function(entry){ entry.tabs.slice().forEach(function(tab){ if (!tab.filepath) return; if (tab.filepath === path || tab.filepath.startsWith(path + "/")){ closeTabWithUUIDAndEditorID(tab.tabUUID, entry.uuid); } }); }); } function renderAllTabs(){ editors.forEach(function(entry){ entry.tabs.forEach(function(tab){ var selector = '[uuid="' + tab.tabUUID + '"]'; $(".tabs .item" + selector).attr("filepath", tab.filepath || "").attr("title", tab.filepath || tab.filename); $(".tabs .item" + selector + " .tabFilename, #openeditors .row" + selector + " .tabFilename").text(tab.filename); }); }); } /* ═══════════════════════════════════════════════════════════════════ Drag and drop inside the tree ═══════════════════════════════════════════════════════════════════ */ function folderDragOver(event){ event.preventDefault(); event.stopPropagation(); $(event.currentTarget).addClass("drag-over"); } function folderDragLeave(event){ event.preventDefault(); event.stopPropagation(); $(event.currentTarget).removeClass("drag-over"); } function folderDrop(event){ event.preventDefault(); event.stopPropagation(); $(event.currentTarget).removeClass("drag-over"); var sourcePath = event.dataTransfer.getData("filepath"); var sourceFilename = event.dataTransfer.getData("filename"); var destFolder = $(event.currentTarget).data("path"); if (!sourcePath || !destFolder) return; var sourceParts = sourcePath.split("/"); sourceParts.pop(); if (sourceParts.join("/") === destFolder) return; //same folder, nothing to do moveFileToFolder(sourcePath, sourceFilename, destFolder); } function directoryExplorerDrop(event){ event.preventDefault(); event.stopPropagation(); $("#directoryExplorer").removeClass("drag-over"); var sourcePath = event.dataTransfer.getData("filepath"); var sourceFilename = event.dataTransfer.getData("filename"); if (!sourcePath || !currentProjectFolder) return; moveFileToFolder(sourcePath, sourceFilename, currentProjectFolder); } function moveFileToFolder(sourcePath, sourceFilename, destFolder){ ao_module_agirun("Code Studio/backend/fileOps.agi", { opr: "move", sourcepath: sourcePath, destfolder: destFolder }, function(result){ if (result.error){ alert("Failed to move: " + result.error); return; } refreshFolderTree(); updateOpenTabsAfterRename(sourcePath, joinVirtualPath(destFolder, sourceFilename)); setStatusMessage("checkmark", "Moved"); }); } /* ═══════════════════════════════════════════════════════════════════ Explorer keyboard shortcuts ═══════════════════════════════════════════════════════════════════ */ $(document).on("keydown", function(event){ //Only act when the tree, not an editor or a text field, has the focus if ($(event.target).is("input, textarea")) return; if (!selectedFolderItem) return; if ($("#directoryExplorer .row.selected").length == 0) return; if (event.key === "F2"){ event.preventDefault(); renameSelectedItem(); } else if (event.key === "Delete"){ event.preventDefault(); deleteSelectedItem(); } }); $(document).ready(function(){ $("#directoryExplorer").attr("ondragover", "allowDrop(event)"); $("#directoryExplorer").attr("ondrop", "directoryExplorerDrop(event)"); });