Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

url hinting support #1747

Merged
merged 8 commits into from
Oct 5, 2012
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 49 additions & 27 deletions src/editor/CodeHintManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/default/HTMLCodeHints/HtmlAttributes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"] },
Expand Down Expand Up @@ -197,7 +197,7 @@
"size": { "attribOption": [] },
"sizes": { "attribOption": ["any"] },
"span": { "attribOption": [] },
"src": { "attribOption": [] },
"src": { "attribOption": [], "type": "url" },
"srcdoc": { "attribOption": [] },
"srclang": { "attribOption": [] },
"standby": { "attribOption": [] },
Expand Down
191 changes: 172 additions & 19 deletions src/extensions/default/HTMLCodeHints/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -126,6 +132,7 @@ define(function (require, exports, module) {
*/
function AttrHints() {
this.globalAttributes = this.readGlobalAttrHints();
this.cachedHints = null;
}

/**
Expand All @@ -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),
Expand Down Expand Up @@ -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);
}
}
};

Expand Down Expand Up @@ -246,6 +256,144 @@ 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.<queryStr: string, ...} query -- a query object with a required property queryStr
* that will be used to filter out code hints
* @return {Array.<string>}
*/
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since site relative path is not supported yet, you need to check whether query.Str starts with "/". If it is, then just return here so that we're not showing incorrect code hints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed.

if (queryUrl) {
queryDir = queryUrl.directory;
}

// build target folder path
var targetDir = docDir + decodeURI(queryDir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need decodeURI here if you're going to show encoded urls in the list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetDir is the folder path passed to NativeFileSystem.requestNativeFileSystem() to retrieve the contents of that folder. It is not seen by user. The list that it returns is the list used for hints, which is encoded.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.


// 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) {
// if (cachedHints.waiting === true), then we'll return an empty list
// and not start another readEntries() operation
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 = [];
self.cachedHints.waiting = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see you checking on this boolean cachedHints.waiting. If you don't need it anymore, then you should remove all instances.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed. I ran into a problem where the waiting flag got stuck on so cache was never cleared. Now, we just return the list whether it had items or not.


NativeFileSystem.requestNativeFileSystem(targetDir, function (dirEntry) {
dirEntry.createReader().readEntries(function (entries) {

entries.forEach(function (entry) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other existing code, can you replace .forEach calls with $.map?
ie. unfiltered = $.map(entries, function(entry) { ... return entryStr; });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this suggestion. We are supposed to use Array.foreach instead of $.each for better performance, so I suspect the same problem with $.map, which seems to be supported by the data here: http://jsperf.com/javascript-map-vs-jquery-map-vs-jquery-each

I think we may want to convert calls from $.map to Array.map in the places where the operation is on an Array (not a jQuery result set). If so, then it might make sense to convert my code to Array.map.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice performance data!

I agree with you. We can make the decision later on.

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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encodeURI won't encode '?' and ';' that can be used in filenames. We may have to consider encodeURIComponent or a combination of escape and encodeURI. I can't remember what we did for our default brackets folder path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just encoding existing file and folder names retrieved from disk. So, there won't be any '?' or ';' chars. This is not user input.

Maybe I do not understand your concern.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an existing filename or folder name has these characters, then we won't be encoding them after url insertion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same encoding function we use every where in Brackets so I'm not sure why it's an issue for this feature. Since this is just a bonus feature, can we add this issue to the followup user story?
https://trello.com/c/nZx8Z3Ti

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. I'm just trying to help you making this bonus feature better. I will try to find the potential issue with these characters later in Brackets.

}
});

self.cachedHints.unfiltered = unfiltered;
self.cachedHints.query = query;
self.cachedHints.queryDir = queryDir;
self.cachedHints.waiting = false;

// 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.
Expand All @@ -260,7 +408,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.
Expand All @@ -274,6 +423,9 @@ define(function (require, exports, module) {
if (attrInfo) {
if (attrInfo.type === "boolean") {
unfiltered = ["false", "true"];
} else if (attrInfo.type === "url") {
unfiltered = this._getUrlList(query);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see you encoding the path for unsafe characters like '(', ')', or space character in _getUrlList. If you're not going to show encoded urls in code hints, then at least you have to encode it before insertion. You also need to decode query before passing it to _getUrlList so that it can match to items in code hint list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The encoded url is shown in the list and inserted in the page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I prefer the other solution. But I think it is acceptable to show encoded url in the list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to implement what you described, but it's too hard to get filtering correct. Part of the reason is that it implies you can, for example, filter by typing a space char when %20 will get inserted in page, which is a conflict . I think it's best to show user encoded url and they learn how they should really look, so I think this is the best solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite if we have to consider double-byte characters and high-ascii characters. Anyway, we can leave it for users who can't stand to read encoded url to log an issue.

sortFunc = StringUtils.urlSort;
} else if (attrInfo.attribOption) {
unfiltered = attrInfo.attribOption;
}
Expand All @@ -285,11 +437,12 @@ define(function (require, exports, module) {
}

if (unfiltered.length) {
console.assert(!result.length);
result = $.map(unfiltered, function (item) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For folder and file sorting you need to call your custom sort() function here instead of $.map().sort().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I pass a custom sorting function to sort().

if (item.indexOf(filter) === 0) {
return item;
}
}).sort();
}).sort(sortFunc);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/project/ProjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Loading