Skip to content

Commit c27664b

Browse files
author
dloverin
committed
Merge pull request adobe#3705 from eztierney/html-script-hinting
[JS Code Hints] Html script hinting
2 parents cc22997 + 64c7ea5 commit c27664b

File tree

6 files changed

+228
-36
lines changed

6 files changed

+228
-36
lines changed

src/extensions/default/JavaScriptCodeHints/ScopeManager.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ define(function (require, exports, module) {
152152
dir = split.dir,
153153
file = split.file;
154154

155-
var ternPromise = getJumptoDef(dir, file, offset, document.getText());
155+
var ternPromise = getJumptoDef(dir, file, offset, session.getJavascriptText());
156156

157157
return {promise: ternPromise};
158158
}
@@ -286,7 +286,7 @@ define(function (require, exports, module) {
286286
* @return {jQuery.Promise} - The promise will not complete until the tern
287287
* hints have completed.
288288
*/
289-
function requestHints(session, document, offset) {
289+
function requestHints(session, document) {
290290
var path = document.file.fullPath,
291291
split = HintUtils.splitPath(path),
292292
dir = split.dir,
@@ -295,12 +295,14 @@ define(function (require, exports, module) {
295295
var $deferredHints = $.Deferred(),
296296
hintPromise,
297297
fnTypePromise,
298-
propsPromise;
298+
propsPromise,
299+
text = session.getJavascriptText(),
300+
offset = session.getOffset();
299301

300-
hintPromise = getTernHints(dir, file, offset, document.getText());
302+
hintPromise = getTernHints(dir, file, offset, text);
301303
var sessionType = session.getType();
302304
if (sessionType.property) {
303-
propsPromise = getTernProperties(dir, file, offset, document.getText());
305+
propsPromise = getTernProperties(dir, file, offset, text);
304306
} else {
305307
var $propsDeferred = $.Deferred();
306308
propsPromise = $propsDeferred.promise();
@@ -309,7 +311,7 @@ define(function (require, exports, module) {
309311

310312
if (sessionType.showFunctionType) {
311313
// Show function sig
312-
fnTypePromise = getTernFunctionType(dir, file, sessionType.functionCallPos, offset, document.getText());
314+
fnTypePromise = getTernFunctionType(dir, file, sessionType.functionCallPos, offset, text);
313315
} else {
314316
var $fnTypeDeferred = $.Deferred();
315317
fnTypePromise = $fnTypeDeferred.promise();

src/extensions/default/JavaScriptCodeHints/Session.js

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
*
2222
*/
2323

24-
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
24+
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
2525
/*global define, brackets, $ */
2626

2727
define(function (require, exports, module) {
2828
"use strict";
2929

3030
var StringMatch = brackets.getModule("utils/StringMatch"),
31+
LanguageManager = brackets.getModule("language/LanguageManager"),
32+
HTMLUtils = brackets.getModule("language/HTMLUtils"),
33+
TokenUtils = brackets.getModule("utils/TokenUtils"),
3134
HintUtils = require("HintUtils"),
3235
ScopeManager = require("ScopeManager");
3336

@@ -273,18 +276,30 @@ define(function (require, exports, module) {
273276
* function being called
274277
*/
275278
Session.prototype.getType = function () {
279+
function getLexicalState(token) {
280+
if (token.state.lexical) {
281+
// in a javascript file this is just in the state field
282+
return token.state.lexical;
283+
} else if (token.state.localState && token.state.localState.lexical) {
284+
// inline javascript in an html file will have this in
285+
// the localState field
286+
return token.state.localState.lexical;
287+
}
288+
}
276289
var propertyLookup = false,
277290
inFunctionCall = false,
278291
showFunctionType = false,
279292
context = null,
280293
cursor = this.getCursor(),
281294
functionCallPos,
282-
token = this.getToken(cursor);
295+
token = this.getToken(cursor),
296+
lexical;
283297

284298
if (token) {
285299
// if this token is part of a function call, then the tokens lexical info
286300
// will be annotated with "call"
287-
if (token.state.lexical.info === "call") {
301+
lexical = getLexicalState(token);
302+
if (lexical.info === "call") {
288303
inFunctionCall = true;
289304
if (this.getQuery().length > 0) {
290305
inFunctionCall = false;
@@ -301,7 +316,7 @@ define(function (require, exports, module) {
301316
// and it will prevent us from walking back thousands of lines if something went wrong.
302317
// there is nothing magical about 9 lines, and it can be adjusted if it doesn't seem to be
303318
// working well
304-
var col = token.state.lexical.column,
319+
var col = lexical.column,
305320
line,
306321
e,
307322
found;
@@ -471,6 +486,51 @@ define(function (require, exports, module) {
471486
}
472487
return hints;
473488
};
489+
490+
/**
491+
* Get the javascript text of the file open in the editor for this Session.
492+
* For a javascript file, this is just the text of the file. For an HTML file,
493+
* this will be only the text in the <script> tags. This is so that we can pass
494+
* just the javascript text to tern, and avoid confusing it with HTML tags, since it
495+
* only knows how to parse javascript.
496+
* @return {string} - the "javascript" text that can be sent to Tern.
497+
*/
498+
Session.prototype.getJavascriptText = function () {
499+
if (LanguageManager.getLanguageForPath(this.editor.document.file.fullPath).getId() === "html") {
500+
// HTML file - need to send back only the bodies of the
501+
// <script> tags
502+
var text = "",
503+
offset = this.getOffset(),
504+
cursor = this.getCursor(),
505+
editor = this.editor,
506+
scriptBlocks = HTMLUtils.findBlocks(editor, "javascript");
507+
508+
// Add all the javascript text
509+
// For non-javascript blocks we replace everything except for newlines
510+
// with whitespace. This is so that the offset and cursor positions
511+
// we get from the document still work.
512+
// Alternatively we could strip the non-javascript text, and modify the offset,
513+
// and/or cursor, but then we have to remember how to reverse the translation
514+
// to support jump-to-definition
515+
var htmlStart = {line: 0, ch: 0};
516+
scriptBlocks.forEach(function (scriptBlock) {
517+
var start = scriptBlock.start,
518+
end = scriptBlock.end;
519+
520+
// get the preceding html text, and replace it with whitespace
521+
var htmlText = editor.document.getRange(htmlStart, start);
522+
htmlText = htmlText.replace(/./g, " ");
523+
524+
htmlStart = end;
525+
text += htmlText + scriptBlock.text;
526+
});
527+
528+
return text;
529+
} else {
530+
// Javascript file, just return the text
531+
return this.editor.document.getText();
532+
}
533+
};
474534

475535
module.exports = Session;
476536
});

src/extensions/default/JavaScriptCodeHints/main.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ define(function (require, exports, module) {
3838
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
3939
StringUtils = brackets.getModule("utils/StringUtils"),
4040
StringMatch = brackets.getModule("utils/StringMatch"),
41+
LanguageManager = brackets.getModule("language/LanguageManager"),
4142
HintUtils = require("HintUtils"),
4243
ScopeManager = require("ScopeManager"),
4344
Session = require("Session"),
@@ -235,6 +236,18 @@ define(function (require, exports, module) {
235236
}
236237
};
237238

239+
/**
240+
* @return {boolean} - true if the document is a html file
241+
*/
242+
function isHTMLFile(document) {
243+
var languageID = LanguageManager.getLanguageForPath(document.file.fullPath).getId();
244+
return languageID === "html";
245+
}
246+
247+
function isInlineScript(editor) {
248+
return editor.getModeForSelection() === "javascript";
249+
}
250+
238251
/**
239252
* Determine whether hints are available for a given editor context
240253
*
@@ -244,6 +257,12 @@ define(function (require, exports, module) {
244257
*/
245258
JSHints.prototype.hasHints = function (editor, key) {
246259
if (session && HintUtils.hintableKey(key)) {
260+
261+
if (isHTMLFile(session.editor.document)) {
262+
if (!isInlineScript(session.editor)) {
263+
return false;
264+
}
265+
}
247266
var cursor = session.getCursor(),
248267
token = session.getToken(cursor);
249268

@@ -292,8 +311,7 @@ define(function (require, exports, module) {
292311
// Compute fresh hints if none exist, or if the session
293312
// type has changed since the last hint computation
294313
if (this.needNewHints(session)) {
295-
var offset = session.getOffset(),
296-
scopeResponse = ScopeManager.requestHints(session, session.editor.document, offset);
314+
var scopeResponse = ScopeManager.requestHints(session, session.editor.document);
297315

298316
if (scopeResponse.hasOwnProperty("promise")) {
299317
var $deferredHints = $.Deferred();
@@ -541,7 +559,7 @@ define(function (require, exports, module) {
541559
installEditorListeners(EditorManager.getActiveEditor());
542560

543561
var jsHints = new JSHints();
544-
CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID], 0);
562+
CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID, "html"], 0);
545563

546564
// for unit testing
547565
exports.getSession = getSession;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Test file for codehints in .html file</title>
6+
</head>
7+
<body>
8+
<script src="file1.js"></script>
9+
<script>
10+
var x = "hi";
11+
</script>
12+
some stuff
13+
14+
<input type="button">
15+
16+
<div class="content">
17+
</div>
18+
<script>
19+
function foo(a){
20+
return "hi";
21+
}
22+
23+
</script>
24+
25+
some other stuff
26+
<div class = "content">
27+
</div>
28+
29+
<script>
30+
foo(10);
31+
32+
</script>
33+
34+
</body>
35+
</html>

src/extensions/default/JavaScriptCodeHints/unittests.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ define(function (require, exports, module) {
3737

3838
var extensionPath = FileUtils.getNativeModuleDirectoryPath(module),
3939
testPath = extensionPath + "/test/file1.js",
40+
testHtmlPath = extensionPath + "/test/index.html",
4041
testDoc = null,
4142
testEditor;
4243

@@ -76,7 +77,7 @@ define(function (require, exports, module) {
7677
if (key === undefined) {
7778
key = null;
7879
}
79-
80+
8081
expect(provider.hasHints(testEditor, key)).toBe(true);
8182
return provider.getHints(null);
8283
}
@@ -324,7 +325,6 @@ define(function (require, exports, module) {
324325
DocumentManager.getDocumentForPath(testPath).done(function (doc) {
325326
testDoc = doc;
326327
});
327-
328328
waitsFor(function () {
329329
return testDoc !== null;
330330
}, "Unable to open test document", 10000);
@@ -779,6 +779,66 @@ define(function (require, exports, module) {
779779
});
780780
});
781781
});
782+
783+
describe("JavaScript Code Hinting in a HTML file", function () {
784+
785+
beforeEach(function () {
786+
787+
DocumentManager.getDocumentForPath(testHtmlPath).done(function (doc) {
788+
testDoc = doc;
789+
});
790+
791+
waitsFor(function () {
792+
return testDoc !== null;
793+
}, "Unable to open test document", 10000);
794+
795+
// create Editor instance (containing a CodeMirror instance)
796+
runs(function () {
797+
testEditor = createMockEditor(testDoc);
798+
JSCodeHints.initializeSession(testEditor, false);
799+
});
800+
});
801+
802+
afterEach(function () {
803+
// The following call ensures that the document is reloaded
804+
// from disk before each test
805+
DocumentManager.closeAll();
806+
807+
SpecRunnerUtils.destroyMockEditor(testDoc);
808+
testEditor = null;
809+
testDoc = null;
810+
});
782811

812+
it("basic codehints in html file", function () {
813+
var start = { line: 30, ch: 9 },
814+
end = { line: 30, ch: 11 };
815+
816+
testDoc.replaceRange("x.", start, start);
817+
testEditor.setCursorPos(end);
818+
var hintObj = expectHints(JSCodeHints.jsHintProvider);
819+
runs(function () {
820+
hintsPresentOrdered(hintObj, ["charAt", "charCodeAt", "concat", "indexOf"]);
821+
});
822+
});
823+
824+
it("function type hint in html file", function () {
825+
var start = { line: 29, ch: 12 };
826+
827+
testEditor.setCursorPos(start);
828+
var hintObj = expectHints(JSCodeHints.jsHintProvider);
829+
runs(function () {
830+
hintsPresentExact(hintObj, ["foo(a: number) -> string"]);
831+
});
832+
});
833+
834+
it("jump-to-def in html file", function () {
835+
var start = { line: 29, ch: 10 };
836+
837+
testEditor.setCursorPos(start);
838+
runs(function () {
839+
editorJumped({line: 18, ch: 20});
840+
});
841+
});
842+
});
783843
});
784844
});

0 commit comments

Comments
 (0)