diff --git a/src/extensions/default/Git/styles/git-styles.less b/src/extensions/default/Git/styles/git-styles.less
index 534fbc1d69..4b98e72795 100644
--- a/src/extensions/default/Git/styles/git-styles.less
+++ b/src/extensions/default/Git/styles/git-styles.less
@@ -907,6 +907,7 @@
white-space: nowrap;
padding: 2px 5px;
margin-left: -5px;
+ margin-right: 2em;
.dropdown-arrow {
display: inline-block;
width: 7px;
diff --git a/src/extensionsIntegrated/CollapseFolders/main.js b/src/extensionsIntegrated/CollapseFolders/main.js
new file mode 100644
index 0000000000..98dd4f3bbb
--- /dev/null
+++ b/src/extensionsIntegrated/CollapseFolders/main.js
@@ -0,0 +1,80 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
+ *
+ */
+
+/* Displays a Collapse button in the sidebar area */
+/* when the button gets clicked, it closes all the directories recursively that are opened */
+/* Styling for the button is done in `../../styles/Extn-CollapseFolders.less` */
+define(function (require, exports, module) {
+ const AppInit = require("utils/AppInit");
+ const ProjectManager = require("project/ProjectManager");
+
+ /**
+ * This is the main function that handles the closing of all the directories
+ */
+ function handleCollapseBtnClick() {
+ // this will give us an array of array's
+ // the root level directories will be at index 0, its next level will be at index 1 and so on
+ const openNodes = ProjectManager._actionCreator.model.getOpenNodes();
+ if (!openNodes || openNodes.length === 0) {
+ return;
+ }
+
+ // traversing from the back because the deepest nested directories should be closed first
+ // Note: this is an array of all the directories at the deepest level
+ for (let i = openNodes.length - 1; i >= 0; i--) {
+ // close all the directories
+ openNodes[i].forEach(function (folderPath) {
+ try {
+ // to close each dir
+ ProjectManager._actionCreator.setDirectoryOpen(folderPath, false);
+ } catch (error) {
+ console.error("Failed to close folder:", folderPath, error);
+ }
+ });
+ }
+ }
+
+ /**
+ * This function is responsible to create the 'Collapse All' button
+ * and append it to the sidebar area on the project-files-header
+ */
+ function createCollapseButton() {
+ const $projectFilesHeader = $("#project-files-header");
+ // make sure that we were able to get the project-files-header DOM element
+ if ($projectFilesHeader.length === 0) {
+ return;
+ }
+
+ // create the collapse btn
+ const $collapseBtn = $(`
+
+
+
+
+ `);
+
+ $collapseBtn.on("click", handleCollapseBtnClick);
+ $projectFilesHeader.append($collapseBtn); // append the btn to the project-files-header
+ }
+
+ AppInit.appReady(function () {
+ createCollapseButton();
+ });
+});
diff --git a/src/extensionsIntegrated/loader.js b/src/extensionsIntegrated/loader.js
index 776a81dd0f..523db814bb 100644
--- a/src/extensionsIntegrated/loader.js
+++ b/src/extensionsIntegrated/loader.js
@@ -45,4 +45,5 @@ define(function (require, exports, module) {
require("./CSSColorPreview/main");
require("./TabBar/main");
require("./CustomSnippets/main");
+ require("./CollapseFolders/main");
});
diff --git a/src/styles/Extn-CollapseFolders.less b/src/styles/Extn-CollapseFolders.less
new file mode 100644
index 0000000000..ecdfaab1ca
--- /dev/null
+++ b/src/styles/Extn-CollapseFolders.less
@@ -0,0 +1,25 @@
+#collapse-folders {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0.2em 0.65em;
+ margin-top: 0.1em;
+ position: absolute !important;
+ right: 0;
+ opacity: 0;
+ visibility: hidden;
+ transition:
+ opacity 0.2s ease-in-out,
+ visibility 0.2s ease-in-out;
+
+ .collapse-icon {
+ font-size: 0.5em;
+ line-height: 1;
+ }
+}
+
+#sidebar:hover #collapse-folders {
+ opacity: 1;
+ visibility: visible;
+}
diff --git a/src/styles/brackets.less b/src/styles/brackets.less
index 5150753c88..f7100dee51 100644
--- a/src/styles/brackets.less
+++ b/src/styles/brackets.less
@@ -45,6 +45,7 @@
@import "Extn-DisplayShortcuts.less";
@import "Extn-CSSColorPreview.less";
@import "Extn-CustomSnippets.less";
+@import "Extn-CollapseFolders.less";
@import "UserProfile.less";
/* Overall layout */
diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js
index 4fd8b5221d..0afbbd1b16 100644
--- a/test/UnitTestSuite.js
+++ b/test/UnitTestSuite.js
@@ -128,6 +128,7 @@ define(function (require, exports, module) {
require("spec/Extn-JSHint-integ-test");
require("spec/Extn-ESLint-integ-test");
require("spec/Extn-CSSColorPreview-integ-test");
+ require("spec/Extn-CollapseFolders-integ-test");
// extension integration tests
require("spec/Extn-CSSCodeHints-integ-test");
require("spec/Extn-HTMLCodeHints-Lint-integ-test");
diff --git a/test/spec/Extn-CollapseFolders-integ-test.js b/test/spec/Extn-CollapseFolders-integ-test.js
new file mode 100644
index 0000000000..b4bec85493
--- /dev/null
+++ b/test/spec/Extn-CollapseFolders-integ-test.js
@@ -0,0 +1,241 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
+ *
+ */
+
+/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, awaits, jsPromise */
+
+define(function (require, exports, module) {
+ const SpecRunnerUtils = require("spec/SpecRunnerUtils");
+
+ describe("integration:Collapse Folders", function () {
+ let testWindow, ProjectManager, FileSystem, $, testProjectPath, testProjectFolder;
+
+ beforeAll(async function () {
+ // Create the test window
+ testWindow = await SpecRunnerUtils.createTestWindowAndRun();
+ // Get reference to useful modules
+ $ = testWindow.$;
+ ProjectManager = testWindow.brackets.test.ProjectManager;
+ FileSystem = testWindow.brackets.test.FileSystem;
+
+ // Setup a test project folder with nested directories
+ testProjectPath = SpecRunnerUtils.getTempDirectory() + "/collapse-folders-test";
+ testProjectFolder = FileSystem.getDirectoryForPath(testProjectPath);
+
+ // Ensure the test directory exists
+ await SpecRunnerUtils.createTempDirectory();
+
+ // Create test project structure
+ await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath);
+ await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder1");
+ await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder1/subfolder1");
+ await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder2");
+ await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder2/subfolder2");
+
+ // Create some test files
+ await jsPromise(SpecRunnerUtils.createTextFile(testProjectPath + "/file.js", "// Test file", FileSystem));
+ await jsPromise(
+ SpecRunnerUtils.createTextFile(testProjectPath + "/folder1/file1.js", "// Test file 1", FileSystem)
+ );
+ await jsPromise(
+ SpecRunnerUtils.createTextFile(
+ testProjectPath + "/folder1/subfolder1/subfile1.js",
+ "// Test subfile 1",
+ FileSystem
+ )
+ );
+ await jsPromise(
+ SpecRunnerUtils.createTextFile(testProjectPath + "/folder2/file2.js", "// Test file 2", FileSystem)
+ );
+ await jsPromise(
+ SpecRunnerUtils.createTextFile(
+ testProjectPath + "/folder2/subfolder2/subfile2.js",
+ "// Test subfile 2",
+ FileSystem
+ )
+ );
+
+ // Load the test project
+ await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
+ }, 30000);
+
+ afterAll(async function () {
+ testWindow = null;
+ await SpecRunnerUtils.closeTestWindow();
+ await SpecRunnerUtils.removeTempDirectory();
+ }, 30000);
+
+ /**
+ * Helper function to open a folder in the project tree
+ * @param {string} folderPath - The path of the folder to open
+ */
+ async function openFolder(folderPath) {
+ const folderEntry = FileSystem.getDirectoryForPath(folderPath);
+ // Call setDirectoryOpen without awaitsForDone since it doesn't return a promise
+ ProjectManager._actionCreator.setDirectoryOpen(folderEntry.fullPath, true);
+
+ // Wait for the folder to be opened in the UI
+ await awaitsFor(
+ function () {
+ const $folderNode = findDirectoryNode(folderPath);
+ return $folderNode && $folderNode.hasClass("jstree-open");
+ },
+ "Folder to be opened: " + folderPath,
+ 1000
+ );
+ }
+
+ /**
+ * Helper function to find a directory node in the project tree
+ * @param {string} path - The path of the directory to find
+ * @returns {jQuery|null} - The jQuery object for the directory node, or null if not found
+ */
+ function findDirectoryNode(path) {
+ const dirName = path.split("/").pop();
+ const $treeItems = $("#project-files-container li");
+ let $result = null;
+
+ $treeItems.each(function () {
+ const $treeNode = $(this);
+ if ($treeNode.children("a").text().trim() === dirName) {
+ $result = $treeNode;
+ return false; // Break the loop
+ }
+ });
+
+ return $result;
+ }
+
+ /**
+ * Helper function to check if a folder is open in the project tree
+ * @param {string} folderPath - The path of the folder to check
+ * @returns {boolean} - True if the folder is open, false otherwise
+ */
+ function isFolderOpen(folderPath) {
+ const $folderNode = findDirectoryNode(folderPath);
+ // If the folder node can't be found, it's definitely not open
+ if (!$folderNode) {
+ return false;
+ }
+ return $folderNode.hasClass("jstree-open");
+ }
+
+ describe("UI", function () {
+ it("should have a collapse button in the project files header", async function () {
+ // Check if the collapse button exists
+ const $collapseBtn = $("#collapse-folders");
+ expect($collapseBtn.length).toBe(1);
+
+ // Check if the button has the collapse icons
+ expect($collapseBtn.find(".collapse-icon").length).toBe(2);
+ });
+
+ it("should collapse all open folders when the collapse button is clicked", async function () {
+ // Open some folders first
+ await openFolder(testProjectPath + "/folder1");
+ await openFolder(testProjectPath + "/folder2");
+
+ // Verify folders are open
+ expect(isFolderOpen(testProjectPath + "/folder1")).toBe(true);
+ expect(isFolderOpen(testProjectPath + "/folder2")).toBe(true);
+
+ // Click the collapse button
+ $("#collapse-folders").click();
+
+ // Wait for folders to be collapsed
+ await awaitsFor(
+ function () {
+ return (
+ !isFolderOpen(testProjectPath + "/folder1") && !isFolderOpen(testProjectPath + "/folder2")
+ );
+ },
+ "Folders to be collapsed",
+ 1000
+ );
+
+ // Verify folders are closed
+ expect(isFolderOpen(testProjectPath + "/folder1")).toBe(false);
+ expect(isFolderOpen(testProjectPath + "/folder2")).toBe(false);
+ });
+
+ it("should collapse nested folders when the collapse button is clicked", async function () {
+ // Open folders with nested structure
+ await openFolder(testProjectPath + "/folder1");
+ await openFolder(testProjectPath + "/folder1/subfolder1");
+ await openFolder(testProjectPath + "/folder2");
+ await openFolder(testProjectPath + "/folder2/subfolder2");
+
+ // Verify folders are open
+ expect(isFolderOpen(testProjectPath + "/folder1")).toBe(true);
+ expect(isFolderOpen(testProjectPath + "/folder1/subfolder1")).toBe(true);
+ expect(isFolderOpen(testProjectPath + "/folder2")).toBe(true);
+ expect(isFolderOpen(testProjectPath + "/folder2/subfolder2")).toBe(true);
+
+ // Click the collapse button
+ $("#collapse-folders").click();
+
+ // Wait for all folders to be collapsed
+ await awaitsFor(
+ function () {
+ return (
+ !isFolderOpen(testProjectPath + "/folder1") &&
+ !isFolderOpen(testProjectPath + "/folder1/subfolder1") &&
+ !isFolderOpen(testProjectPath + "/folder2") &&
+ !isFolderOpen(testProjectPath + "/folder2/subfolder2")
+ );
+ },
+ "All folders to be collapsed",
+ 1000
+ );
+
+ // Verify all folders are closed
+ expect(isFolderOpen(testProjectPath + "/folder1")).toBe(false);
+ expect(isFolderOpen(testProjectPath + "/folder1/subfolder1")).toBe(false);
+ expect(isFolderOpen(testProjectPath + "/folder2")).toBe(false);
+ expect(isFolderOpen(testProjectPath + "/folder2/subfolder2")).toBe(false);
+ });
+
+ it("should do nothing when no folders are open", async function () {
+ // Make sure all folders are closed
+ $("#collapse-folders").click();
+ await awaitsFor(
+ function () {
+ return (
+ !isFolderOpen(testProjectPath + "/folder1") && !isFolderOpen(testProjectPath + "/folder2")
+ );
+ },
+ "Folders to be collapsed",
+ 1000
+ );
+
+ // Get the current state of the project tree
+ const initialState = $("#project-files-container").html();
+
+ // Click the collapse button again
+ $("#collapse-folders").click();
+
+ // Wait a bit to ensure any potential changes would have happened
+ await awaits(300);
+
+ // Verify the project tree hasn't changed
+ expect($("#project-files-container").html()).toBe(initialState);
+ });
+ });
+ });
+});