Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

Making the most out of JavaScript Intellisense in VS2012

4.97/5 (13 votes)
18 Sep 2012CPOL13 min read 49.6K  
Making the most out of JavaScript Intellisense in VS2012 when using AMD/require.js

Introduction   

Intellisense for JavaScript has always felt to me like an unfinished addition to Visual Studio, something that promises so much but doesn’t deliver for larger projects. Manually adding and then maintaining another set of dependency references just for Intellisense has just never seemed worth it. However, with the launch of Visual Studio 2012 Intellisense has had a refresh, does this mean it’s now worth another look?

In this post I’ve taken Colin Eberhardt’s HTML5 Property Finder app and enhanced it with AMD (Asynchronous Module Definitions) allowing each JavaScript file to define it’s own dependencies, much like C#. This one source of dependency data is then used when writing the code to automatically provide Intellisense, during debugging in the browser to load each JavaScript file in the correct order (no more long lists of script tags!) and when releasing to build a single optimised JavaScript file.

Image 1

Intellisense for JavaScript    

When writing C#, Intellisense makes life a lot easier by presenting auto-completion of statements and type/member/variable lookup with context sensitive documentation. As a developer it rapidly becomes an essential part of your toolkit, so much so that when you start out on a JavaScript project and see just how poorly it can perform, despair soon sets in! At this point it’s worth taking a step back and considering why this is the case, why can’t the JavaScript Intellisense live up to its C# counterpart?

The simple fact is that JavaScript just doesn’t have many of the language constructs which Intellisense relies on. Its dynamic nature means that concepts of types or type information are meaningless, any object can be augmented at runtime with new methods/properties or conversely have methods/properties removed. Some concepts that are taken for granted in other languages such as namespacing and importing are simply missing.

This is not a criticism of the language itself, its use has evolved over time to be something it simply wasn’t designed for. And as such, developers have filled in the gaps, for example many techniques now exist to allow imports each with their own advantages and disadvantages. However, from Intellisense’s perspective this makes things even worse, instead of there being one standard technique for achieving X it now has to deal with N potentially conflicting techniques.

Image 2

In a bid to overcome these problems VS2010 introduced pseudo-execution of the JavaScript code. This involved running the code in a special Intellisense JavaScript context to determine what the state of a particular object was at a particular point in the code. If a file required another file, the only way to inform Intellisense of the dependency was to add a specially crafted Intellisense comment which pointed at the other file.

VS2012 has taken this a step further, adding an API for interacting with Intellisense from JavaScript and hooks to run additional code specifically for the Intellisense context. This is great news, we can now write code to help Intellisense understand how specific constructs are being used in our code. The rest of this post deals with integrating the popular AMD framework require.js into Colin’s Property Finder app and helping Intellisense to play nicely with it.

What is AMD?  

The basic idea of AMD is that each JavaScript file becomes a module, named according to its path in a similar way to a namespace in C#. All of the code for the module is placed within a callback and the module declares its dependencies in one of a few standard ways. Here’s a simple example from the Property Finder project -
JavaScript
define("model/JSONFileDataSource", function (require) {
  var $ = require("lib/jquery");
  return function () {
    /// <summary>
    /// A test version of JSONDataSource, which returns 'canned' responses.
    /// </summary>
    this.findProperties = function (location, pageNumber, callback) {
      function fetchData() {
        $.ajax(...);
      }
      ...
    };
  };
}); 

The example defines a module called model/JSONFileDataSource which has a dependency on another module called lib/jquery. The dependency is assigned to a locally scoped variable, and the module exports itself by returning a constructor function from the callback. This allows another module to reference it -
JavaScript
define("example", function (require) {
  var DataSource = require("model/JSONFileDataSource");
  var dataSource = new DataSource();
  dataSource.findProperties(...);
  return ...;
});

The missing piece of the puzzle is the loader, which provides the define global method used above. It has two responsibilities, loading the referenced dependencies before invoking the callback as shown above, and bootstrapping the loading from an entry-point main module e.g -
HTML
<script src="Scripts/lib/require.js" type="text/javascript" data-main="example"></script>

AMD in the Property Finder App

Whilst porting the Property Finder project over to use AMD, I’ve tried to follow the best practices recommended in the require.js docs as closely as possible. However, I was forced to make one compromise in order to achieve the level of Intellisense integration I wanted.

The docs recommend using anonymous modules, avoiding redundancy by relying on the loader to derive their name based on their file path. Unfortunately due to the way Intellisense loads references, there is simply no way for the loader running in the Intellisense context to access the file paths. Therefore in order to keep the Intellisense accurate, all of the modules must be explicitly named. It’s a shame that such a restriction must be imposed but it is not dissimilar to how types are named explicitly in C#.

You’ve probably read enough by this point and want to take it for a whirl. You’re going to need Visual Studio 2012 with support for web projects (I’m using VS Express 2012 for Web) and either a clone of this git repository or a zipball of the latest code. I’ve stuck with Colin’s original directory structure but only updated the iPhonePropertySearch project - does anyone other than Colin actually use a Windows Phone? Smile | <img src= " />   


