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

Commit

Permalink
Merge pull request #3705 from eztierney/html-script-hinting
Browse files Browse the repository at this point in the history
[JS Code Hints] Html script hinting
  • Loading branch information
dloverin committed May 10, 2013
2 parents cc22997 + 64c7ea5 commit c27664b
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 36 deletions.
14 changes: 8 additions & 6 deletions src/extensions/default/JavaScriptCodeHints/ScopeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ define(function (require, exports, module) {
dir = split.dir,
file = split.file;

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

return {promise: ternPromise};
}
Expand Down Expand Up @@ -286,7 +286,7 @@ define(function (require, exports, module) {
* @return {jQuery.Promise} - The promise will not complete until the tern
* hints have completed.
*/
function requestHints(session, document, offset) {
function requestHints(session, document) {
var path = document.file.fullPath,
split = HintUtils.splitPath(path),
dir = split.dir,
Expand All @@ -295,12 +295,14 @@ define(function (require, exports, module) {
var $deferredHints = $.Deferred(),
hintPromise,
fnTypePromise,
propsPromise;
propsPromise,
text = session.getJavascriptText(),
offset = session.getOffset();

hintPromise = getTernHints(dir, file, offset, document.getText());
hintPromise = getTernHints(dir, file, offset, text);
var sessionType = session.getType();
if (sessionType.property) {
propsPromise = getTernProperties(dir, file, offset, document.getText());
propsPromise = getTernProperties(dir, file, offset, text);
} else {
var $propsDeferred = $.Deferred();
propsPromise = $propsDeferred.promise();
Expand All @@ -309,7 +311,7 @@ define(function (require, exports, module) {

if (sessionType.showFunctionType) {
// Show function sig
fnTypePromise = getTernFunctionType(dir, file, sessionType.functionCallPos, offset, document.getText());
fnTypePromise = getTernFunctionType(dir, file, sessionType.functionCallPos, offset, text);
} else {
var $fnTypeDeferred = $.Deferred();
fnTypePromise = $fnTypeDeferred.promise();
Expand Down
68 changes: 64 additions & 4 deletions src/extensions/default/JavaScriptCodeHints/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
*
*/

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, brackets, $ */

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

var StringMatch = brackets.getModule("utils/StringMatch"),
LanguageManager = brackets.getModule("language/LanguageManager"),
HTMLUtils = brackets.getModule("language/HTMLUtils"),
TokenUtils = brackets.getModule("utils/TokenUtils"),
HintUtils = require("HintUtils"),
ScopeManager = require("ScopeManager");

Expand Down Expand Up @@ -273,18 +276,30 @@ define(function (require, exports, module) {
* function being called
*/
Session.prototype.getType = function () {
function getLexicalState(token) {
if (token.state.lexical) {
// in a javascript file this is just in the state field
return token.state.lexical;
} else if (token.state.localState && token.state.localState.lexical) {
// inline javascript in an html file will have this in
// the localState field
return token.state.localState.lexical;
}
}
var propertyLookup = false,
inFunctionCall = false,
showFunctionType = false,
context = null,
cursor = this.getCursor(),
functionCallPos,
token = this.getToken(cursor);
token = this.getToken(cursor),
lexical;

if (token) {
// if this token is part of a function call, then the tokens lexical info
// will be annotated with "call"
if (token.state.lexical.info === "call") {
lexical = getLexicalState(token);
if (lexical.info === "call") {
inFunctionCall = true;
if (this.getQuery().length > 0) {
inFunctionCall = false;
Expand All @@ -301,7 +316,7 @@ define(function (require, exports, module) {
// and it will prevent us from walking back thousands of lines if something went wrong.
// there is nothing magical about 9 lines, and it can be adjusted if it doesn't seem to be
// working well
var col = token.state.lexical.column,
var col = lexical.column,
line,
e,
found;
Expand Down Expand Up @@ -471,6 +486,51 @@ define(function (require, exports, module) {
}
return hints;
};

/**
* Get the javascript text of the file open in the editor for this Session.
* For a javascript file, this is just the text of the file. For an HTML file,
* this will be only the text in the <script> tags. This is so that we can pass
* just the javascript text to tern, and avoid confusing it with HTML tags, since it
* only knows how to parse javascript.
* @return {string} - the "javascript" text that can be sent to Tern.
*/
Session.prototype.getJavascriptText = function () {
if (LanguageManager.getLanguageForPath(this.editor.document.file.fullPath).getId() === "html") {
// HTML file - need to send back only the bodies of the
// <script> tags
var text = "",
offset = this.getOffset(),
cursor = this.getCursor(),
editor = this.editor,
scriptBlocks = HTMLUtils.findBlocks(editor, "javascript");

// Add all the javascript text
// For non-javascript blocks we replace everything except for newlines
// with whitespace. This is so that the offset and cursor positions
// we get from the document still work.
// Alternatively we could strip the non-javascript text, and modify the offset,
// and/or cursor, but then we have to remember how to reverse the translation
// to support jump-to-definition
var htmlStart = {line: 0, ch: 0};
scriptBlocks.forEach(function (scriptBlock) {
var start = scriptBlock.start,
end = scriptBlock.end;

// get the preceding html text, and replace it with whitespace
var htmlText = editor.document.getRange(htmlStart, start);
htmlText = htmlText.replace(/./g, " ");

htmlStart = end;
text += htmlText + scriptBlock.text;
});

return text;
} else {
// Javascript file, just return the text
return this.editor.document.getText();
}
};

module.exports = Session;
});
24 changes: 21 additions & 3 deletions src/extensions/default/JavaScriptCodeHints/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ define(function (require, exports, module) {
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
StringUtils = brackets.getModule("utils/StringUtils"),
StringMatch = brackets.getModule("utils/StringMatch"),
LanguageManager = brackets.getModule("language/LanguageManager"),
HintUtils = require("HintUtils"),
ScopeManager = require("ScopeManager"),
Session = require("Session"),
Expand Down Expand Up @@ -235,6 +236,18 @@ define(function (require, exports, module) {
}
};

/**
* @return {boolean} - true if the document is a html file
*/
function isHTMLFile(document) {
var languageID = LanguageManager.getLanguageForPath(document.file.fullPath).getId();
return languageID === "html";
}

function isInlineScript(editor) {
return editor.getModeForSelection() === "javascript";
}

/**
* Determine whether hints are available for a given editor context
*
Expand All @@ -244,6 +257,12 @@ define(function (require, exports, module) {
*/
JSHints.prototype.hasHints = function (editor, key) {
if (session && HintUtils.hintableKey(key)) {

if (isHTMLFile(session.editor.document)) {
if (!isInlineScript(session.editor)) {
return false;
}
}
var cursor = session.getCursor(),
token = session.getToken(cursor);

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

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

var jsHints = new JSHints();
CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID], 0);
CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID, "html"], 0);

// for unit testing
exports.getSession = getSession;
Expand Down
35 changes: 35 additions & 0 deletions src/extensions/default/JavaScriptCodeHints/test/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Test file for codehints in .html file</title>
</head>
<body>
<script src="file1.js"></script>
<script>
var x = "hi";
</script>
some stuff

<input type="button">

<div class="content">
</div>
<script>
function foo(a){
return "hi";
}

</script>

some other stuff
<div class = "content">
</div>

<script>
foo(10);

</script>

</body>
</html>
64 changes: 62 additions & 2 deletions src/extensions/default/JavaScriptCodeHints/unittests.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ define(function (require, exports, module) {

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

Expand Down Expand Up @@ -76,7 +77,7 @@ define(function (require, exports, module) {
if (key === undefined) {
key = null;
}

expect(provider.hasHints(testEditor, key)).toBe(true);
return provider.getHints(null);
}
Expand Down Expand Up @@ -324,7 +325,6 @@ define(function (require, exports, module) {
DocumentManager.getDocumentForPath(testPath).done(function (doc) {
testDoc = doc;
});

waitsFor(function () {
return testDoc !== null;
}, "Unable to open test document", 10000);
Expand Down Expand Up @@ -779,6 +779,66 @@ define(function (require, exports, module) {
});
});
});

describe("JavaScript Code Hinting in a HTML file", function () {

beforeEach(function () {

DocumentManager.getDocumentForPath(testHtmlPath).done(function (doc) {
testDoc = doc;
});

waitsFor(function () {
return testDoc !== null;
}, "Unable to open test document", 10000);

// create Editor instance (containing a CodeMirror instance)
runs(function () {
testEditor = createMockEditor(testDoc);
JSCodeHints.initializeSession(testEditor, false);
});
});

afterEach(function () {
// The following call ensures that the document is reloaded
// from disk before each test
DocumentManager.closeAll();

SpecRunnerUtils.destroyMockEditor(testDoc);
testEditor = null;
testDoc = null;
});

it("basic codehints in html file", function () {
var start = { line: 30, ch: 9 },
end = { line: 30, ch: 11 };

testDoc.replaceRange("x.", start, start);
testEditor.setCursorPos(end);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
runs(function () {
hintsPresentOrdered(hintObj, ["charAt", "charCodeAt", "concat", "indexOf"]);
});
});

it("function type hint in html file", function () {
var start = { line: 29, ch: 12 };

testEditor.setCursorPos(start);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
runs(function () {
hintsPresentExact(hintObj, ["foo(a: number) -> string"]);
});
});

it("jump-to-def in html file", function () {
var start = { line: 29, ch: 10 };

testEditor.setCursorPos(start);
runs(function () {
editorJumped({line: 18, ch: 20});
});
});
});
});
});
Loading

0 comments on commit c27664b

Please sign in to comment.