diff --git a/src/editor/CodeHintManager.js b/src/editor/CodeHintManager.js index 7c4e442b262..546c640f8fa 100644 --- a/src/editor/CodeHintManager.js +++ b/src/editor/CodeHintManager.js @@ -73,14 +73,23 @@ define(function (require, exports, module) { /** * @private - * Enters the code completion text into the editor + * Enters the code completion text into the editor and closes list * @string {string} completion - text to insert into current code editor */ CodeHintList.prototype._handleItemClick = function (completion) { - this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos()); + this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos(), true); this.close(); }; + /** + * @private + * Enters the code completion text into the editor without closing list + * @string {string} completion - text to insert into current code editor + */ + CodeHintList.prototype._handleItemSelect = function (completion) { + this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos(), false); + }; + /** * Adds a single item to the hint list * @param {string} name @@ -98,6 +107,12 @@ define(function (require, exports, module) { // bootstrap-dropdown). e.stopPropagation(); self._handleItemClick(name); + }) + .on("select", function (e) { + // Don't let the "select" propagate upward (otherwise it will hit the close handler in + // bootstrap-dropdown). + e.stopPropagation(); + self._handleItemSelect(name); }); this.$hintMenu.find("ul.dropdown-menu") @@ -179,32 +194,39 @@ define(function (require, exports, module) { var keyCode = event.keyCode; // Up arrow, down arrow and enter key are always handled here - if (event.type !== "keypress" && - (keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN || keyCode === KeyEvent.DOM_VK_RETURN || - keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN)) { - - if (event.type === "keydown") { - if (keyCode === KeyEvent.DOM_VK_UP) { - // Up arrow - this.setSelectedIndex(this.selectedIndex - 1); - } else if (keyCode === KeyEvent.DOM_VK_DOWN) { - // Down arrow - this.setSelectedIndex(this.selectedIndex + 1); - } else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) { - // Page Up - this.setSelectedIndex(this.selectedIndex - this.getItemsPerPage()); - } else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) { - // Page Down - this.setSelectedIndex(this.selectedIndex + this.getItemsPerPage()); - } else { - // Enter/return key - // Trigger a click handler to commmit the selected item - $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click"); + if (event.type !== "keypress") { + + if (keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN || keyCode === KeyEvent.DOM_VK_RETURN || + keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN) { + + if (event.type === "keydown") { + if (keyCode === KeyEvent.DOM_VK_UP) { + // Up arrow + this.setSelectedIndex(this.selectedIndex - 1); + } else if (keyCode === KeyEvent.DOM_VK_DOWN) { + // Down arrow + this.setSelectedIndex(this.selectedIndex + 1); + } else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) { + // Page Up + this.setSelectedIndex(this.selectedIndex - this.getItemsPerPage()); + } else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) { + // Page Down + this.setSelectedIndex(this.selectedIndex + this.getItemsPerPage()); + } else { + // Enter/return key + // Trigger a click handler to commmit the selected item + $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click"); + } } + + event.preventDefault(); + return; + + } else if (keyCode === KeyEvent.DOM_VK_TAB) { + // Tab key is used for "select and continue hinting" + $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("select"); + event.preventDefault(); } - - event.preventDefault(); - return; } // All other key events trigger a rebuild of the list, but only @@ -420,7 +442,7 @@ define(function (require, exports, module) { * * @param {Object.< getQueryInfo: function(editor, cursor), * search: function(string), - * handleSelect: function(string, Editor, cursor), + * handleSelect: function(string, Editor, cursor, closeHints), * shouldShowHintsOnKey: function(string)>} * * Parameter Details: diff --git a/src/extensions/default/HTMLCodeHints/HtmlAttributes.json b/src/extensions/default/HTMLCodeHints/HtmlAttributes.json index 97227f602e4..cf5b34a615b 100644 --- a/src/extensions/default/HTMLCodeHints/HtmlAttributes.json +++ b/src/extensions/default/HTMLCodeHints/HtmlAttributes.json @@ -138,7 +138,7 @@ "headers": { "attribOption": [] }, "height": { "attribOption": [] }, "high": { "attribOption": [] }, - "href": { "attribOption": [] }, + "href": { "attribOption": [], "type": "url" }, "hreflang": { "attribOption": [] }, "hspace": { "attribOption": [] }, "http-equiv": { "attribOption": ["content-type", "default-style", "refresh"] }, @@ -197,7 +197,7 @@ "size": { "attribOption": [] }, "sizes": { "attribOption": ["any"] }, "span": { "attribOption": [] }, - "src": { "attribOption": [] }, + "src": { "attribOption": [], "type": "url" }, "srcdoc": { "attribOption": [] }, "srclang": { "attribOption": [] }, "standby": { "attribOption": [] }, diff --git a/src/extensions/default/HTMLCodeHints/main.js b/src/extensions/default/HTMLCodeHints/main.js index e5516ff2f05..ea05f0d5a9b 100644 --- a/src/extensions/default/HTMLCodeHints/main.js +++ b/src/extensions/default/HTMLCodeHints/main.js @@ -29,12 +29,17 @@ define(function (require, exports, module) { "use strict"; // Load dependent modules - var HTMLUtils = brackets.getModule("language/HTMLUtils"), - HTMLTags = require("text!HtmlTags.json"), - HTMLAttributes = require("text!HtmlAttributes.json"), - CodeHintManager = brackets.getModule("editor/CodeHintManager"), - tags = JSON.parse(HTMLTags), - attributes = JSON.parse(HTMLAttributes); + var CodeHintManager = brackets.getModule("editor/CodeHintManager"), + DocumentManager = brackets.getModule("document/DocumentManager"), + EditorManager = brackets.getModule("editor/EditorManager"), + HTMLUtils = brackets.getModule("language/HTMLUtils"), + NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, + ProjectManager = brackets.getModule("project/ProjectManager"), + StringUtils = brackets.getModule("utils/StringUtils"), + HTMLTags = require("text!HtmlTags.json"), + HTMLAttributes = require("text!HtmlAttributes.json"), + tags = JSON.parse(HTMLTags), + attributes = JSON.parse(HTMLAttributes); /** * @constructor @@ -88,8 +93,9 @@ define(function (require, exports, module) { * @param {string} completion - text to insert into current code editor * @param {Editor} editor * @param {Cursor} current cursor location + * @param {boolean} closeHints - true to close hints, or false to continue hinting */ - TagHints.prototype.handleSelect = function (completion, editor, cursor) { + TagHints.prototype.handleSelect = function (completion, editor, cursor, closeHints) { var start = {line: -1, ch: -1}, end = {line: -1, ch: -1}, tagInfo = HTMLUtils.getTagInfo(editor, cursor), @@ -126,6 +132,7 @@ define(function (require, exports, module) { */ function AttrHints() { this.globalAttributes = this.readGlobalAttrHints(); + this.cachedHints = null; } /** @@ -146,8 +153,9 @@ define(function (require, exports, module) { * @param {string} completion - text to insert into current code editor * @param {Editor} editor * @param {Cursor} current cursor location + * @param {boolean} closeHints - true to close hints, or false to continue hinting */ - AttrHints.prototype.handleSelect = function (completion, editor, cursor) { + AttrHints.prototype.handleSelect = function (completion, editor, cursor, closeHints) { var start = {line: -1, ch: -1}, end = {line: -1, ch: -1}, tagInfo = HTMLUtils.getTagInfo(editor, cursor), @@ -195,15 +203,17 @@ define(function (require, exports, module) { } } - if (insertedName) { - editor.setCursorPos(start.line, start.ch + completion.length - 1); - - // Since we're now inside the double-quotes we just inserted, - // mmediately pop up the attribute value hint. - CodeHintManager.showHint(editor); - } else if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.hasEndQuote) { - // Move the cursor to the right of the existing end quote after value insertion. - editor.setCursorPos(start.line, start.ch + completion.length + 1); + if (closeHints) { + if (insertedName) { + editor.setCursorPos(start.line, start.ch + completion.length - 1); + + // Since we're now inside the double-quotes we just inserted, + // immediately pop up the attribute value hint. + CodeHintManager.showHint(editor); + } else if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.hasEndQuote) { + // Move the cursor to the right of the existing end quote after value insertion. + editor.setCursorPos(start.line, start.ch + completion.length + 1); + } } }; @@ -246,6 +256,141 @@ define(function (require, exports, module) { return query; }; + /** + * Helper function for search(). Create a list of urls to existing files based on the query. + * @param {Object.} + */ + AttrHints.prototype._getUrlList = function (query) { + var doc, + result = []; + + // site-root relative links are not yet supported, so filter them out + if (query.queryStr.length > 0 && query.queryStr[0] === "/") { + return result; + } + + // get path to current document + doc = DocumentManager.getCurrentDocument(); + if (!doc || !doc.file) { + return result; + } + + var docUrl = window.PathUtils.parseUrl(doc.file.fullPath); + if (!docUrl) { + return result; + } + + var docDir = docUrl.domain + docUrl.directory; + + // get relative path from query string + // TODO: handle site-root relative + var queryDir = ""; + var queryUrl = window.PathUtils.parseUrl(query.queryStr); + if (queryUrl) { + queryDir = queryUrl.directory; + } + + // build target folder path + var targetDir = docDir + decodeURI(queryDir); + + // get list of files from target folder + var unfiltered = []; + + // Getting the file/folder info is an asynch operation, so it works like this: + // + // The initial pass initiates the asynchronous retrieval of data and returns an + // empty list, so no code hints are displayed. In the async callback, the code + // hints and the original query are stored in a cache, and then the process to + // show code hints is re-initiated. + // + // During the next pass, there should now be code hints cached from the initial + // pass, but user may have typed while file/folder info was being retrieved from + // disk, so we need to make sure code hints still apply to current query. If so, + // display them, otherwise, clear cache and start over. + // + // As user types within a folder, the same unfiltered file/folder list is still + // valid and re-used from cache. Filtering based on user input is done outside + // of this method. When user moves to a new folder, then the cache is deleted, + // and file/folder info for new folder is then retrieved. + + if (this.cachedHints) { + // url hints have been cached, so determine if they're stale + if (!this.cachedHints.query || + this.cachedHints.query.tag !== query.tag || + this.cachedHints.query.attrName !== query.attrName || + this.cachedHints.queryDir !== queryDir) { + + // delete stale cache + this.cachedHints = null; + } + } + + if (this.cachedHints) { + // use cached hints + unfiltered = this.cachedHints.unfiltered; + + } else { + var self = this, + origEditor = EditorManager.getFocusedEditor(); + + // create empty object so we can detect "waiting" state + self.cachedHints = {}; + self.cachedHints.unfiltered = []; + + NativeFileSystem.requestNativeFileSystem(targetDir, function (dirEntry) { + dirEntry.createReader().readEntries(function (entries) { + + entries.forEach(function (entry) { + if (ProjectManager.shouldShow(entry)) { + // convert to doc relative path + var entryStr = entry.fullPath.replace(docDir, ""); + + // code hints show the same strings that are inserted into text, + // so strings in list will be encoded. wysiwyg, baby! + unfiltered.push(encodeURI(entryStr)); + } + }); + + self.cachedHints.unfiltered = unfiltered; + self.cachedHints.query = query; + self.cachedHints.queryDir = queryDir; + + // If the editor has not changed, then re-initiate code hints. Cached data + // is still valid for folder even if we're not going to show it now. + if (origEditor === EditorManager.getFocusedEditor()) { + CodeHintManager.showHint(origEditor); + } + }); + }); + + return result; + } + + // build list + + // without these entries, typing "../" will not display entries for containing folder + if (queryUrl.filename === ".") { + result.push(queryDir + "."); + } else if (queryUrl.filename === "..") { + result.push(queryDir + ".."); + } + + // add file/folder entries + unfiltered.forEach(function (item) { + result.push(item); + }); + + // TODO: filter by desired file type based on tag, type attr, etc. + + // TODO: add list item to top of list to popup modal File Finder dialog + // New string: "Browse..." or "Choose a File..." + // Command: Commands.FILE_OPEN + + return result; + }; + /** * Create a complete list of attributes for the tag in the query. Then filter * the list by attrName in the query and return the result. @@ -260,7 +405,8 @@ define(function (require, exports, module) { var tagName = query.tag, attrName = query.attrName, filter = query.queryStr, - unfiltered = []; + unfiltered = [], + sortFunc = null; if (attrName) { // We look up attribute values with tagName plus a slash and attrName first. @@ -274,6 +420,9 @@ define(function (require, exports, module) { if (attrInfo) { if (attrInfo.type === "boolean") { unfiltered = ["false", "true"]; + } else if (attrInfo.type === "url") { + unfiltered = this._getUrlList(query); + sortFunc = StringUtils.urlSort; } else if (attrInfo.attribOption) { unfiltered = attrInfo.attribOption; } @@ -285,11 +434,12 @@ define(function (require, exports, module) { } if (unfiltered.length) { + console.assert(!result.length); result = $.map(unfiltered, function (item) { if (item.indexOf(filter) === 0) { return item; } - }).sort(); + }).sort(sortFunc); } } diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index cd9849e518a..04cef75dedc 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -510,7 +510,7 @@ define(function (require, exports, module) { /** @param {Entry} entry File or directory to filter */ function shouldShow(entry) { - return [".git", ".svn", ".DS_Store", "Thumbs.db"].indexOf(entry.name) === -1; + return [".git", ".gitignore", ".gitmodules", ".svn", ".DS_Store", "Thumbs.db"].indexOf(entry.name) === -1; } /** diff --git a/src/utils/StringUtils.js b/src/utils/StringUtils.js index 21ef65a48f2..a89b604f6a4 100644 --- a/src/utils/StringUtils.js +++ b/src/utils/StringUtils.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ -/*global define, $ */ +/*global define, $, brackets */ /** * Utilities functions related to string manipulation @@ -119,6 +119,28 @@ define(function (require, exports, module) { } } + function urlSort(a, b) { + var a2, b2; + function isFile(s) { + return ((s.lastIndexOf("/") + 1) < s.length); + } + + if (brackets.platform === "win") { + // Windows: prepend folder names with a '0' and file names with a '1' so folders are listed first + a2 = ((isFile(a)) ? "1" : "0") + a.toLowerCase(); + b2 = ((isFile(b)) ? "1" : "0") + b.toLowerCase(); + } else { + a2 = a.toLowerCase(); + b2 = b.toLowerCase(); + } + + if (a2 === b2) { + return 0; + } else { + return (a2 > b2) ? 1 : -1; + } + } + // Define public API exports.format = format; exports.htmlEscape = htmlEscape; @@ -126,4 +148,5 @@ define(function (require, exports, module) { exports.jQueryIdEscape = jQueryIdEscape; exports.getLines = getLines; exports.offsetToLineNum = offsetToLineNum; + exports.urlSort = urlSort; });