You should be able to open any of the JavaScript files in Scripts (excluding _references.js and lib/*) and get full Intellisense auto-completion for any of the dependencies referenced at the top of the file. Depending on exactly where in the file and from which dependency you try to auto-complete, it may take a couple of ctrl+spaces to make the dependencies fully load, I’ll explain why in detail a little later on.

Image 4 

Intellisense uses pseudo-execution of the JavaScript code to offer it’s auto-complete suggestions, so in order to get anywhere we’re going to need to point it in the direction of our code. The easiest way to do this is to rely on the implicit Intellisense reference that is added to all Web projects by default ~/Scripts/_references.js. This file will be parsed by Intellisense ahead of any other code, allowing you to add references to other files and generally make changes to the JavaScript code specific to the Intellisense execution environment (i.e. the file won’t be included in client code).

For the Property Finder app we first reference jQuery to avoid load order problems and then require.js. Require.js also needs an additional bit of configuration to tell it the base location from which to resolve module references, in this case the Scripts folder found in the project root. If you need to customise any of the paths the Intellisense documentation covers the path format and resolution rules in more detail.

JavaScript
// jQuery is loaded separately into the page to avoid load
// order problems so we must reference it separately here
// also.
// http://requirejs.org/docs/jquery.html#advanced
/// <reference path="lib/jquery-1.8.1.js" />
// Require.js is included directly when loaded in the browser
// (only while developing) so we need to reference it here so
// that intellisense will recognise it.
/// <reference path="lib/require.js" />
// Tell require.js where this projects scripts are located.
requirejs.config({
  baseUrl: '~/Scripts/'
}); 

When Intellisense encounters the jQuery and require.js references it will additionally look for similarly named intellisense files and load those in addition to the originals. In this case there is a jquery-1.8.1.intellisense.js file which provides additional documentation for jQuery and a require.intellisense.js file which provides Intellisense with the extra help it needs to correctly resolve the require.js dependency syntax.

If you want to use require.js with jQuery in your project the following files are the bare minimum you’ll need to make Intellisense work correctly -
  • Scripts/_references.js  
  • Scripts/lib/require.js 
  • Scripts/lib/require.intellisense.js 
  • Scripts/lib/jquery.js 
  • Scripts/lib/jquery-1.8.1.js 
  • Scripts/lib/jquery-1.8.1.intellisense.js 

And the following bootstrap code in your HTML page (where Scripts/app.js is the root of your application) -

HTML
<script src="Scripts/lib/jquery-1.8.1.js" type="text/javascript"></script>
<script src="Scripts/lib/require.js" type="text/javascript" data-main="Scripts/app"></script>

Optimising for Release 

One of the criticisms often levelled at AMD is that it shouldn’t be the responsibility of the client to resolve all the script references in the released code. Doing so can result in a much slower experience for users while their browser labours away in the background making multiple roundtrips to load individual JavaScript files, if you throw a mobile connection into the mix things start to look very grim!

Ideally we’d run the loader code once to generate the complete dependency bundle, throw away the loader code (it’s not needed anymore) and then run a standard JavaScript optimiser over the resulting bundle. As normally happens, someone else already thought of that and has kindly done that work for us in the form of the require.js optimiser and almond.js.

The require.js optimiser is a command line tool, running on top of Node.js, which resolves module dependencies and optimises the result. Normally require.js would still be required as a separate include in the HTML page. However, by including almond.js in the optimiser output, it acts as a lightweight bundled replacement for require.js.

To run the optimiser from the project directory run the following command -

MC++
r.js -o build.json 

This tells the require.js optimiser to use the configuration found in build.json to produce the optimised output file. The build.json in the Property Finder project is based on the example from the almond.js documentation -
JavaScript
{
  baseUrl: 'Scripts',
  name: '../almond',
  include: ['lib/jquery-1.8.1','app'],
  insertRequire: ['app'],
  out: 'Scripts/app.min.js',
  wrap: true
}

To make your app use the new optimised version remove the two script references to jQuery and require.js from your HTML page and add this in their place -
HTML
<script src="Scripts/app.min.js" type="text/javascript"></script>

It could be argued that this isn’t a big issue here, as all the code will be local to the device once the application is packaged. However, on resource constrained mobile devices we should be aiming to make the browser’s life as easy as possible.

What's Happening Behind the Scenes?

When you first start typing you’ll see logging in the the JavaScript Language Service output window stating that the modules which have been require’d have been loaded. These are the direct dependencies of the file, but what about the dependencies of the dependencies? Those dependencies are referred to as transitive dependencies and on each successive press of ctrl+space another layer of transitive dependencies will be loaded, until everything referenced in the entire dependency tree is loaded.

You might be thinking that this progressive load of dependencies must lead to some form of race condition i.e. attempting to auto-complete for a reference that hasn’t yet being loaded. Initially I was concerned about this too, but it turns out there are two very convenient mitigating factors.

Dependency Caching

The first relates to how intellisense handles referenced files. Once a file has been detected as being a dependency of the current file, that fact is set in stone until the current file is closed. This set of dependencies is then automatically processed before processing the current file. Great news for us, as it means that the layer-by-layer transitive dependency loading only needs to happen once.

Unfortunately there’s a flipside to this, it turns out that the auto-loading is the reason that anonymous modules can’t be supported. The auto-loading is a feature of Intellisense so it all happens before the modules are requested by require.js, so as far as it is concerned anonymous modules are being loaded which it didn’t request and therefore can’t accurately name. The net result being it panics and the following will show up in the log -

ERROR:mismatch::Error: Mismatched anonymous define() module

Object Graph Traversal 

The second reason is far more mundane, if you need a transitive dependency from n-layers deep it is very likely that you’ve navigated at least n objects to get to that dependency. That’s a confusing statement but I can’t think of another way of describing it so maybe an example will help -

Let’s say that Car (n=0) depends on Wheel (n=1) which itself depends on Tyre (n=2). When you goto reference the Tyre from an instance of Car it is most likely found via a reference like this.frontLeftWheel.tyre.PROPERTY. In this case you’ve traversed over 3 objects before needing the properties defined in Tyre, so Intellisense will be able to supply them (i.e. 3>2).

This is obviously not foolproof so you will probably bump into Intellisense errors from time to time when first editing a newly opened file. These errors manifest themselves as an autocompletion list of every property known to mankind with yellow icons next to them. If this happens, then pressing ctrl+space the pre-requisite number of times should fix things by allowing Intellisense to traverse those extra layers of transitive dependencies.

Conclusion - Was it Worth it? 

Let’s start with a quick rundown of the changes -
  1. Removed the intellisense.js file which referenced all of the JavaScript files in dependency order and the back-reference to the intellisense.js file from each of the JavaScript files. There is no longer a need to maintain a list of source files specifically for Intellisense as this is derived from the AMD defined dependencies listed in the files themselves. A Scripts/_references.js file was added, but as this is a build-in implicit reference there was no need to back-reference it from each of the JavaScript files. 
     
  2. Removed the Namespaces.js file which established the namespace objects for the JavaScript code. AMD does not make use of any globals so there is no need to namespace code in the same way. Additionally naming collisions are implicitly prevented because the relative file path forms the module identifier.
     
  3. Removed the list of script tags in index.html which referenced all of the JavaScript files in dependency order. Another long list of source files that is no longer required, it was replaced with a single script tag for require.js and a reference to the main-module of the application. 
    In fact the original list contained an error in the dependency order, PropertySearchViewModel referenced LocationViewModel but was included before it. This didn’t cause a runtime error because of how the reference is used, but does demonstrate how easy it is to get the dependency list wrong.  
     
  4. Added an optimised release version of the JavaScript which is again built from the AMD defined dependencies.

I think it’s pretty obvious from the above that AMD can bring many benefits to a larger JavaScript project, when you combine that with Intellisense that “just works” it is a very compelling proposition. I hope you feel inspired to try it out and good luck integrating it with your project!

 

Chris 


Appendix - A Couple of Common JavaScript Mistakes

Whilst porting the project over to use AMD I came across a couple of situations where there seemed to be some misunderstanding about JavaScript semantics. I don’t want to poke holes in Colin’s code, I’m only covering them here for the benefit of anyone who may have struggled with the same concepts.

JavaScript
new window["ViewModel"][state.factoryName](); 

The above code creates a new view-model by calling a constructor function based on a variable. There’s nothing at all wrong with the pattern and the code works just fine, the issue here is the unnecessary use of square-bracket notation as reported by JSLint/JSHint -

Line 1: new window["ViewModel"][state.factoryName]();
['ViewModel']'is better written in dot notation. 

Square bracket notation should not be required if the property name is a constant string, exceptions to this rule are when the constant string starts with a number or contains an illegal identifier character. However, in this case it is string consisting entirely of letters and can be re-written more clearly in dot-notation as -
JavaScript
new ViewModel[state.factoryName]();

The second issue I spotted is again functionally sound but highlights a potential misunderstanding about another language feature -
JavaScript
propertySearchViewModel.addToFavourites.call(propertySearchViewModel, currentViewModel());

The call method on the Function prototype allows a method to be invoked with a specified context rather than the implicit context associated with the invocation. The implicit context in the above code would be propertySearchViewModel (that is the object from which the function reference is being invoked) so there is no need to force the context to be propertySearchViewModel. Instead the code can be re-written more clearly as -
JavaScript
propertySearchViewModel.addToFavourites(currentViewModel());

This is true for any of the following situations -

a.b.apply(a, [c...]) equivalent to a.b(c...)
a.b.apply(a) equivalent to a.b()
a.b.call(a, c...) equivalent to a.b(c...)
a.b.call(a) equivalent to a.b()

N.B. in this case JSLint/JSHint does not warn of any issues but I believe it should.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)