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

Commit a856faa

Browse files
committed
Merge pull request #1747 from adobe/randy/code-hint-url
url hinting support
2 parents 431f63c + 8295aac commit a856faa

5 files changed

Lines changed: 245 additions & 50 deletions

File tree

src/editor/CodeHintManager.js

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,23 @@ define(function (require, exports, module) {
7373

7474
/**
7575
* @private
76-
* Enters the code completion text into the editor
76+
* Enters the code completion text into the editor and closes list
7777
* @string {string} completion - text to insert into current code editor
7878
*/
7979
CodeHintList.prototype._handleItemClick = function (completion) {
80-
this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos());
80+
this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos(), true);
8181
this.close();
8282
};
8383

84+
/**
85+
* @private
86+
* Enters the code completion text into the editor without closing list
87+
* @string {string} completion - text to insert into current code editor
88+
*/
89+
CodeHintList.prototype._handleItemSelect = function (completion) {
90+
this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos(), false);
91+
};
92+
8493
/**
8594
* Adds a single item to the hint list
8695
* @param {string} name
@@ -98,6 +107,12 @@ define(function (require, exports, module) {
98107
// bootstrap-dropdown).
99108
e.stopPropagation();
100109
self._handleItemClick(name);
110+
})
111+
.on("select", function (e) {
112+
// Don't let the "select" propagate upward (otherwise it will hit the close handler in
113+
// bootstrap-dropdown).
114+
e.stopPropagation();
115+
self._handleItemSelect(name);
101116
});
102117

103118
this.$hintMenu.find("ul.dropdown-menu")
@@ -179,32 +194,39 @@ define(function (require, exports, module) {
179194
var keyCode = event.keyCode;
180195

181196
// Up arrow, down arrow and enter key are always handled here
182-
if (event.type !== "keypress" &&
183-
(keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN || keyCode === KeyEvent.DOM_VK_RETURN ||
184-
keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN)) {
185-
186-
if (event.type === "keydown") {
187-
if (keyCode === KeyEvent.DOM_VK_UP) {
188-
// Up arrow
189-
this.setSelectedIndex(this.selectedIndex - 1);
190-
} else if (keyCode === KeyEvent.DOM_VK_DOWN) {
191-
// Down arrow
192-
this.setSelectedIndex(this.selectedIndex + 1);
193-
} else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) {
194-
// Page Up
195-
this.setSelectedIndex(this.selectedIndex - this.getItemsPerPage());
196-
} else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) {
197-
// Page Down
198-
this.setSelectedIndex(this.selectedIndex + this.getItemsPerPage());
199-
} else {
200-
// Enter/return key
201-
// Trigger a click handler to commmit the selected item
202-
$(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click");
197+
if (event.type !== "keypress") {
198+
199+
if (keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN || keyCode === KeyEvent.DOM_VK_RETURN ||
200+
keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN) {
201+
202+
if (event.type === "keydown") {
203+
if (keyCode === KeyEvent.DOM_VK_UP) {
204+
// Up arrow
205+
this.setSelectedIndex(this.selectedIndex - 1);
206+
} else if (keyCode === KeyEvent.DOM_VK_DOWN) {
207+
// Down arrow
208+
this.setSelectedIndex(this.selectedIndex + 1);
209+
} else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) {
210+
// Page Up
211+
this.setSelectedIndex(this.selectedIndex - this.getItemsPerPage());
212+
} else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) {
213+
// Page Down
214+
this.setSelectedIndex(this.selectedIndex + this.getItemsPerPage());
215+
} else {
216+
// Enter/return key
217+
// Trigger a click handler to commmit the selected item
218+
$(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click");
219+
}
203220
}
221+
222+
event.preventDefault();
223+
return;
224+
225+
} else if (keyCode === KeyEvent.DOM_VK_TAB) {
226+
// Tab key is used for "select and continue hinting"
227+
$(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("select");
228+
event.preventDefault();
204229
}
205-
206-
event.preventDefault();
207-
return;
208230
}
209231

210232
// All other key events trigger a rebuild of the list, but only
@@ -420,7 +442,7 @@ define(function (require, exports, module) {
420442
*
421443
* @param {Object.< getQueryInfo: function(editor, cursor),
422444
* search: function(string),
423-
* handleSelect: function(string, Editor, cursor),
445+
* handleSelect: function(string, Editor, cursor, closeHints),
424446
* shouldShowHintsOnKey: function(string)>}
425447
*
426448
* Parameter Details:

src/extensions/default/HTMLCodeHints/HtmlAttributes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
"headers": { "attribOption": [] },
139139
"height": { "attribOption": [] },
140140
"high": { "attribOption": [] },
141-
"href": { "attribOption": [] },
141+
"href": { "attribOption": [], "type": "url" },
142142
"hreflang": { "attribOption": [] },
143143
"hspace": { "attribOption": [] },
144144
"http-equiv": { "attribOption": ["content-type", "default-style", "refresh"] },
@@ -197,7 +197,7 @@
197197
"size": { "attribOption": [] },
198198
"sizes": { "attribOption": ["any"] },
199199
"span": { "attribOption": [] },
200-
"src": { "attribOption": [] },
200+
"src": { "attribOption": [], "type": "url" },
201201
"srcdoc": { "attribOption": [] },
202202
"srclang": { "attribOption": [] },
203203
"standby": { "attribOption": [] },

src/extensions/default/HTMLCodeHints/main.js

Lines changed: 169 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,17 @@ define(function (require, exports, module) {
2929
"use strict";
3030

3131
// Load dependent modules
32-
var HTMLUtils = brackets.getModule("language/HTMLUtils"),
33-
HTMLTags = require("text!HtmlTags.json"),
34-
HTMLAttributes = require("text!HtmlAttributes.json"),
35-
CodeHintManager = brackets.getModule("editor/CodeHintManager"),
36-
tags = JSON.parse(HTMLTags),
37-
attributes = JSON.parse(HTMLAttributes);
32+
var CodeHintManager = brackets.getModule("editor/CodeHintManager"),
33+
DocumentManager = brackets.getModule("document/DocumentManager"),
34+
EditorManager = brackets.getModule("editor/EditorManager"),
35+
HTMLUtils = brackets.getModule("language/HTMLUtils"),
36+
NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem,
37+
ProjectManager = brackets.getModule("project/ProjectManager"),
38+
StringUtils = brackets.getModule("utils/StringUtils"),
39+
HTMLTags = require("text!HtmlTags.json"),
40+
HTMLAttributes = require("text!HtmlAttributes.json"),
41+
tags = JSON.parse(HTMLTags),
42+
attributes = JSON.parse(HTMLAttributes);
3843

3944
/**
4045
* @constructor
@@ -88,8 +93,9 @@ define(function (require, exports, module) {
8893
* @param {string} completion - text to insert into current code editor
8994
* @param {Editor} editor
9095
* @param {Cursor} current cursor location
96+
* @param {boolean} closeHints - true to close hints, or false to continue hinting
9197
*/
92-
TagHints.prototype.handleSelect = function (completion, editor, cursor) {
98+
TagHints.prototype.handleSelect = function (completion, editor, cursor, closeHints) {
9399
var start = {line: -1, ch: -1},
94100
end = {line: -1, ch: -1},
95101
tagInfo = HTMLUtils.getTagInfo(editor, cursor),
@@ -126,6 +132,7 @@ define(function (require, exports, module) {
126132
*/
127133
function AttrHints() {
128134
this.globalAttributes = this.readGlobalAttrHints();
135+
this.cachedHints = null;
129136
}
130137

131138
/**
@@ -146,8 +153,9 @@ define(function (require, exports, module) {
146153
* @param {string} completion - text to insert into current code editor
147154
* @param {Editor} editor
148155
* @param {Cursor} current cursor location
156+
* @param {boolean} closeHints - true to close hints, or false to continue hinting
149157
*/
150-
AttrHints.prototype.handleSelect = function (completion, editor, cursor) {
158+
AttrHints.prototype.handleSelect = function (completion, editor, cursor, closeHints) {
151159
var start = {line: -1, ch: -1},
152160
end = {line: -1, ch: -1},
153161
tagInfo = HTMLUtils.getTagInfo(editor, cursor),
@@ -195,15 +203,17 @@ define(function (require, exports, module) {
195203
}
196204
}
197205

198-
if (insertedName) {
199-
editor.setCursorPos(start.line, start.ch + completion.length - 1);
200-
201-
// Since we're now inside the double-quotes we just inserted,
202-
// mmediately pop up the attribute value hint.
203-
CodeHintManager.showHint(editor);
204-
} else if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.hasEndQuote) {
205-
// Move the cursor to the right of the existing end quote after value insertion.
206-
editor.setCursorPos(start.line, start.ch + completion.length + 1);
206+
if (closeHints) {
207+
if (insertedName) {
208+
editor.setCursorPos(start.line, start.ch + completion.length - 1);
209+
210+
// Since we're now inside the double-quotes we just inserted,
211+
// immediately pop up the attribute value hint.
212+
CodeHintManager.showHint(editor);
213+
} else if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.hasEndQuote) {
214+
// Move the cursor to the right of the existing end quote after value insertion.
215+
editor.setCursorPos(start.line, start.ch + completion.length + 1);
216+
}
207217
}
208218
};
209219

@@ -246,6 +256,141 @@ define(function (require, exports, module) {
246256
return query;
247257
};
248258

259+
/**
260+
* Helper function for search(). Create a list of urls to existing files based on the query.
261+
* @param {Object.<queryStr: string, ...} query -- a query object with a required property queryStr
262+
* that will be used to filter out code hints
263+
* @return {Array.<string>}
264+
*/
265+
AttrHints.prototype._getUrlList = function (query) {
266+
var doc,
267+
result = [];
268+
269+
// site-root relative links are not yet supported, so filter them out
270+
if (query.queryStr.length > 0 && query.queryStr[0] === "/") {
271+
return result;
272+
}
273+
274+
// get path to current document
275+
doc = DocumentManager.getCurrentDocument();
276+
if (!doc || !doc.file) {
277+
return result;
278+
}
279+
280+
var docUrl = window.PathUtils.parseUrl(doc.file.fullPath);
281+
if (!docUrl) {
282+
return result;
283+
}
284+
285+
var docDir = docUrl.domain + docUrl.directory;
286+
287+
// get relative path from query string
288+
// TODO: handle site-root relative
289+
var queryDir = "";
290+
var queryUrl = window.PathUtils.parseUrl(query.queryStr);
291+
if (queryUrl) {
292+
queryDir = queryUrl.directory;
293+
}
294+
295+
// build target folder path
296+
var targetDir = docDir + decodeURI(queryDir);
297+
298+
// get list of files from target folder
299+
var unfiltered = [];
300+
301+
// Getting the file/folder info is an asynch operation, so it works like this:
302+
//
303+
// The initial pass initiates the asynchronous retrieval of data and returns an
304+
// empty list, so no code hints are displayed. In the async callback, the code
305+
// hints and the original query are stored in a cache, and then the process to
306+
// show code hints is re-initiated.
307+
//
308+
// During the next pass, there should now be code hints cached from the initial
309+
// pass, but user may have typed while file/folder info was being retrieved from
310+
// disk, so we need to make sure code hints still apply to current query. If so,
311+
// display them, otherwise, clear cache and start over.
312+
//
313+
// As user types within a folder, the same unfiltered file/folder list is still
314+
// valid and re-used from cache. Filtering based on user input is done outside
315+
// of this method. When user moves to a new folder, then the cache is deleted,
316+
// and file/folder info for new folder is then retrieved.
317+
318+
if (this.cachedHints) {
319+
// url hints have been cached, so determine if they're stale
320+
if (!this.cachedHints.query ||
321+
this.cachedHints.query.tag !== query.tag ||
322+
this.cachedHints.query.attrName !== query.attrName ||
323+
this.cachedHints.queryDir !== queryDir) {
324+
325+
// delete stale cache
326+
this.cachedHints = null;
327+
}
328+
}
329+
330+
if (this.cachedHints) {
331+
// use cached hints
332+
unfiltered = this.cachedHints.unfiltered;
333+
334+
} else {
335+
var self = this,
336+
origEditor = EditorManager.getFocusedEditor();
337+
338+
// create empty object so we can detect "waiting" state
339+
self.cachedHints = {};
340+
self.cachedHints.unfiltered = [];
341+
342+
NativeFileSystem.requestNativeFileSystem(targetDir, function (dirEntry) {
343+
dirEntry.createReader().readEntries(function (entries) {
344+
345+
entries.forEach(function (entry) {
346+
if (ProjectManager.shouldShow(entry)) {
347+
// convert to doc relative path
348+
var entryStr = entry.fullPath.replace(docDir, "");
349+
350+
// code hints show the same strings that are inserted into text,
351+
// so strings in list will be encoded. wysiwyg, baby!
352+
unfiltered.push(encodeURI(entryStr));
353+
}
354+
});
355+
356+
self.cachedHints.unfiltered = unfiltered;
357+
self.cachedHints.query = query;
358+
self.cachedHints.queryDir = queryDir;
359+
360+
// If the editor has not changed, then re-initiate code hints. Cached data
361+
// is still valid for folder even if we're not going to show it now.
362+
if (origEditor === EditorManager.getFocusedEditor()) {
363+
CodeHintManager.showHint(origEditor);
364+
}
365+
});
366+
});
367+
368+
return result;
369+
}
370+
371+
// build list
372+
373+
// without these entries, typing "../" will not display entries for containing folder
374+
if (queryUrl.filename === ".") {
375+
result.push(queryDir + ".");
376+
} else if (queryUrl.filename === "..") {
377+
result.push(queryDir + "..");
378+
}
379+
380+
// add file/folder entries
381+
unfiltered.forEach(function (item) {
382+
result.push(item);
383+
});
384+
385+
// TODO: filter by desired file type based on tag, type attr, etc.
386+
387+
// TODO: add list item to top of list to popup modal File Finder dialog
388+
// New string: "Browse..." or "Choose a File..."
389+
// Command: Commands.FILE_OPEN
390+
391+
return result;
392+
};
393+
249394
/**
250395
* Create a complete list of attributes for the tag in the query. Then filter
251396
* the list by attrName in the query and return the result.
@@ -260,7 +405,8 @@ define(function (require, exports, module) {
260405
var tagName = query.tag,
261406
attrName = query.attrName,
262407
filter = query.queryStr,
263-
unfiltered = [];
408+
unfiltered = [],
409+
sortFunc = null;
264410

265411
if (attrName) {
266412
// We look up attribute values with tagName plus a slash and attrName first.
@@ -274,6 +420,9 @@ define(function (require, exports, module) {
274420
if (attrInfo) {
275421
if (attrInfo.type === "boolean") {
276422
unfiltered = ["false", "true"];
423+
} else if (attrInfo.type === "url") {
424+
unfiltered = this._getUrlList(query);
425+
sortFunc = StringUtils.urlSort;
277426
} else if (attrInfo.attribOption) {
278427
unfiltered = attrInfo.attribOption;
279428
}
@@ -285,11 +434,12 @@ define(function (require, exports, module) {
285434
}
286435

287436
if (unfiltered.length) {
437+
console.assert(!result.length);
288438
result = $.map(unfiltered, function (item) {
289439
if (item.indexOf(filter) === 0) {
290440
return item;
291441
}
292-
}).sort();
442+
}).sort(sortFunc);
293443
}
294444
}
295445

src/project/ProjectManager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ define(function (require, exports, module) {
513513

514514
/** @param {Entry} entry File or directory to filter */
515515
function shouldShow(entry) {
516-
return [".git", ".svn", ".DS_Store", "Thumbs.db"].indexOf(entry.name) === -1;
516+
return [".git", ".gitignore", ".gitmodules", ".svn", ".DS_Store", "Thumbs.db"].indexOf(entry.name) === -1;
517517
}
518518

519519
/**

0 commit comments

Comments
 (0)