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}} + + + + {{#results}} - - + + + {{/results}} {{/reportList}}
{{providerName}} ({{results.length}})
{{friendlyLine}}{{message}}{{codeSnippet}}{{message}}{{codeSnippet}}
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(); + }); + }); + }); + }); +});