diff --git a/src/brackets.config.json b/src/brackets.config.json index 0c4268620db..06794edbb7d 100644 --- a/src/brackets.config.json +++ b/src/brackets.config.json @@ -6,6 +6,7 @@ "about_icon" : "styles/images/brackets_icon.svg", "update_info_url" : "http://dev.brackets.io/updates/stable/", "how_to_use_url" : "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", + "glob_help_url" : "https://github.com/adobe/brackets/wiki/Using-File-Filters", "forum_url" : "https://groups.google.com/forum/?fromgroups#!forum/brackets-dev", "release_notes_url" : "https://github.com/adobe/brackets/wiki/Release-Notes", "report_issue_url" : "https://github.com/adobe/brackets/wiki/How-to-Report-an-Issue", diff --git a/src/brackets.js b/src/brackets.js index 4b766ceddaa..566ae02ef20 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -159,6 +159,8 @@ define(function (require, exports, module) { HTMLInstrumentation : require("language/HTMLInstrumentation"), MultiRangeInlineEditor : require("editor/MultiRangeInlineEditor").MultiRangeInlineEditor, LanguageManager : LanguageManager, + FindInFiles : require("search/FindInFiles"), + FileFilters : require("search/FileFilters"), doneLoading : false }; diff --git a/src/config.json b/src/config.json index e36fa95e914..2d20392c809 100644 --- a/src/config.json +++ b/src/config.json @@ -5,6 +5,7 @@ "about_icon": "styles/images/brackets_icon.svg", "update_info_url": "http://dev.brackets.io/updates/stable/", "how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", + "glob_help_url": "https://github.com/adobe/brackets/wiki/Using-File-Filters", "forum_url": "https://groups.google.com/forum/?fromgroups#!forum/brackets-dev", "release_notes_url": "https://github.com/adobe/brackets/wiki/Release-Notes", "report_issue_url": "https://github.com/adobe/brackets/wiki/How-to-Report-an-Issue", diff --git a/src/editor/CSSInlineEditor.js b/src/editor/CSSInlineEditor.js index 89550074b3f..6e4fe5b4f59 100644 --- a/src/editor/CSSInlineEditor.js +++ b/src/editor/CSSInlineEditor.js @@ -30,23 +30,18 @@ define(function (require, exports, module) { // Load dependent modules var CSSUtils = require("language/CSSUtils"), + DropdownButton = require("widgets/DropdownButton").DropdownButton, CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), DocumentManager = require("document/DocumentManager"), - DropdownEventHandler = require("utils/DropdownEventHandler").DropdownEventHandler, EditorManager = require("editor/EditorManager"), Editor = require("editor/Editor").Editor, - PanelManager = require("view/PanelManager"), ProjectManager = require("project/ProjectManager"), HTMLUtils = require("language/HTMLUtils"), - Menus = require("command/Menus"), MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor"), - PopUpManager = require("widgets/PopUpManager"), Strings = require("strings"), ViewUtils = require("utils/ViewUtils"), _ = require("thirdparty/lodash"); - - var StylesheetsMenuTemplate = require("text!htmlContent/stylesheets-menu.html"); var _newRuleCmd, _newRuleHandlers = []; @@ -102,19 +97,6 @@ define(function (require, exports, module) { return selectorName; } - /** - * @private - * Create the list of stylesheets in the dropdown menu. - * @return {string} The html content - */ - function _renderList(cssFileInfos) { - var templateVars = { - styleSheetList : cssFileInfos - }; - - return Mustache.render(StylesheetsMenuTemplate, templateVars); - } - /** * @private * Add a new rule for the given selector to the given stylesheet, then add the rule to the @@ -147,6 +129,16 @@ define(function (require, exports, module) { } } + /** Item renderer for stylesheet-picker dropdown */ + function _stylesheetListRenderer(item) { + var html = "" + _.escape(item.name); + if (item.subDirStr.length) { + html += " — " + _.escape(item.subDirStr) + ""; + } + html += ""; + return html; + } + /** * This function is registered with EditManager as an inline editor provider. It creates a CSSInlineEditor * when cursor is on an HTML tag name, class attribute, or id attribute, find associated @@ -180,107 +172,14 @@ define(function (require, exports, module) { var result = new $.Deferred(), cssInlineEditor, cssFileInfos = [], - $newRuleButton, - $dropdown, - $dropdownItem, - dropdownEventHandler; - - /** - * @private - * Close the dropdown externally to dropdown, which ultimately calls the - * _cleanupDropdown callback. - */ - function _closeDropdown() { - if (dropdownEventHandler) { - dropdownEventHandler.close(); - } - } - - /** - * @private - * Handle click - */ - function _onClickOutside(event) { - var $container = $(event.target).closest(".stylesheet-dropdown"); - - // If click is outside dropdown list, then close dropdown list - if ($container.length === 0 || $container[0] !== $dropdown[0]) { - _closeDropdown(); - } - } - - /** - * @private - * Remove the various event handlers that close the dropdown. This is called by the - * PopUpManager when the dropdown is closed. - */ - function _cleanupDropdown() { - window.document.body.removeEventListener("click", _onClickOutside, true); - $(hostEditor).off("scroll", _closeDropdown); - $(PanelManager).off("editorAreaResize", _closeDropdown); - dropdownEventHandler = null; - $dropdown = null; - - EditorManager.focusEditor(); - } + newRuleButton; /** * @private * Callback when item from dropdown list is selected - * @param {jQueryObject} $link The `a` element selected with mouse or keyboard */ - function _onSelect($link) { - var path = $link.data("path"); - - if (path) { - _addRule(selectorName, cssInlineEditor, path); - } - } - - /** - * @private - * Show or hide the stylesheets dropdown. - */ - function _showDropdown() { - Menus.closeAll(); - - $dropdown = $(_renderList(cssFileInfos)) - .appendTo($("body")); - - var toggleOffset = $newRuleButton.offset(), - posLeft = toggleOffset.left, - posTop = toggleOffset.top + $newRuleButton.outerHeight(), - elementRect = { - top: posTop, - left: posLeft, - height: $dropdown.height(), - width: $dropdown.width() - }, - clip = ViewUtils.getElementClipSize($(window), elementRect); - - if (clip.bottom > 0) { - // Bottom is clipped, so move entire menu above button - posTop = Math.max(0, toggleOffset.top - $dropdown.height() - 4); - } - - if (clip.right > 0) { - // Right is clipped, so adjust left to fit menu in editor - posLeft = Math.max(0, posLeft - clip.right); - } - - $dropdown.css({ - left: posLeft, - top: posTop - }); - - dropdownEventHandler = new DropdownEventHandler($dropdown, _onSelect, _cleanupDropdown); - dropdownEventHandler.open(); - - $dropdown.focus(); - - window.document.body.addEventListener("click", _onClickOutside, true); - $(hostEditor).on("scroll", _closeDropdown); - $(PanelManager).on("editorAreaResize", _closeDropdown); + function _onDropdownSelect(event, fileInfo) { + _addRule(selectorName, cssInlineEditor, fileInfo.fullPath); } /** @@ -302,7 +201,7 @@ define(function (require, exports, module) { * Update the enablement of associated menu commands. */ function _updateCommands() { - _newRuleCmd.setEnabled(cssInlineEditor.hasFocus() && !$newRuleButton.hasClass("disabled")); + _newRuleCmd.setEnabled(cssInlineEditor.hasFocus() && !newRuleButton.$button.hasClass("disabled")); } /** @@ -310,19 +209,16 @@ define(function (require, exports, module) { * Create a new rule on click. */ function _handleNewRuleClick(e) { - if (!$newRuleButton.hasClass("disabled")) { + if (!newRuleButton.$button.hasClass("disabled")) { if (cssFileInfos.length === 1) { // Just go ahead and create the rule. _addRule(selectorName, cssInlineEditor, cssFileInfos[0].fullPath); - } else if ($dropdown) { - _closeDropdown(); } else { - _showDropdown(); + // Although not attached to button click in 'dropdown mode', this handler can still be + // invoked via the command shortcut. Just toggle dropdown open/closed in that case. + newRuleButton.toggleDropdown(); } } - if (e) { - e.stopPropagation(); - } } /** @@ -388,6 +284,10 @@ define(function (require, exports, module) { return fileInfos; } + function _onHostEditorScroll() { + newRuleButton.closeDropdown(); + } + CSSUtils.findMatchingRules(selectorName, hostEditor.document) .done(function (rules) { var inlineEditorDeferred = new $.Deferred(); @@ -401,17 +301,21 @@ define(function (require, exports, module) { inlineEditorDeferred.resolve(); }); $(cssInlineEditor).on("close", function () { - _closeDropdown(); + newRuleButton.closeDropdown(); + $(hostEditor).off("scroll", _onHostEditorScroll); }); var $header = $(".inline-editor-header", cssInlineEditor.$htmlContent); - $newRuleButton = $(""), + $button = $picker.find("button"); + + function joinBolded(segments) { + return segments + .map(function (seg) { + return "" + _.escape(seg) + ""; + }) + .join(", "); + } + function itemRenderer(filter) { + // Format filter in condensed form + if (filter.length > 2) { + return Strings.FILE_FILTER_LIST_PREFIX + " " + joinBolded(filter.slice(0, 2)) + " " + + StringUtils.format(Strings.FILE_FILTER_CLIPPED_SUFFIX, filter.length - 2); + } else { + return Strings.FILE_FILTER_LIST_PREFIX + " " + joinBolded(filter); + } + } + + function updatePicker() { + var filter = getLastFilter(); + if (filter.length) { + $button.text(Strings.EDIT_FILE_FILTER); + $picker.find(".filter-label").html(itemRenderer(filter)) + .attr("title", filter.join("\n")); + } else { + $button.text(Strings.NO_FILE_FILTER); + $picker.find(".filter-label").html("").attr("title", ""); + } + } + + updatePicker(); + + $button.click(function () { + editFilter(getLastFilter()) + .done(function (buttonId) { + if (buttonId === Dialogs.DIALOG_BTN_OK) { + updatePicker(); + } + }); + }); + + return $picker; + } + + + /** + * Returns false if the given path matches any of the exclusion globs in the given filter. Returns true + * if the path does not match any of the globs. If filtering many paths at once, use filterFileList() + * for much better performance. + * + * @param {!string} compiledFilter 'Compiled' filter object as returned by compile() + * @param {!string} fullPath + * @return {boolean} + */ + function filterPath(compiledFilter, fullPath) { + if (!compiledFilter) { + return true; + } + + var re = new RegExp(compiledFilter); + return !fullPath.match(re); + } + + /** + * Returns a copy of 'files' filtered to just those that don't match any of the exclusion globs in the filter. + * + * @param {!string} compiledFilter 'Compiled' filter object as returned by compile() + * @param {!Array.} files + * @return {!Array.} + */ + function filterFileList(compiledFilter, files) { + if (!compiledFilter) { + return files; + } + + var re = new RegExp(compiledFilter); + return files.filter(function (f) { + return !f.fullPath.match(re); + }); + } + + + exports.createFilterPicker = createFilterPicker; + exports.commitPicker = commitPicker; + exports.getLastFilter = getLastFilter; + exports.editFilter = editFilter; + exports.compile = compile; + exports.filterPath = filterPath; + exports.filterFileList = filterFileList; +}); diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 6673efc93fc..c80d47a0a5b 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -42,14 +42,15 @@ define(function (require, exports, module) { "use strict"; - var _ = require("thirdparty/lodash"); - - var Async = require("utils/Async"), + var _ = require("thirdparty/lodash"), + FileFilters = require("search/FileFilters"), + Async = require("utils/Async"), Resizer = require("utils/Resizer"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), + PreferencesManager = require("preferences/PreferencesManager"), ProjectManager = require("project/ProjectManager"), DocumentModule = require("document/Document"), DocumentManager = require("document/DocumentManager"), @@ -743,24 +744,12 @@ define(function (require, exports, module) { return result.promise(); } - /** - * @private - * Used to filter out image files when building a list of file in which to - * search. Ideally this would filter out ALL binary files. - * @param {FileSystemEntry} entry The entry to test - * @return {boolean} Whether or not the entry's contents should be searched - */ - function _findInFilesFilter(entry) { - var language = LanguageManager.getLanguageForPath(entry.fullPath); - return !language.isBinary(); - } - /** * @private * Executes the Find in Files search inside the 'currentScope' * @param {string} query String to be searched */ - function _doSearch(query) { + function _doSearch(query, userFilter) { currentQuery = query; currentQueryExpr = _getQueryRegExp(query); @@ -773,8 +762,21 @@ define(function (require, exports, module) { var scopeName = currentScope ? currentScope.fullPath : ProjectManager.getProjectRoot().fullPath, perfTimer = PerfUtils.markStart("FindIn: " + scopeName + " - " + query); - ProjectManager.getAllFiles(_findInFilesFilter, true) + /** + * Filters out files that are known binary types (currently just image/audio; ideally we'd filter out ALL binary files). + * @param {FileSystemEntry} entry The entry to test + * @return {boolean} True if the entry's contents should be included in the file list + */ + function fileFilter(entry) { + var language = LanguageManager.getLanguageForPath(entry.fullPath); + return !language.isBinary(); + } + + ProjectManager.getAllFiles(fileFilter, true) .then(function (fileListResult) { + // Filter out files/folders that match user's current exclusion filter + fileListResult = FileFilters.filterFileList(userFilter, fileListResult); + var doSearch = _doSearchInOneFile.bind(undefined, _addSearchMatches); return Async.doInParallel(fileListResult, doSearch); }) @@ -785,6 +787,8 @@ define(function (require, exports, module) { StatusBar.hideBusyIndicator(); PerfUtils.addMeasurement(perfTimer); $(DocumentModule).on("documentChange.findInFiles", _documentChangeHandler); + + exports._searchResults = searchResults; // for unit tests }) .fail(function (err) { console.log("find in files failed: ", err); @@ -823,12 +827,14 @@ define(function (require, exports, module) { if (this.closed) { return; } - + this.modalBar.close(true, !suppressAnimation); + }; + + FindInFilesDialog.prototype._handleClose = function () { // Hide error popup, since it hangs down low enough to make the slide-out look awkward $(".modal-bar .error").hide(); this.closed = true; - this.modalBar.close(true, !suppressAnimation); EditorManager.focusEditor(); dialog = null; }; @@ -852,9 +858,18 @@ define(function (require, exports, module) { // (Any previous open FindInFiles bar instance was already handled by our caller) FindReplace._closeFindBar(); - this.modalBar = new ModalBar(dialogHTML, false); + this.modalBar = new ModalBar(dialogHTML, true); + $(this.modalBar).on("close", this._handleClose.bind(this)); + + // Custom closing behavior: if in the middle of executing search, blur shouldn't close ModalBar yet. And + // don't close bar when opening Edit Filter dialog either. + var self = this; + this.modalBar.isLockedOpen = function () { + return self.getDialogTextField().attr("disabled") || $(".modal.instance .exclusions-editor").length > 0; + }; - var $searchField = $("input#find-what"); + var $searchField = $("input#find-what"), + filterPicker; function handleQueryChange() { // Check the query expression on every input event. This way the user is alerted @@ -881,17 +896,12 @@ define(function (require, exports, module) { } else if (event.keyCode === KeyEvent.DOM_VK_RETURN) { StatusBar.showBusyIndicator(true); that.getDialogTextField().attr("disabled", "disabled"); - _doSearch(query); + var userFilter = FileFilters.commitPicker(filterPicker); + _doSearch(query, userFilter); } } }) .bind("input", handleQueryChange) - .blur(function () { - if (that.getDialogTextField().attr("disabled")) { - return; - } - that._close(); - }) .focus(); this.modalBar.getRoot().on("click", "#find-case-sensitive, #find-regexp", function (e) { @@ -901,6 +911,9 @@ define(function (require, exports, module) { handleQueryChange(); // re-validate regexp if needed }); + filterPicker = FileFilters.createFilterPicker(); + this.modalBar.getRoot().find("#find-group").append(filterPicker); + // Initial UI state (including prepopulated initialString passed into template) FindReplace._updateSearchBarFromPrefs(); handleQueryChange(); @@ -950,6 +963,7 @@ define(function (require, exports, module) { currentQueryExpr = null; currentScope = scope; maxHitsFoundInFile = false; + exports._searchResults = null; // for unit tests dialog.showDialog(initialString, scope); } @@ -1114,4 +1128,7 @@ define(function (require, exports, module) { // Initialize: command handlers CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.EDIT_FIND_IN_FILES, _doFindInFiles); CommandManager.register(Strings.CMD_FIND_IN_SUBTREE, Commands.EDIT_FIND_IN_SUBTREE, _doFindInSubtree); + + // For unit testing - updated in _doSearch() when search complete + exports._searchResults = null; }); diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 2f6b1caf1c3..4570dca5958 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -1164,6 +1164,26 @@ a, img { } } +// File exclusion filter (used only in Find in Files search bar, for now) +.filter-picker { + display: inline-block; + margin-left: 8px; + button { + margin-left: 8px; + } +} + +// File exclusion filter editor dialog +textarea.exclusions-editor { + display: block; + width: 547px; + height: 160px; + margin-top: 12px; + margin-bottom: 0; + .code-font(); +} + + // Search result highlighting - CodeMirror highlighting is pretty constrained. Highlights are // blended on TOP of the selection color. The "current" search result is indicated by selection, // so we want the selection visible underneath the highlight. To do this, the highlight must be diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index b1e71dec43b..0f1006ea322 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -403,34 +403,31 @@ a:focus { margin: 0; } -/* Stylesheet button & dropdown styles */ +/* DropdownButton widget */ -.stylesheet-button.btn-mini { - margin-left: 8px; - position: relative; - top: -1px; -} - -.stylesheet-button.btn-mini.btn-dropdown { - padding-right: 24px; +.btn-dropdown, .btn-dropdown.btn-mini { + position: relative; // needed to position ::after arrow + padding-right: 24px; // makes room for ::after arrow } - -.stylesheet-button.btn-dropdown::after { +.btn-dropdown::after { content: ""; display: block; height: 0px; width: 0px; position: absolute; right: 6px; - top: 7px; + top: 11px; /* dropdown triangle */ border-top: 4px solid @tc-gray-component-triangle; border-left: 4px solid transparent; border-right: 4px solid transparent; } +.btn-dropdown.btn-mini::after { + top: 7px; +} -.stylesheet-dropdown { +.dropdownbutton-popup { &.dropdown-menu:focus { outline: none; } @@ -449,10 +446,6 @@ a:focus { z-index: @z-index-brackets-stylesheet-menu; } - .stylesheet-link, .stylesheet-name { - white-space: nowrap; - } - &.dropdown-menu li a { padding: 1px 15px 1px 15px; } @@ -461,14 +454,6 @@ a:focus { display: block; } - .stylesheet-name { - color: @tc-text; - } - - .stylesheet-dir { - color: @tc-quiet-text; - } - &.dropdown-menu a.selected { background: @tc-highlight; color: @bc-black !important; @@ -479,6 +464,26 @@ a:focus { } } +/* Inline editor stylesheet-picker DropdownButton */ + +.stylesheet-button.btn-mini { + margin-left: 8px; + top: -1px; +} + +.dropdownbutton-popup { + .stylesheet-link, .stylesheet-name { + white-space: nowrap; + } + + .stylesheet-name { + color: @tc-text; + } + + .stylesheet-dir { + color: @tc-quiet-text; + } +} /* Dialog-related styles */ @@ -1110,6 +1115,7 @@ input[type="color"], } } + /* Tables */ .table, table { diff --git a/src/widgets/DropdownButton.js b/src/widgets/DropdownButton.js new file mode 100644 index 00000000000..257736ff940 --- /dev/null +++ b/src/widgets/DropdownButton.js @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2014 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, indent: 4, maxerr: 50 */ +/*global define, $, window */ + +/** + * Button that opens a dropdown list when clicked. More akin to a popup menu than a combobox. Compared to a + * simple