diff --git a/src/command/Commands.js b/src/command/Commands.js index caa7c4bd14..edf3bd1fd4 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -534,6 +534,9 @@ define(function (require, exports, module) { /** Closes unmodified files */ exports.CMD_GIT_CLOSE_UNMODIFIED = "git-close-unmodified-files"; + /** Opens all modified/untracked files */ + exports.CMD_GIT_OPEN_CHANGED_FILES = "git-open-changed-files"; + /** Checks out a branch or commit */ exports.CMD_GIT_CHECKOUT = "git-checkout"; diff --git a/src/extensions/default/Git/src/Branch.js b/src/extensions/default/Git/src/Branch.js index 51f3236c98..905779e9c9 100644 --- a/src/extensions/default/Git/src/Branch.js +++ b/src/extensions/default/Git/src/Branch.js @@ -32,6 +32,9 @@ define(function (require, exports) { currentEditor, $dropdown; + let lastRenderedBranchName = null; + let $dropdownAnchor = null; + function renderList(branches) { branches = branches.map(function (name) { return { @@ -303,6 +306,7 @@ define(function (require, exports) { function attachCloseEvents() { $("html").on("click", closeDropdown); $("#project-files-container").on("scroll", closeDropdown); + $("#git-panel .table-container").on("scroll", closeDropdown); $("#titlebar .nav").on("click", closeDropdown); currentEditor = EditorManager.getCurrentFullEditor(); @@ -316,6 +320,7 @@ define(function (require, exports) { function detachCloseEvents() { $("html").off("click", closeDropdown); $("#project-files-container").off("scroll", closeDropdown); + $("#git-panel .table-container").off("scroll", closeDropdown); $("#titlebar .nav").off("click", closeDropdown); if (currentEditor) { @@ -325,15 +330,74 @@ define(function (require, exports) { // $(window).off("keydown", keydownHook); $dropdown = null; + $dropdownAnchor = null; + } + + function _positionDropdownBelow($toggle) { + // two margins to account for the preceding project dropdown as well + const marginLeft = (parseInt($toggle.css("margin-left"), 10) * 2) || 0; + + const toggleOffset = $toggle.offset(); + + $dropdown + .css({ + left: toggleOffset.left - marginLeft + 3, + top: toggleOffset.top + $toggle.outerHeight() - 3 + }) + .appendTo($("body")); + + // fix so it doesn't overflow the screen + const maxHeight = $dropdown.parent().height(), + height = $dropdown.height(), + topOffset = $dropdown.position().top; + if (height + topOffset >= maxHeight - 10) { + $dropdown.css("bottom", "10px"); + } + } + + function _positionDropdownAbove($anchor) { + const anchorOffset = $anchor.offset(); + + $dropdown + .css({ + left: anchorOffset.left, + // #git-branch-dropdown carries a negative margin-left meant for the + // sidebar toggle alignment, neutralize it so left matches the anchor + "margin-left": 0, + // the .dropdown-menu class positions with "top: 100%", it has to be + // explicitly overridden or the bottom positioning below is over-constrained + top: "auto", + bottom: $(window).height() - anchorOffset.top + 3, + // grow upwards from the anchor instead of the default top-left origin + "transform-origin": "0 100%" + }) + .appendTo($("body")); + + // fix so it doesn't overflow the screen + if ($dropdown.height() >= anchorOffset.top - 10) { + $dropdown.css("top", "10px"); + } + + const rightOverflow = $dropdown.offset().left + $dropdown.outerWidth() - ($(window).width() - 10); + if (rightOverflow > 0) { + $dropdown.css("left", anchorOffset.left - rightOverflow); + } } function toggleDropdown(e) { e.stopPropagation(); + // currentTarget is only valid while the event is being dispatched, + // so it has to be captured before the async branch listing below + const $anchor = $(e.currentTarget); - // If the dropdown is already visible, close it + // clicking the anchor that opened the dropdown closes it, clicking the + // other anchor moves the dropdown there if ($dropdown) { + const sameAnchor = $dropdownAnchor && $dropdownAnchor[0] === $anchor[0]; closeDropdown(); - return; + if (sameAnchor) { + return; + } } Menus.closeAll(); @@ -341,6 +405,9 @@ define(function (require, exports) { Git.getBranches().catch(function (err) { ErrorHandler.showError(err, Strings.ERROR_GETTING_BRANCH_LIST); }).then(function (branches = []) { + if ($dropdown) { + return; + } branches = branches.reduce(function (arr, branch) { if (!branch.currentBranch && !branch.remote) { arr.push(branch.name); @@ -349,25 +416,12 @@ define(function (require, exports) { }, []); $dropdown = $(renderList(branches)); - const $toggle = $("#git-branch-dropdown-toggle"); - // two margins to account for the preceding project dropdown as well - const marginLeft = (parseInt($toggle.css("margin-left"), 10) * 2) || 0; - - const toggleOffset = $toggle.offset(); - - $dropdown - .css({ - left: toggleOffset.left - marginLeft + 3, - top: toggleOffset.top + $toggle.outerHeight() - 3 - }) - .appendTo($("body")); - - // fix so it doesn't overflow the screen - var maxHeight = $dropdown.parent().height(), - height = $dropdown.height(), - topOffset = $dropdown.position().top; - if (height + topOffset >= maxHeight - 10) { - $dropdown.css("bottom", "10px"); + $dropdownAnchor = $anchor; + if ($anchor.closest("#git-panel").length) { + // the git panel sits at the bottom of the screen, open upwards from there + _positionDropdownAbove($anchor); + } else { + _positionDropdownBelow($("#git-branch-dropdown-toggle")); } PopUpManager.addPopUp($dropdown, detachCloseEvents, true, {closeCurrentPopups: true}); @@ -409,10 +463,9 @@ define(function (require, exports) { return; } - var branchInHead = m[1], - branchInUi = $gitBranchName.text(); + const branchInHead = m[1]; - if (branchInHead !== branchInUi) { + if (branchInHead !== lastRenderedBranchName) { refresh(); } }); @@ -426,6 +479,7 @@ define(function (require, exports) { .text("\u2026") .parent() .show(); + Panel.setBranchName("\u2026", ""); return Git.getGitRoot().then(function (gitRoot) { var projectRoot = Utils.getProjectRoot(), @@ -437,9 +491,12 @@ define(function (require, exports) { Preferences.set("currentGitRoot", projectRoot); Preferences.set("currentGitSubfolder", ""); + lastRenderedBranchName = null; $gitBranchName .off("click") - .text("not a git repo"); + .text(Strings.GIT_NOT_A_REPO); + Panel.setBranchName(Strings.GIT_NOT_A_REPO, ""); + $("#git-panel .git-panel-branch").removeClass("clickable").off("click"); Panel.disable("not-repo"); return; @@ -471,17 +528,26 @@ define(function (require, exports) { EventEmitter.emit(Events.REBASE_MERGE_MODE, mergeInfo.rebaseMode, mergeInfo.mergeMode); - var MAX_LEN = 18; + const MAX_LEN = 18; + lastRenderedBranchName = branchName; const tooltip = StringUtils.format(Strings.ON_BRANCH, branchName); - const html = ` ${ - branchName.length > MAX_LEN ? branchName.substring(0, MAX_LEN) + "\u2026" : branchName - }`; + const displayName = branchName.length > MAX_LEN + ? branchName.substring(0, MAX_LEN) + "\u2026" + : branchName; + // branch names may contain characters like "<", so set them + // as text and never as html $gitBranchName - .html(html) + .text(" " + displayName) + .prepend('') .attr("title", tooltip) .off("click") .on("click", toggleDropdown); + Panel.setBranchName(displayName, tooltip); + $("#git-panel .git-panel-branch") + .addClass("clickable") + .off("click") + .on("click", toggleDropdown); Panel.enable(); }).catch(function (err) { @@ -490,9 +556,12 @@ define(function (require, exports) { }).catch(function (ex) { if (ErrorHandler.contains(ex, "unknown revision")) { + lastRenderedBranchName = null; $gitBranchName .off("click") - .text("no branch"); + .text(Strings.GIT_NO_BRANCH); + Panel.setBranchName(Strings.GIT_NO_BRANCH, ""); + $("#git-panel .git-panel-branch").removeClass("clickable").off("click"); Panel.enable(); } else { throw ex; diff --git a/src/extensions/default/Git/src/Constants.js b/src/extensions/default/Git/src/Constants.js index 2f851d7a52..5f7699d7e0 100644 --- a/src/extensions/default/Git/src/Constants.js +++ b/src/extensions/default/Git/src/Constants.js @@ -36,6 +36,7 @@ define(function (require, exports) { exports.CMD_GIT_CLONE_WITH_URL = Commands.CMD_GIT_CLONE_WITH_URL; exports.CMD_GIT_SETTINGS_COMMAND_ID = Commands.CMD_GIT_SETTINGS_COMMAND_ID; exports.CMD_GIT_CLOSE_UNMODIFIED = Commands.CMD_GIT_CLOSE_UNMODIFIED; + exports.CMD_GIT_OPEN_CHANGED_FILES = Commands.CMD_GIT_OPEN_CHANGED_FILES; exports.CMD_GIT_CHECKOUT = Commands.CMD_GIT_CHECKOUT; exports.CMD_GIT_RESET_HARD = Commands.CMD_GIT_RESET_HARD; exports.CMD_GIT_RESET_SOFT = Commands.CMD_GIT_RESET_SOFT; diff --git a/src/extensions/default/Git/src/Main.js b/src/extensions/default/Git/src/Main.js index 16249c4ca4..15e8aedb14 100644 --- a/src/extensions/default/Git/src/Main.js +++ b/src/extensions/default/Git/src/Main.js @@ -241,6 +241,7 @@ define(function (require, exports) { Constants.CMD_GIT_RESET_SOFT, // "More options" context menu commands + Constants.CMD_GIT_OPEN_CHANGED_FILES, Constants.CMD_GIT_DISCARD_ALL_CHANGES, Constants.CMD_GIT_UNDO_LAST_COMMIT, Constants.CMD_GIT_TOGGLE_UNTRACKED, @@ -322,6 +323,8 @@ define(function (require, exports) { // create context menu for git more options const optionsCmenu = Menus.registerContextMenu(Constants.GIT_PANEL_OPTIONS_CMENU); Menus.ContextMenu.assignContextMenuToSelector(".git-more-options-btn", optionsCmenu); + optionsCmenu.addMenuItem(Constants.CMD_GIT_OPEN_CHANGED_FILES); + optionsCmenu.addMenuDivider(); optionsCmenu.addMenuItem(Constants.CMD_GIT_DISCARD_ALL_CHANGES); optionsCmenu.addMenuItem(Constants.CMD_GIT_UNDO_LAST_COMMIT); optionsCmenu.addMenuDivider(); @@ -398,6 +401,7 @@ define(function (require, exports) { Utils.enableCommand(Constants.CMD_GIT_GOTO_NEXT_CHANGE, enabled); Utils.enableCommand(Constants.CMD_GIT_GOTO_PREVIOUS_CHANGE, enabled); Utils.enableCommand(Constants.CMD_GIT_CLOSE_UNMODIFIED, enabled); + Utils.enableCommand(Constants.CMD_GIT_OPEN_CHANGED_FILES, enabled); Utils.enableCommand(Constants.CMD_GIT_AUTHORS_OF_SELECTION, enabled); Utils.enableCommand(Constants.CMD_GIT_AUTHORS_OF_FILE, enabled); diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 33a5c3b547..d40bde040e 100644 --- a/src/extensions/default/Git/src/Panel.js +++ b/src/extensions/default/Git/src/Panel.js @@ -15,6 +15,7 @@ define(function (require, exports) { Menus = brackets.getModule("command/Menus"), Mustache = brackets.getModule("thirdparty/mustache/mustache"), FindInFiles = brackets.getModule("search/FindInFiles"), + MainViewManager = brackets.getModule("view/MainViewManager"), WorkspaceManager = brackets.getModule("view/WorkspaceManager"), ProjectManager = brackets.getModule("project/ProjectManager"), StringUtils = brackets.getModule("utils/StringUtils"), @@ -851,6 +852,14 @@ define(function (require, exports) { } } + // called by Branch.refresh() with the already truncated display name so the panel + // indicator always mirrors the sidebar one, including decorations like "main|MERGING" + function setBranchName(branchName, tooltip) { + const $branch = $gitPanel.find(".git-panel-branch"); + $branch.attr("title", tooltip || ""); + $branch.find(".git-branch-name").text(branchName); + } + function refreshCommitCounts() { // Find Push and Pull buttons var $pullBtn = $gitPanel.find(".git-pull"); @@ -1007,6 +1016,64 @@ define(function (require, exports) { }); } + const OPEN_ALL_CONFIRM_THRESHOLD = 50; + + function _openChangedFiles(fileList) { + MainViewManager.addListToWorkingSet(MainViewManager.ACTIVE_PANE, fileList); + if (!MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE)) { + CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileList[0].fullPath }); + } + } + + function openAllChangedFiles() { + return Git.status().then(function (files) { + files = _.filter(files, function (file) { + if (!shouldShow(file)) { + return false; + } + // deleted files no longer exist on disk, so there is nothing to open + if (file.status.indexOf(Git.FILE_STATUS.DELETED) !== -1) { + return false; + } + // respect the "show untracked files" panel toggle + if (!showingUntracked && file.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1) { + return false; + } + return true; + }); + + if (files.length === 0) { + return; + } + + const currentGitRoot = Preferences.get("currentGitRoot"); + const fileList = files.map(function (file) { + return FileSystem.getFileForPath(currentGitRoot + file.file); + }); + + // count only the files the command would actually add, so re-running it + // when most of them are already open doesn't ask for confirmation again + const newFileCount = _.filter(fileList, function (file) { + return MainViewManager.findInAllWorkingSets(file.fullPath).length === 0; + }).length; + + if (newFileCount > OPEN_ALL_CONFIRM_THRESHOLD) { + return Utils.askQuestion(Strings.CMD_OPEN_CHANGED_FILES, + StringUtils.format(Strings.OPEN_CHANGED_FILES_CONFIRM, newFileCount), + { booleanResponse: true }) + .then(function (response) { + if (response === true) { + _openChangedFiles(fileList); + } + }); + } + + _openChangedFiles(fileList); + }).catch(function (err) { + ErrorHandler.showError(err, Strings.ERROR_OPENING_CHANGED_FILES); + }); + } + var lastCheckOneClicked = null; function attachDefaultTableHandlers() { @@ -1214,6 +1281,7 @@ define(function (require, exports) { const mainToolbarWidth = $mainToolbar.width(); let overFlowWidth = 540; const breakpoints = [ + { width: 600, className: "hide-when-medium" }, { width: overFlowWidth, className: "hide-when-small" }, { width: 400, className: "hide-when-x-small" } ]; @@ -1345,6 +1413,8 @@ define(function (require, exports) { CommandManager.register(Strings.VIEW_AUTHORS_SELECTION, Constants.CMD_GIT_AUTHORS_OF_SELECTION, handleAuthorsSelection); CommandManager.register(Strings.VIEW_AUTHORS_FILE, Constants.CMD_GIT_AUTHORS_OF_FILE, handleAuthorsFile); CommandManager.register(Strings.HIDE_UNTRACKED, Constants.CMD_GIT_TOGGLE_UNTRACKED, handleToggleUntracked); + CommandManager.register(Strings.CMD_OPEN_CHANGED_FILES, + Constants.CMD_GIT_OPEN_CHANGED_FILES, openAllChangedFiles); CommandManager.register(Strings.GIT_INIT, Constants.CMD_GIT_INIT, EventEmitter.getEmitter(Events.HANDLE_GIT_INIT)); CommandManager.register(Strings.GIT_CLONE, Constants.CMD_GIT_CLONE, EventEmitter.getEmitter(Events.HANDLE_GIT_CLONE)); CommandManager.register(Strings.GIT_SHOW_HISTORY, Constants.CMD_GIT_HISTORY_GLOBAL, ()=>{ @@ -1555,6 +1625,7 @@ define(function (require, exports) { exports.toggle = toggle; exports.enable = enable; exports.disable = disable; + exports.setBranchName = setBranchName; exports.getSelectedHistoryCommit = getSelectedHistoryCommit; exports.getPanel = function () { return $gitPanel; }; diff --git a/src/extensions/default/Git/styles/git-styles.less b/src/extensions/default/Git/styles/git-styles.less index 0cf885384b..035dce6841 100644 --- a/src/extensions/default/Git/styles/git-styles.less +++ b/src/extensions/default/Git/styles/git-styles.less @@ -1088,6 +1088,28 @@ right: 32px; top: 5px; } + .git-panel-branch { + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + height: 22px; + margin-top: 2px; + margin-right: 8px; + padding: 2px 10px; + font-size: 12px; + line-height: 16px; + white-space: nowrap; + cursor: default; + i { + margin-right: 4px; + } + &:not(.clickable) { + opacity: .5; + } + &.clickable { + cursor: pointer; + } + } .octicon:not(:only-child) { margin-right: 5px; vertical-align: -1px; diff --git a/src/extensions/default/Git/templates/git-panel.html b/src/extensions/default/Git/templates/git-panel.html index b0133c2c87..132d7cce07 100644 --- a/src/extensions/default/Git/templates/git-panel.html +++ b/src/extensions/default/Git/templates/git-panel.html @@ -57,6 +57,9 @@