New Linting API

Article by: Raymond Camden

New Linting API

One of the more popular types of Brackets extensions are linters (or code quality checkers). Brackets ships with JSLint support built in, but you can currently find extensions for CSSLint, W3CValidation, JSHint, and even JSON. Brackets 31 introduces an improved API for better linting integration.

When I first experimented with writing Brackets extensions I wrote a JSHint extension as a way to get a feel for the process. After I did that I quickly wrote a CSSLinter and a W3CValidator. As you can imagine, these extensions were virtually the exact same code just with different linting engines in the back end.

My code basically revolved around:

  • Adding a menu option for my linter
  • Detecting when the menu option was used
  • Determining if the file matched what I could lint
  • Asking the Brackets editor for the current text
  • Passing that text to my linter
  • And finally – writing out the results to a panel at the bottom

Of the steps above, literally the only thing changing was the call to the linter being used. Here is an example from the CSSLinter extension. I’m not sharing all the code, just the core JavaScript file. The other bits include a few HTML templates, a style sheet for my results, and the linter itself.


/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, $, window, CSSLint, Mustache */
define(function (require, exports, module) {
    'use strict';
    var AppInit = brackets.getModule("utils/AppInit"),
        Commands = brackets.getModule("command/Commands"),
        CommandManager = brackets.getModule("command/CommandManager"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        EditorManager = brackets.getModule("editor/EditorManager"),
        ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
        LanguageManager = brackets.getModule("language/LanguageManager"),
        Menus = brackets.getModule("command/Menus"),
        PanelManager = brackets.getModule("view/PanelManager");
    var panelHtml = require("text!templates/bottom-panel.html"),
        tableHtml = require("text!templates/csslint-table.html");
    require("csslint/csslint");
    // Commands
    var VIEW_HIDE_CSSLINT = "csslint.run";
    //Determines if we are enabled or not. Previously we based this on if we could
    //see the panel, but now the panel will be hidden on non-CSS files
    var cssLintEnabled = false; var $csslint;
    function isCSSDoc(fileEntry) {
        console.dir(fileEntry);
        var language = LanguageManager.getLanguageForPath(fileEntry.file.fullPath);
        // Maybe in the future LESS
        return (language.getId() === "css" || language.getId() === "sass");
    }
    function _handleLint() {
        var messages, results;
        var editor = EditorManager.getCurrentFullEditor();
        if (!editor) { $csslint.hide(); EditorManager.resizeEditor(); return; }
        if (!isCSSDoc(editor.document)) { $csslint.hide(); EditorManager.resizeEditor(); return; }
           else { $csslint.show(); EditorManager.resizeEditor(); }
        var text = editor.document.getText();
        results = CSSLint.verify(text);
        messages = results.messages;
        if (results.messages.length) {
            var $selectedRow;
            var html = Mustache.render(tableHtml, {reportList: results.messages});
            $("#csslint .resizable-content").empty().append(html);
            $("#csslint .resizable-content").find("tr").on("click", function (e) {
              if ($selectedRow) { $selectedRow.removeClass("selected"); }
              $(this).addClass("selected");
              $selectedRow = $(this);
              var lineTd = $(this).find("td.line");
              var line = lineTd.text();
              var col = lineTd.data("col");
              var editor = EditorManager.getCurrentFullEditor();
              editor.setCursorPos(line - 1, col - 1);
              EditorManager.focusEditor();
             });
        }
        else {
            $("#csslint .resizable-content").empty().append("<p>No issues.</p>");
        }
    }
    function _handleShowCSSLint() {
        if (cssLintEnabled) {
            cssLintEnabled = false;
            CommandManager.get(VIEW_HIDE_CSSLINT).setChecked(false);
            $(DocumentManager).off("currentDocumentChange documentSaved", null, _handleLint);
            // if visible, hide
            $csslint.hide();
            EditorManager.resizeEditor();
        }
        else {
            cssLintEnabled = true;
            CommandManager.get(VIEW_HIDE_CSSLINT).setChecked(true);
            $(DocumentManager).on("currentDocumentChange documentSaved", _handleLint);
            _handleLint();
        }
    }
    CommandManager.register("Enable CSSLint", VIEW_HIDE_CSSLINT, _handleShowCSSLint);
    AppInit.htmlReady(function () {
        var s;
        ExtensionUtils.loadStyleSheet(module, "csslint.css");
        s = Mustache.render(panelHtml);
        $csslint = PanelManager.createBottomPanel("csslint.display.csslint", $(s), 200);
        var menu = Menus.getMenu(Menus.AppMenuBar.VIEW_MENU);
        menu.addMenuItem(VIEW_HIDE_CSSLINT, "", Menus.AFTER);
        $('#csslint .csslint-close').click(function () {
            CommandManager.execute(VIEW_HIDE_CSSLINT);
        });
      });
 });

I won’t go over the code line by line, but even if you’ve never seen the code behind a Brackets extension before, you can probably recognize the various parts that handle each of the points I made above. Not terribly difficult of course and the total line count (again, for this part of the extension) is only 134 lines.

Brackets Sprint 31 makes this incredibly simpler. Extension writers now have a way to register their own linting code for a file format. This is done via a CodeInspection module that ships with Brackets (and is used by the built in JSLint support). By using CodeInspection.register, you can specify that for a particular file type, your code should be run for linting. Note that currently there is no way to handle multiple linters so if you use multiple extensions for one file type, the last one will probably win, but support for handling that better will come in the future.

So what does this mean for code? Here is the new version of the CSSLinter:


/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, $, window, CSSLint, Mustache */
define(function (require, exports, module) {
    'use strict';
    var AppInit = brackets.getModule("utils/AppInit"),
        CodeInspection = brackets.getModule("language/CodeInspection");
    require("csslint/csslint");
    function cssLinter(text, fullPath) {
        var results;
        results = CSSLint.verify(text);
        if (results.messages.length) {
            var result = { errors: [] };
            for(var i=0, len=results.messages.length; i<len; i++) {
                var messageOb = results.messages[i];
                //default
                var type = CodeInspection.Type.WARNING;
                if(messageOb.type === "error") {
                     type = CodeInspection.Type.ERROR;
                }
                else if(messageOb.type === "warning") {
                    type = CodeInspection.Type.WARNING;
                }
                result.errors.push({pos: {line:messageOb.line-1, ch:messageOb.col}, message:messageOb.message, type:type });
            }
            return result;
        }
        else {
            //no errors
            return null;
        }
    }
    AppInit.appReady(function () { CodeInspection.register("css", { name: "CSSLint", scanFile: cssLinter });});
}); 

54 lines. Round about 1/3 of the size of the previous version. Oh, and I also got to get rid of the external style sheets and the HTML templates. Brackets now handles all of that for me. Let’s look at an example in the UI. I’ve got a CSS file open and I’ve intentionally broken it.

Note that I removed a closing } from the h1, h2, etc CSS declaration and I used a bad CSS style called “frak”. In the bottom right corner I’ve called out the yellow icon. That’s my CSS linter working automatically and detecting that something is wrong. If you click on that icon, the issues are displayed:

All of the design you see here was done by Brackets itself. Even better, when the folks who handle UX at Brackets decide to make improvements, my extension will get it automatically.

You can check this out today in the latest build of Brackets. If you haven’t installed my extension yet, remember that you can do it via the handy graphical extension manager.

Leave a Reply

Your email address will not be published. Required fields are marked *