diff --git a/src/htmlContent/problems-panel-table.html b/src/htmlContent/problems-panel-table.html
index d8cc2b001a6..2e382840afd 100644
--- a/src/htmlContent/problems-panel-table.html
+++ b/src/htmlContent/problems-panel-table.html
@@ -1,11 +1,16 @@
{{#reportList}}
+
+ {{providerName}} ({{results.length}})
+
+ {{#results}}
{{friendlyLine}}
- {{message}}
- {{codeSnippet}}
+ {{message}}
+ {{codeSnippet}}
+ {{/results}}
{{/reportList}}
diff --git a/src/htmlContent/problems-panel.html b/src/htmlContent/problems-panel.html
index 6c5b06906a6..e0eea02f28e 100644
--- a/src/htmlContent/problems-panel.html
+++ b/src/htmlContent/problems-panel.html
@@ -1,7 +1,7 @@
+
\ No newline at end of file
diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js
index 2a537ac753a..4d8e81b1418 100644
--- a/src/language/CodeInspection.js
+++ b/src/language/CodeInspection.js
@@ -1,24 +1,24 @@
/*
* Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
- *
+ *
* Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
- *
+ *
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
- *
+ *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
- *
+ *
*/
@@ -38,7 +38,9 @@
*/
define(function (require, exports, module) {
"use strict";
-
+
+ var _ = require("thirdparty/lodash");
+
// Load dependent modules
var Commands = require("command/Commands"),
PanelManager = require("view/PanelManager"),
@@ -54,15 +56,16 @@ define(function (require, exports, module) {
AppInit = require("utils/AppInit"),
Resizer = require("utils/Resizer"),
StatusBar = require("widgets/StatusBar"),
+ Async = require("utils/Async"),
PanelTemplate = require("text!htmlContent/problems-panel.html"),
ResultsTemplate = require("text!htmlContent/problems-panel-table.html");
-
+
var INDICATOR_ID = "status-inspection",
defaultPrefs = {
enabled: brackets.config["linting.enabled_by_default"],
collapsed: false
};
-
+
/** Values for problem's 'type' property */
var Type = {
/** Unambiguous error, such as a syntax error */
@@ -72,13 +75,13 @@ define(function (require, exports, module) {
/** Inspector unable to continue, code too complex for static analysis, etc. Not counted in error/warning tally. */
META: "problem_type_meta"
};
-
+
/**
* @private
* @type {PreferenceStorage}
*/
var _prefs = null;
-
+
/**
* When disabled, the errors panel is closed and the status bar icon is grayed out.
* Takes precedence over _collapsed.
@@ -86,20 +89,20 @@ define(function (require, exports, module) {
* @type {boolean}
*/
var _enabled = true;
-
+
/**
* When collapsed, the errors panel is closed but the status bar icon is kept up to date.
* @private
* @type {boolean}
*/
var _collapsed = false;
-
+
/**
* @private
* @type {$.Element}
*/
var $problemsPanel;
-
+
/**
* @private
* @type {$.Element}
@@ -111,19 +114,19 @@ define(function (require, exports, module) {
* @type {boolean}
*/
var _gotoEnabled = false;
-
+
/**
* @private
- * @type {Object.}
+ * @type {Object.>}
*/
var _providers = {};
-
+
/**
* @private
- * @type {?Array.}
+ * @type {boolean}
*/
- var _lastResult;
-
+ var _hasErrors;
+
/**
* Enable or disable the "Go to First Error" command
* @param {boolean} gotoEnabled Whether it is enabled.
@@ -132,52 +135,70 @@ define(function (require, exports, module) {
CommandManager.get(Commands.NAVIGATE_GOTO_FIRST_PROBLEM).setEnabled(gotoEnabled);
_gotoEnabled = gotoEnabled;
}
-
-
+
+ function _unregisterAll() {
+ _providers = {};
+ }
+
/**
- * Returns a provider for given file path, if one is available.
+ * Returns a list of provider for given file path, if available.
* Decision is made depending on the file extension.
*
* @param {!string} filePath
- * @return ?{{name:string, scanFile:function(string, string):?{!errors:Array, aborted:boolean}} provider
+ * @return ?{Array.<{name:string, scanFile:function(string, string):?{errors:!Array, aborted:boolean}}>} provider
*/
- function getProviderForPath(filePath) {
+ function getProvidersForPath(filePath) {
return _providers[LanguageManager.getLanguageForPath(filePath).getId()];
}
/**
- * Runs a file inspection over passed file, specifying a provider is optional.
+ * Runs a file inspection over passed file. Uses the given list of providers if specified, otherwise uses
+ * the set of providers that are registered for the file's language.
* This method doesn't update the Brackets UI, just provides inspection results.
- * These results will reflect any unsaved changes present in the file that is currently opened.
+ * These results will reflect any unsaved changes present in the file if currently open.
+ *
+ * The Promise yields an array of provider-result pair objects (the result is the return value of the
+ * provider's scanFile() - see register() for details). The result object may be null if there were no
+ * errors from that provider.
+ * If there are no providers registered for this file, the Promise yields null instead.
*
* @param {!File} file File that will be inspected for errors.
- * @param ?{{name:string, scanFile:function(string, string):?{!errors:Array, aborted:boolean}} provider
- * @return {$.Promise} a jQuery promise that will be resolved with ?{!errors:Array, aborted:boolean}
+ * @param {?Array.<{name:string, scanFile:function(string, string):?{errors:!Array, aborted:boolean}}>} providerList
+ * @return {$.Promise} a jQuery promise that will be resolved with ?Array.<{provider:Object, result: ?{errors:!Array, aborted:boolean}}>
*/
- function inspectFile(file, provider) {
- var response = new $.Deferred();
- provider = provider || getProviderForPath(file.fullPath);
+ function inspectFile(file, providerList) {
+ var response = new $.Deferred(),
+ results = [];
- if (!provider) {
+ providerList = (providerList || getProvidersForPath(file.fullPath)) || [];
+
+ if (!providerList.length) {
response.resolve(null);
return response.promise();
}
DocumentManager.getDocumentText(file)
.done(function (fileText) {
- var result,
- perfTimerInspector = PerfUtils.markStart("CodeInspection '" + provider.name + "':\t" + file.fullPath);
-
- try {
- result = provider.scanFile(fileText, file.fullPath);
- } catch (err) {
- console.error("[CodeInspection] Provider " + provider.name + " threw an error: " + err);
- response.reject(err);
- return;
- }
+ var perfTimerInspector = PerfUtils.markStart("CodeInspection:\t" + file.fullPath);
+
+ providerList.forEach(function (provider) {
+ var perfTimerProvider = PerfUtils.markStart("CodeInspection '" + provider.name + "':\t" + file.fullPath);
+
+ try {
+ var scanResult = provider.scanFile(fileText, file.fullPath);
+ results.push({provider: provider, result: scanResult});
+ } catch (err) {
+ console.error("[CodeInspection] Provider " + provider.name + " threw an error: " + err);
+ response.reject(err);
+ return;
+ }
+
+ PerfUtils.addMeasurement(perfTimerProvider);
+ });
PerfUtils.addMeasurement(perfTimerInspector);
- response.resolve(result);
+
+ response.resolve(results);
})
.fail(function (err) {
console.error("[CodeInspection] Could not read file for inspection: " + file.fullPath);
@@ -187,82 +208,152 @@ define(function (require, exports, module) {
return response.promise();
}
+ /**
+ * Update the title of the problem panel and the tooltip of the status bar icon. The title and the tooltip will
+ * change based on the number of problems reported and how many provider reported problems.
+ *
+ * @param {Number} numProblems - total number of problems across all providers
+ * @param {Array.<{name:string, scanFile:function(string, string):Object}>} providersReportingProblems - providers that reported problems
+ * @param {boolean} aborted - true if any provider returned a result with the 'aborted' flag set
+ */
+ function updatePanelTitleAndStatusBar(numProblems, providersReportingProblems, aborted) {
+ var message;
+
+ if (providersReportingProblems.length === 1) {
+ // don't show a header if there is only one provider available for this file type
+ $problemsPanelTable.find(".inspector-section").hide();
+
+ if (numProblems === 1 && !aborted) {
+ message = StringUtils.format(Strings.SINGLE_ERROR, providersReportingProblems[0].name);
+ } else {
+ if (aborted) {
+ numProblems += "+";
+ }
+
+ message = StringUtils.format(Strings.MULTIPLE_ERRORS, providersReportingProblems[0].name, numProblems);
+ }
+
+ $problemsPanel.find(".title").text(message);
+ StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-errors", message);
+ } else if (providersReportingProblems.length > 1) {
+ $problemsPanelTable.find(".inspector-section").show();
+
+ if (aborted) {
+ numProblems += "+";
+ }
+
+ message = StringUtils.format(Strings.ERRORS_PANEL_TITLE_MULTIPLE, numProblems);
+ $problemsPanel.find(".title").text(message);
+ StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-errors", message);
+ }
+ }
+
/**
* Run inspector applicable to current document. Updates status bar indicator and refreshes error list in
* bottom panel.
*/
function run() {
if (!_enabled) {
- _lastResult = null;
+ _hasErrors = false;
Resizer.hide($problemsPanel);
StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-disabled", Strings.LINT_DISABLED);
setGotoEnabled(false);
return;
}
-
+
var currentDoc = DocumentManager.getCurrentDocument(),
- provider = currentDoc && getProviderForPath(currentDoc.file.fullPath);
-
- if (provider) {
- inspectFile(currentDoc.file, provider).then(function (result) {
+ providerList = currentDoc && getProvidersForPath(currentDoc.file.fullPath);
+
+ if (providerList && providerList.length) {
+ var numProblems = 0;
+ var aborted = false;
+ var allErrors = [];
+ var html;
+ var providersReportingProblems = [];
+
+ // run all the providers registered for this file type
+ inspectFile(currentDoc.file, providerList).then(function (results) {
// check if current document wasn't changed while inspectFile was running
if (currentDoc !== DocumentManager.getCurrentDocument()) {
return;
}
- _lastResult = result;
+ // how many errors in total?
+ var errors = results.reduce(function (a, item) { return a + (item.result ? item.result.errors.length : 0); }, 0);
+
+ _hasErrors = Boolean(errors);
- if (!result || !result.errors.length) {
+ if (!errors) {
Resizer.hide($problemsPanel);
- StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-valid", StringUtils.format(Strings.NO_ERRORS, provider.name));
+
+ var message = Strings.NO_ERRORS_MULTIPLE_PROVIDER;
+ if (providerList.length === 1) {
+ message = StringUtils.format(Strings.NO_ERRORS, providerList[0].name);
+ }
+
+ StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-valid", message);
+
setGotoEnabled(false);
return;
}
var perfTimerDOM = PerfUtils.markStart("ProblemsPanel render:\t" + currentDoc.file.fullPath);
-
+
// Augment error objects with additional fields needed by Mustache template
- var numProblems = 0;
- result.errors.forEach(function (error) {
- error.friendlyLine = error.pos.line + 1;
- error.codeSnippet = currentDoc.getLine(error.pos.line);
- error.codeSnippet = error.codeSnippet.substr(0, Math.min(175, error.codeSnippet.length)); // limit snippet width
-
- if (error.type !== Type.META) {
- numProblems++;
+ results.forEach(function (inspectionResult) {
+ var provider = inspectionResult.provider;
+
+ if (inspectionResult.result) {
+ inspectionResult.result.errors.forEach(function (error) {
+ // some inspectors don't always provide a line number
+ if (!isNaN(error.pos.line)) {
+ error.friendlyLine = error.pos.line + 1;
+ error.codeSnippet = currentDoc.getLine(error.pos.line);
+ error.codeSnippet = error.codeSnippet.substr(0, Math.min(175, error.codeSnippet.length)); // limit snippet width
+ }
+
+ if (error.type !== Type.META) {
+ numProblems++;
+ }
+ });
+
+ // if the code inspector was unable to process the whole file, we keep track to show a different status
+ if (inspectionResult.result.aborted) {
+ aborted = true;
+ }
+
+ if (inspectionResult.result.errors) {
+ allErrors.push({
+ providerName: provider.name,
+ results: inspectionResult.result.errors
+ });
+
+ providersReportingProblems.push(provider);
+ }
}
});
// Update results table
- var html = Mustache.render(ResultsTemplate, {reportList: result.errors});
+ html = Mustache.render(ResultsTemplate, {reportList: allErrors});
$problemsPanelTable
.empty()
.append(html)
.scrollTop(0); // otherwise scroll pos from previous contents is remembered
-
- $problemsPanel.find(".title").text(StringUtils.format(Strings.ERRORS_PANEL_TITLE, provider.name));
+
if (!_collapsed) {
Resizer.show($problemsPanel);
}
-
- if (numProblems === 1 && !result.aborted) {
- StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-errors", StringUtils.format(Strings.SINGLE_ERROR, provider.name));
- } else {
- // If inspector was unable to process the whole file, number of errors is indeterminate; indicate with a "+"
- if (result.aborted) {
- numProblems += "+";
- }
- StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-errors",
- StringUtils.format(Strings.MULTIPLE_ERRORS, provider.name, numProblems));
- }
+
+ updatePanelTitleAndStatusBar(numProblems, providersReportingProblems, aborted);
setGotoEnabled(true);
PerfUtils.addMeasurement(perfTimerDOM);
});
+
} else {
// No provider for current file
- _lastResult = null;
+ _hasErrors = false;
Resizer.hide($problemsPanel);
var language = currentDoc && LanguageManager.getLanguageForPath(currentDoc.file.fullPath);
if (language) {
@@ -273,23 +364,36 @@ define(function (require, exports, module) {
setGotoEnabled(false);
}
}
-
+
/**
* The provider is passed the text of the file and its fullPath. Providers should not assume
* that the file is open (i.e. DocumentManager.getOpenDocumentForPath() may return null) or
* that the file on disk matches the text given (file may have unsaved changes).
+ *
+ * Registering any provider for the "javascript" language automatically unregisters the built-in
+ * Brackets JSLint provider. This is a temporary convenience until UI exists for disabling
+ * registered providers.
*
* @param {string} languageId
- * @param {{name:string, scanFile:function(string, string):?{!errors:Array, aborted:boolean}} provider
+ * @param {{name:string, scanFile:function(string, string):?{errors:!Array, aborted:boolean}} provider
*
* Each error is: { pos:{line,ch}, endPos:?{line,ch}, message:string, type:?Type }
* If type is unspecified, Type.WARNING is assumed.
*/
function register(languageId, provider) {
- if (_providers[languageId]) {
- console.warn("Overwriting existing inspection/linting provider for language " + languageId);
+ if (!_providers[languageId]) {
+ _providers[languageId] = [];
}
- _providers[languageId] = provider;
+
+ if (languageId === "javascript") {
+ // This is a special case to enable extension provider to replace the JSLint provider
+ // in favor of their own implementation
+ _.remove(_providers[languageId], function (registeredProvider) {
+ return registeredProvider.name === "JSLint";
+ });
+ }
+
+ _providers[languageId].push(provider);
run(); // in case a file of this type is open currently
}
@@ -313,7 +417,7 @@ define(function (require, exports, module) {
$(DocumentManager).off(".codeInspection");
}
}
-
+
/**
* Enable or disable all inspection.
* @param {?boolean} enabled Enabled state. If omitted, the state is toggled.
@@ -323,40 +427,39 @@ define(function (require, exports, module) {
enabled = !_enabled;
}
_enabled = enabled;
-
+
CommandManager.get(Commands.VIEW_TOGGLE_INSPECTION).setChecked(_enabled);
updateListeners();
_prefs.setValue("enabled", _enabled);
-
+
// run immediately
run();
}
-
-
- /**
+
+ /**
* Toggle the collapsed state for the panel. This explicitly collapses the panel (as opposed to
* the auto collapse due to files with no errors & filetypes with no provider). When explicitly
* collapsed, the panel will not reopen automatically on switch files or save.
- *
+ *
* @param {?boolean} collapsed Collapsed state. If omitted, the state is toggled.
*/
function toggleCollapsed(collapsed) {
if (collapsed === undefined) {
collapsed = !_collapsed;
}
-
+
_collapsed = collapsed;
_prefs.setValue("collapsed", _collapsed);
-
+
if (_collapsed) {
Resizer.hide($problemsPanel);
} else {
- if (_lastResult && _lastResult.errors.length) {
+ if (_hasErrors) {
Resizer.show($problemsPanel);
}
}
}
-
+
/** Command to go to the first Error/Warning */
function handleGotoFirstProblem() {
run();
@@ -364,22 +467,21 @@ define(function (require, exports, module) {
$problemsPanel.find("tr:first-child").trigger("click");
}
}
-
-
+
// Register command handlers
CommandManager.register(Strings.CMD_VIEW_TOGGLE_INSPECTION, Commands.VIEW_TOGGLE_INSPECTION, toggleEnabled);
CommandManager.register(Strings.CMD_GOTO_FIRST_PROBLEM, Commands.NAVIGATE_GOTO_FIRST_PROBLEM, handleGotoFirstProblem);
-
+
// Init PreferenceStorage
_prefs = PreferencesManager.getPreferenceStorage(module, defaultPrefs);
-
+
// Initialize items dependent on HTML DOM
AppInit.htmlReady(function () {
// Create bottom panel to list error details
var panelHtml = Mustache.render(PanelTemplate, Strings);
var resultsPanel = PanelManager.createBottomPanel("errors", $(panelHtml), 100);
$problemsPanel = $("#problems-panel");
-
+
var $selectedRow;
$problemsPanelTable = $problemsPanel.find(".table-container")
.on("click", "tr", function (e) {
@@ -389,40 +491,56 @@ define(function (require, exports, module) {
$selectedRow = $(e.currentTarget);
$selectedRow.addClass("selected");
- var lineTd = $selectedRow.find(".line-number");
- var line = parseInt(lineTd.text(), 10) - 1; // convert friendlyLine back to pos.line
- var character = lineTd.data("character");
- var editor = EditorManager.getCurrentFullEditor();
- editor.setCursorPos(line, character, true);
- EditorManager.focusEditor();
+ // This is a inspector title row, expand/collapse on click
+ if ($selectedRow.hasClass("inspector-section")) {
+ // Clicking the inspector title section header collapses/expands result rows
+ $selectedRow.nextUntil(".inspector-section").toggle();
+
+ var $triangle = $(".disclosure-triangle", $selectedRow);
+ $triangle.toggleClass("expanded").toggleClass("collapsed");
+ } else {
+ // This is a problem marker row, show the result on click
+ // Grab the required position data
+ var lineTd = $selectedRow.find(".line-number");
+ var line = parseInt(lineTd.text(), 10) - 1; // convert friendlyLine back to pos.line
+ // if there is no line number available, don't do anything
+ if (!isNaN(line)) {
+ var character = lineTd.data("character");
+
+ var editor = EditorManager.getCurrentFullEditor();
+ editor.setCursorPos(line, character, true);
+ EditorManager.focusEditor();
+ }
+ }
});
$("#problems-panel .close").click(function () {
toggleCollapsed(true);
});
-
+
// Status bar indicator - icon & tooltip updated by run()
var statusIconHtml = Mustache.render("
", Strings);
StatusBar.addIndicator(INDICATOR_ID, $(statusIconHtml), true, "", "", "status-indent");
-
+
$("#status-inspection").click(function () {
// Clicking indicator toggles error panel, if any errors in current file
- if (_lastResult && _lastResult.errors.length) {
+ if (_hasErrors) {
toggleCollapsed();
}
});
-
-
+
// Set initial UI state
toggleEnabled(_prefs.getValue("enabled"));
toggleCollapsed(_prefs.getValue("collapsed"));
});
-
-
+
+ // Testing
+ exports._unregisterAll = _unregisterAll;
+
// Public API
- exports.register = register;
- exports.Type = Type;
- exports.toggleEnabled = toggleEnabled;
- exports.inspectFile = inspectFile;
+ exports.register = register;
+ exports.Type = Type;
+ exports.toggleEnabled = toggleEnabled;
+ exports.inspectFile = inspectFile;
});
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index 797f97f55e8..6ae663e3e6a 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -199,12 +199,11 @@ define({
"STATUSBAR_USER_EXTENSIONS_DISABLED" : "User Extensions Disabled",
// CodeInspection: errors/warnings
- "ERRORS_PANEL_TITLE" : "{0} Errors",
- "ERRORS_PANEL_TITLE_SINGLE" : "{0} Issues",
- "ERRORS_PANEL_TITLE_MULTI" : "Lint Issues",
- "SINGLE_ERROR" : "1 {0} Error",
- "MULTIPLE_ERRORS" : "{1} {0} Errors",
- "NO_ERRORS" : "No {0} errors - good job!",
+ "ERRORS_PANEL_TITLE_MULTIPLE" : "{0} Problems",
+ "SINGLE_ERROR" : "1 {0} Problem",
+ "MULTIPLE_ERRORS" : "{1} {0} Problems",
+ "NO_ERRORS" : "No {0} problems found - good job!",
+ "NO_ERRORS_MULTIPLE_PROVIDER" : "No problems found - good job!",
"LINT_DISABLED" : "Linting is disabled",
"NO_LINT_AVAILABLE" : "No linter available for {0}",
"NOTHING_TO_LINT" : "Nothing to lint",
diff --git a/src/styles/brackets.less b/src/styles/brackets.less
index 6f65e29b1d2..5fae3503333 100644
--- a/src/styles/brackets.less
+++ b/src/styles/brackets.less
@@ -982,7 +982,7 @@ a, img {
}
}
-#search-results .disclosure-triangle {
+#search-results .disclosure-triangle, #problems-panel .disclosure-triangle {
.jstree-sprite;
display: inline-block;
&.expanded {
@@ -1334,8 +1334,22 @@ a, img {
.line {
text-align: right; // make line number line up with editor line numbers
}
-}
+ .line-text {
+ white-space: nowrap;
+ width: auto;
+ }
+
+ .line-snippet {
+ white-space: nowrap;
+ width: 100%;
+ padding-left: 10px;
+ }
+
+ .inspector-section > td {
+ padding-left: 5px;
+ }
+}
/* Line up label text and input text */
label input {
diff --git a/src/utils/Async.js b/src/utils/Async.js
index 60ccb8c4a1d..a4086eda62e 100644
--- a/src/utils/Async.js
+++ b/src/utils/Async.js
@@ -280,8 +280,7 @@ define(function (require, exports, module) {
return masterDeferred.promise();
}
-
-
+
/** Value passed to fail() handlers that have been triggered due to withTimeout()'s timeout */
var ERROR_TIMEOUT = {};
diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js
index 9088b3d93ec..8664763f98b 100644
--- a/test/UnitTestSuite.js
+++ b/test/UnitTestSuite.js
@@ -29,6 +29,7 @@ define(function (require, exports, module) {
require("spec/Async-test");
require("spec/CodeHint-test");
require("spec/CodeHintUtils-test");
+ require("spec/CodeInspection-test");
require("spec/CommandManager-test");
require("spec/CSSUtils-test");
require("spec/JSUtils-test");
diff --git a/test/spec/CodeInspection-test-files/errors.css b/test/spec/CodeInspection-test-files/errors.css
new file mode 100644
index 00000000000..061a1f9fc5f
--- /dev/null
+++ b/test/spec/CodeInspection-test-files/errors.css
@@ -0,0 +1,3 @@
+h1 {
+ color: black;
+
\ No newline at end of file
diff --git a/test/spec/CodeInspection-test-files/errors.js b/test/spec/CodeInspection-test-files/errors.js
new file mode 100644
index 00000000000..8254868cb3d
--- /dev/null
+++ b/test/spec/CodeInspection-test-files/errors.js
@@ -0,0 +1,2 @@
+// mispelled function keyword
+funtion foo() {};
\ No newline at end of file
diff --git a/test/spec/CodeInspection-test-files/no-errors.js b/test/spec/CodeInspection-test-files/no-errors.js
new file mode 100644
index 00000000000..fff4bd0ac3e
--- /dev/null
+++ b/test/spec/CodeInspection-test-files/no-errors.js
@@ -0,0 +1,3 @@
+function foo() {
+ "use strict";
+}
\ No newline at end of file
diff --git a/test/spec/CodeInspection-test.js b/test/spec/CodeInspection-test.js
new file mode 100644
index 00000000000..47a512d18b2
--- /dev/null
+++ b/test/spec/CodeInspection-test.js
@@ -0,0 +1,539 @@
+/*
+ * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
+/*global define, describe, it, expect, beforeEach, beforeFirst, afterEach, afterLast, waitsFor, runs, brackets, waitsForDone, spyOn, xit, jasmine */
+
+define(function (require, exports, module) {
+ "use strict";
+
+ var SpecRunnerUtils = require("spec/SpecRunnerUtils"),
+ FileSystem = require("filesystem/FileSystem");
+
+ describe("Code Inspection", function () {
+ this.category = "integration";
+
+ var testFolder = SpecRunnerUtils.getTestPath("/spec/CodeInspection-test-files/"),
+ testWindow,
+ $,
+ brackets,
+ CodeInspection,
+ EditorManager;
+
+ var toggleJSLintResults = function (visible) {
+ $("#status-inspection").triggerHandler("click");
+ expect($("#problems-panel").is(":visible")).toBe(visible);
+ };
+
+ function createCodeInspector(name, result) {
+ var provider = {
+ name: name,
+ // arguments to this function: text, fullPath
+ // omit the warning
+ scanFile: function () { return result; }
+ };
+
+ spyOn(provider, "scanFile").andCallThrough();
+
+ return provider;
+ }
+
+ function successfulLintResult() {
+ return {errors: []};
+ }
+
+ function failLintResult() {
+ return {
+ errors: [
+ {
+ pos: { line: 1, ch: 3 },
+ message: "Some errors here and there",
+ type: CodeInspection.Type.WARNING
+ }
+ ]
+ };
+ }
+
+ beforeFirst(function () {
+ runs(function () {
+ SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
+ testWindow = w;
+ // Load module instances from brackets.test
+ $ = testWindow.$;
+ brackets = testWindow.brackets;
+ EditorManager = brackets.test.EditorManager;
+ CodeInspection = brackets.test.CodeInspection;
+ CodeInspection.toggleEnabled(true);
+ });
+ });
+
+ runs(function () {
+ SpecRunnerUtils.loadProjectInTestWindow(testFolder);
+ });
+ });
+
+ afterEach(function () {
+ testWindow.closeAllFiles();
+ });
+
+ afterLast(function () {
+ testWindow = null;
+ $ = null;
+ brackets = null;
+ EditorManager = null;
+ SpecRunnerUtils.closeTestWindow();
+ });
+
+ describe("Unit level tests", function () {
+ var simpleJavascriptFileEntry;
+
+ beforeEach(function () {
+ CodeInspection._unregisterAll();
+ simpleJavascriptFileEntry = new FileSystem.getFileForPath(testFolder + "/errors.js");
+ });
+
+ it("should run a single registered linter", function () {
+ var codeInspector = createCodeInspector("text linter", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(codeInspector.scanFile).toHaveBeenCalled();
+ });
+ });
+
+ it("should run two linters", function () {
+ var codeInspector1 = createCodeInspector("text linter 1", successfulLintResult());
+ var codeInspector2 = createCodeInspector("text linter 2", successfulLintResult());
+
+ CodeInspection.register("javascript", codeInspector1);
+ CodeInspection.register("javascript", codeInspector2);
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(codeInspector1.scanFile).toHaveBeenCalled();
+ expect(codeInspector2.scanFile).toHaveBeenCalled();
+ });
+ });
+
+ it("should run one linter return some errors", function () {
+ var result;
+
+ var codeInspector1 = createCodeInspector("javascript linter", failLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+ promise.done(function (lintingResult) {
+ result = lintingResult;
+ });
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(codeInspector1.scanFile).toHaveBeenCalled();
+ expect(result.length).toEqual(1);
+ expect(result[0].provider.name).toEqual("javascript linter");
+ expect(result[0].result.errors.length).toEqual(1);
+ });
+ });
+
+ it("should run two linter and return some errors", function () {
+ var result;
+
+ var codeInspector1 = createCodeInspector("javascript linter 1", failLintResult());
+ var codeInspector2 = createCodeInspector("javascript linter 2", failLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+ CodeInspection.register("javascript", codeInspector2);
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+ promise.done(function (lintingResult) {
+ result = lintingResult;
+ });
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(result.length).toEqual(2);
+ expect(result[0].result.errors.length).toEqual(1);
+ expect(result[1].result.errors.length).toEqual(1);
+ });
+ });
+
+ it("should not call any other linter for javascript document", function () {
+ var codeInspector1 = createCodeInspector("any other linter linter 1", successfulLintResult());
+ CodeInspection.register("whatever", codeInspector1);
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(codeInspector1.scanFile).not.toHaveBeenCalled();
+ });
+ });
+
+ it("should call linter even if linting on save is disabled", function () {
+ var codeInspector1 = createCodeInspector("javascript linter 1", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+
+ CodeInspection.toggleEnabled(false);
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(codeInspector1.scanFile).toHaveBeenCalled();
+
+ CodeInspection.toggleEnabled(true);
+ });
+ });
+
+ it("should return no result if there is no linter registered", function () {
+ var expectedResult;
+
+ runs(function () {
+ var promise = CodeInspection.inspectFile(simpleJavascriptFileEntry);
+ promise.done(function (result) {
+ expectedResult = result;
+ });
+
+ waitsForDone(promise, "file linting", 5000);
+ });
+
+ runs(function () {
+ expect(expectedResult).toBeNull();
+ });
+ });
+ });
+
+ describe("Code Inspection UI", function () {
+ beforeEach(function () {
+ CodeInspection._unregisterAll();
+ });
+
+ it("should run test linter when a JavaScript document opens and indicate errors in the panel", function () {
+ var codeInspector = createCodeInspector("javascript linter", failLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect($("#problems-panel").is(":visible")).toBe(true);
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+ });
+ });
+
+ it("should show problems panel after too many errors", function () {
+ var lintResult = {
+ errors: [
+ {
+ pos: { line: 1, ch: 3 },
+ message: "Some errors here and there",
+ type: CodeInspection.Type.WARNING
+ },
+ {
+ pos: { line: 1, ch: 5 },
+ message: "Stopping. (33% scanned).",
+ type: CodeInspection.Type.META
+ }
+ ],
+ aborted: true
+ };
+
+ var codeInspector = createCodeInspector("javascript linter", lintResult);
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect($("#problems-panel").is(":visible")).toBe(true);
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ var tooltip = $statusBar.attr("title");
+ // tooltip will contain + in the title if the inspection was aborted
+ expect(tooltip.lastIndexOf("+")).not.toBe(-1);
+ });
+ });
+
+ it("should not run test linter when a JavaScript document opens and linting is disabled", function () {
+ CodeInspection.toggleEnabled(false);
+
+ var codeInspector = createCodeInspector("javascript linter", failLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect(codeInspector.scanFile).not.toHaveBeenCalled();
+ expect($("#problems-panel").is(":visible")).toBe(false);
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ CodeInspection.toggleEnabled(true);
+ });
+ });
+
+ it("should not show the problems panel when there is no linting error", function () {
+ var codeInspector = createCodeInspector("javascript linter", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect($("#problems-panel").is(":visible")).toBe(false);
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+ });
+ });
+
+ it("status icon should toggle Errors panel when errors present", function () {
+ var codeInspector = createCodeInspector("javascript linter", failLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file");
+
+ runs(function () {
+ toggleJSLintResults(false);
+ toggleJSLintResults(true);
+ });
+ });
+
+ it("should run two linter and display two expanded collapsible sections in the errors panel", function () {
+ var codeInspector1 = createCodeInspector("javascript linter 1", failLintResult());
+ var codeInspector2 = createCodeInspector("javascript linter 2", failLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+ CodeInspection.register("javascript", codeInspector2);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ var $inspectorSections = $(".inspector-section td");
+ expect($inspectorSections.length).toEqual(2);
+ expect($inspectorSections[0].innerHTML.lastIndexOf("javascript linter 1 (1)")).not.toBe(-1);
+ expect($inspectorSections[1].innerHTML.lastIndexOf("javascript linter 2 (1)")).not.toBe(-1);
+
+ var $expandedInspectorSections = $inspectorSections.find(".expanded");
+ expect($expandedInspectorSections.length).toEqual(2);
+ });
+ });
+
+ it("should run the linter and display no collapsible header section in the errors panel", function () {
+ var codeInspector1 = createCodeInspector("javascript linter 1", failLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect($(".inspector-section").is(":visible")).toBeFalsy();
+ });
+ });
+
+ it("status icon should not toggle Errors panel when no errors present", function () {
+ var codeInspector = createCodeInspector("javascript linter", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file");
+
+ runs(function () {
+ toggleJSLintResults(false);
+ toggleJSLintResults(false);
+ });
+ });
+
+ it("should show the error count and the name of the linter in the panel title for one error", function () {
+ var codeInspector = createCodeInspector("JavaScript Linter", failLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file");
+
+ runs(function () {
+ var $problemPanelTitle = $("#problems-panel .title").text();
+ expect($problemPanelTitle).toBe("1 JavaScript Linter Problem");
+
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ var tooltip = $statusBar.attr("title");
+ expect(tooltip).toBe("1 JavaScript Linter Problem");
+ });
+ });
+
+ it("should show the error count and the name of the linter in the panel title and tooltip for multiple errors", function () {
+ var lintResult = {
+ errors: [
+ {
+ pos: { line: 1, ch: 3 },
+ message: "Some errors here and there",
+ type: CodeInspection.Type.WARNING
+ },
+ {
+ pos: { line: 1, ch: 5 },
+ message: "Some errors there and there and over there",
+ type: CodeInspection.Type.WARNING
+ }
+ ]
+ };
+
+ var codeInspector = createCodeInspector("JavaScript Linter", lintResult);
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file");
+
+ runs(function () {
+ var $problemPanelTitle = $("#problems-panel .title").text();
+ expect($problemPanelTitle).toBe("2 JavaScript Linter Problems");
+
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ var tooltip = $statusBar.attr("title");
+ expect(tooltip).toBe("2 JavaScript Linter Problems");
+ });
+ });
+
+ it("should show the generic panel title if more than one inspector reported problems", function () {
+ var lintResult = failLintResult();
+
+ var codeInspector1 = createCodeInspector("JavaScript Linter1", lintResult);
+ CodeInspection.register("javascript", codeInspector1);
+ var codeInspector2 = createCodeInspector("JavaScript Linter2", lintResult);
+ CodeInspection.register("javascript", codeInspector2);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file");
+
+ runs(function () {
+ var $problemPanelTitle = $("#problems-panel .title").text();
+ expect($problemPanelTitle).toBe("2 Problems");
+
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ var tooltip = $statusBar.attr("title");
+ // tooltip will contain + in the title if the inspection was aborted
+ expect(tooltip).toBe("2 Problems");
+ });
+ });
+
+ it("should show no problems tooltip in status bar for multiple inspectors", function () {
+ var codeInspector = createCodeInspector("JavaScript Linter1", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector);
+ codeInspector = createCodeInspector("JavaScript Linter2", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file");
+
+ runs(function () {
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ var tooltip = $statusBar.attr("title");
+ expect(tooltip).toBe("No problems found - good job!");
+ });
+ });
+
+ it("should show no problems tooltip in status bar for 1 inspector", function () {
+ var codeInspector = createCodeInspector("JavaScript Linter1", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["errors.js"]), "open test file");
+
+ runs(function () {
+ var $statusBar = $("#status-inspection");
+ expect($statusBar.is(":visible")).toBe(true);
+
+ var tooltip = $statusBar.attr("title");
+ expect(tooltip).toBe("No JavaScript Linter1 problems found - good job!");
+ });
+ });
+ });
+
+ describe("Code Inspector Registration", function () {
+ beforeEach(function () {
+ CodeInspection._unregisterAll();
+ });
+
+ it("should unregister JSLint linter if a new javascript linter is registered", function () {
+ var codeInspector1 = createCodeInspector("JSLint", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+ var codeInspector2 = createCodeInspector("javascript inspector", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector2, true);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect(codeInspector1.scanFile).not.toHaveBeenCalled();
+ expect(codeInspector2.scanFile).toHaveBeenCalled();
+ });
+ });
+
+ it("should call inspector 1 and inspector 2", function () {
+ var codeInspector1 = createCodeInspector("javascript inspector 1", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+ var codeInspector2 = createCodeInspector("javascript inspector 2", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector2);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect(codeInspector1.scanFile).toHaveBeenCalled();
+ expect(codeInspector2.scanFile).toHaveBeenCalled();
+ });
+ });
+
+ it("should keep inspector 1 because the name of inspector 2 is different", function () {
+ var codeInspector1 = createCodeInspector("javascript inspector 1", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector1);
+ var codeInspector2 = createCodeInspector("javascript inspector 2", successfulLintResult());
+ CodeInspection.register("javascript", codeInspector2, true);
+
+ waitsForDone(SpecRunnerUtils.openProjectFiles(["no-errors.js"]), "open test file", 5000);
+
+ runs(function () {
+ expect(codeInspector1.scanFile).toHaveBeenCalled();
+ expect(codeInspector2.scanFile).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});