diff --git a/src/command/Commands.js b/src/command/Commands.js index 0a1b0ca195e..95d67dba21e 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -77,6 +77,7 @@ define(function (require, exports, module) { // Navigate exports.NAVIGATE_NEXT_DOC = "navigate.nextDoc"; exports.NAVIGATE_PREV_DOC = "navigate.prevDoc"; + exports.NAVIGATE_SHOW_IN_FILE_TREE = "navigate.showInFileTree"; exports.NAVIGATE_QUICK_OPEN = "navigate.quickOpen"; exports.NAVIGATE_GOTO_DEFINITION = "navigate.gotoDefinition"; exports.NAVIGATE_GOTO_LINE = "navigate.gotoLine"; diff --git a/src/command/Menus.js b/src/command/Menus.js index 7b0121748f0..9130cdc743d 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -921,6 +921,8 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.NAVIGATE_PREV_DOC, [{key: "Ctrl-Shift-Tab", platform: "win"}, {key: "Ctrl-Shift-Tab", platform: "mac"}]); menu.addMenuDivider(); + menu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); + menu.addMenuDivider(); menu.addMenuItem(Commands.TOGGLE_QUICK_EDIT, "Ctrl-E"); menu.addMenuItem(Commands.QUICK_EDIT_PREV_MATCH, {key: "Alt-Up", displayKey: "Alt-\u2191"}); menu.addMenuItem(Commands.QUICK_EDIT_NEXT_MATCH, {key: "Alt-Down", displayKey: "Alt-\u2193"}); diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 6995654c1f2..0945e93721f 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -816,6 +816,10 @@ define(function (require, exports, module) { goNextPrevDoc(-1); } + function handleShowInTree() { + ProjectManager.showInTree(DocumentManager.getCurrentDocument().file); + } + function init($titleContainerToolbar) { _$titleContainerToolbar = $titleContainerToolbar; @@ -833,16 +837,18 @@ define(function (require, exports, module) { CommandManager.register(Strings.CMD_FILE_NEW_FOLDER, Commands.FILE_NEW_FOLDER, handleNewFolderInProject); CommandManager.register(Strings.CMD_FILE_SAVE, Commands.FILE_SAVE, handleFileSave); CommandManager.register(Strings.CMD_FILE_SAVE_ALL, Commands.FILE_SAVE_ALL, handleFileSaveAll); - + CommandManager.register(Strings.CMD_FILE_RENAME, Commands.FILE_RENAME, handleFileRename); + CommandManager.register(Strings.CMD_FILE_CLOSE, Commands.FILE_CLOSE, handleFileClose); CommandManager.register(Strings.CMD_FILE_CLOSE_ALL, Commands.FILE_CLOSE_ALL, handleFileCloseAll); - CommandManager.register(Strings.CMD_FILE_RENAME, Commands.FILE_RENAME, handleFileRename); CommandManager.register(Strings.CMD_CLOSE_WINDOW, Commands.FILE_CLOSE_WINDOW, handleFileCloseWindow); CommandManager.register(Strings.CMD_QUIT, Commands.FILE_QUIT, handleFileQuit); CommandManager.register(Strings.CMD_REFRESH_WINDOW, Commands.DEBUG_REFRESH_WINDOW, handleFileReload); + CommandManager.register(Strings.CMD_ABORT_QUIT, Commands.APP_ABORT_QUIT, _handleAbortQuit); + CommandManager.register(Strings.CMD_NEXT_DOC, Commands.NAVIGATE_NEXT_DOC, handleGoNextDoc); CommandManager.register(Strings.CMD_PREV_DOC, Commands.NAVIGATE_PREV_DOC, handleGoPrevDoc); - CommandManager.register(Strings.CMD_ABORT_QUIT, Commands.APP_ABORT_QUIT, _handleAbortQuit); + CommandManager.register(Strings.CMD_SHOW_IN_TREE, Commands.NAVIGATE_SHOW_IN_FILE_TREE, handleShowInTree); // Listen for changes that require updating the editor titlebar $(DocumentManager).on("dirtyFlagChange", handleDirtyChange); diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 7c2abb8cb02..1e19bd4e568 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -80,6 +80,7 @@ define(function (require, exports, module) { FileUtils = require("file/FileUtils"), CommandManager = require("command/CommandManager"), Async = require("utils/Async"), + CollectionUtils = require("utils/CollectionUtils"), PerfUtils = require("utils/PerfUtils"), Commands = require("command/Commands"); @@ -171,13 +172,9 @@ define(function (require, exports, module) { function findInWorkingSet(fullPath, list) { list = list || _workingSet; - var ret = -1; - var found = list.some(function findByPath(file, i) { - ret = i; - return file.fullPath === fullPath; - }); - - return (found ? ret : -1); + return CollectionUtils.indexOf(list, function (file, i) { + return file.fullPath === fullPath; + }); } /** diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 77c6ce06611..fe70fdbd498 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -195,6 +195,7 @@ define({ "CMD_QUICK_EDIT_NEXT_MATCH" : "Next Match", "CMD_NEXT_DOC" : "Next Document", "CMD_PREV_DOC" : "Previous Document", + "CMD_SHOW_IN_TREE" : "Show in File Tree", // Debug menu commands "DEBUG_MENU" : "Debug", diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 29172c92029..e6096a2e7e9 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -60,6 +60,7 @@ define(function (require, exports, module) { FileViewController = require("project/FileViewController"), PerfUtils = require("utils/PerfUtils"), ViewUtils = require("utils/ViewUtils"), + CollectionUtils = require("utils/CollectionUtils"), FileUtils = require("file/FileUtils"), Urls = require("i18n!nls/urls"), KeyEvent = require("utils/KeyEvent"); @@ -765,7 +766,76 @@ define(function (require, exports, module) { return result.promise(); } - + + + /** + * Returns the tree node corresponding to the given file/folder. Returns null if the path lies + * outside the project, or if it doesn't exist. + * + * @param {!Entry} entry FileEntry of DirectoryEntry to show + * @return {$.Promise} Resolved with jQ obj for the jsTree tree node; or rejected if not found + */ + function _findTreeNode(entry) { + var result = new $.Deferred(); + + // If path not within project, ignore + var projRelativePath = makeProjectRelativeIfPossible(entry.fullPath); + if (projRelativePath === entry.fullPath) { + return result.reject().promise(); + } + + var treeAPI = $.jstree._reference(_projectTree); + + // We're going to traverse from root of tree, one segment at a time + var pathSegments = projRelativePath.split("/"); + + function findInSubtree($nodes, segmentI) { + var seg = pathSegments[segmentI]; + var match = CollectionUtils.indexOf($nodes, function (node, i) { + var nodeName = $(node).data("entry").name; + return nodeName === seg; + }); + + if (match === -1) { + result.reject(); // path doesn't exist + } else { + var $node = $nodes.eq(match); + if (segmentI === pathSegments.length - 1) { + result.resolve($node); // done searching! + } else { + // Search next level down + var subChildren = treeAPI._get_children($node); + if (subChildren.length > 0) { + findInSubtree(subChildren, segmentI + 1); + } else { + // Subtree not loaded yet: force async load & try again + treeAPI.load_node($node, function (data) { + subChildren = treeAPI._get_children($node); + findInSubtree(subChildren, segmentI + 1); + }, function (err) { + result.reject(); // includes case where folder is empty + }); + } + } + } + } + + // Begin searching from root + var topLevelNodes = treeAPI._get_children(-1); // -1 means top level in jsTree-ese + findInSubtree(topLevelNodes, 0); + + return result.promise(); + } + + function showInTree(fileEntry) { + _findTreeNode(fileEntry) + .done(function ($node) { + // jsTree will automatically expand parent nodes to ensure visible + _projectTree.jstree("select_node", $node, false); + }); + } + + /** * Open a new project. Currently, Brackets must always have a project open, so * this method handles both closing the current project and opening a new project. @@ -1194,4 +1264,5 @@ define(function (require, exports, module) { exports.createNewItem = createNewItem; exports.renameSelectedItem = renameSelectedItem; exports.forceFinishRename = forceFinishRename; + exports.showInTree = showInTree; }); diff --git a/src/utils/CollectionUtils.js b/src/utils/CollectionUtils.js new file mode 100644 index 00000000000..e26bda47808 --- /dev/null +++ b/src/utils/CollectionUtils.js @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, $ */ + +/** + * Utilities functions related to data collections (arrays & maps) + */ +define(function (require, exports, module) { + "use strict"; + + /** + * Returns the first index in 'array' for which isMatch() returns true, or -1 if none + * @param {!Array.<*>|jQueryObject} array + * @param {!function(*, Number):boolean} isMatch Passed (item, index), same as with forEach() + */ + function indexOf(array, isMatch) { + // Old-fashioned loop, instead of Array.some, to support jQuery "arrays" + var i; + for (i = 0; i < array.length; i++) { + if (isMatch(array[i], i)) { + return i; + } + } + return -1; + } + + + // Define public API + exports.indexOf = indexOf; +});