diff --git a/src/brackets.js b/src/brackets.js index d316640a63c..32ef4fdb79d 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -72,6 +72,10 @@ define(function (require, exports, module) { ExtensionLoader = require("utils/ExtensionLoader"), SidebarView = require("project/SidebarView"), Async = require("utils/Async"); + + // Local variables + var bracketsReady = false, + bracketsReadyHandlers = []; //Load modules that self-register and just need to get included in the main project require("editor/CodeHintManager"); @@ -79,7 +83,36 @@ define(function (require, exports, module) { require("debug/DebugCommandHandlers"); require("view/ViewCommandHandlers"); require("search/FindInFiles"); + require("utils/ExtensionUtils"); + + function _callBracketsReadyHandler(handler) { + try { + handler(); + } catch (e) { + console.log("Exception when calling a 'brackets done loading' handler"); + console.log(e); + } + } + function _onBracketsReady() { + var i; + bracketsReady = true; + for (i = 0; i < bracketsReadyHandlers.length; i++) { + _callBracketsReadyHandler(bracketsReadyHandlers[i]); + } + bracketsReadyHandlers = []; + } + + // WARNING: This event won't fire if ANY extension fails to load or throws an error during init. + // To fix this, we need to make a change to _initExtensions (filed as issue 1029) + function _registerBracketsReadyHandler(handler) { + if (bracketsReady) { + _callBracketsReadyHandler(handler); + } else { + bracketsReadyHandlers.push(handler); + } + } + // TODO: Issue 949 - the following code should be shared function _initGlobalBrackets() { @@ -119,10 +152,22 @@ define(function (require, exports, module) { // Note: we change the name to "getModule" because this won't do exactly the same thing as 'require' in AMD-wrapped // modules. The extension will only be able to load modules that have already been loaded once. brackets.getModule = require; + + // Provide a way for anyone (including code not using require) to register a handler for the brackets 'ready' event + // This event is like $(document).ready in that it will call the handler immediately if brackets is already done loading + // + // WARNING: This event won't fire if ANY extension fails to load or throws an error during init. + // To fix this, we need to make a change to _initExtensions (filed as issue 1029) + // + // TODO (issue 1034): We *could* use a $.Deferred for this, except deferred objects enter a broken + // state if any resolution callback throws an exception. Since third parties (e.g. extensions) may + // add callbacks to this, we need to be robust to exceptions + brackets.ready = _registerBracketsReadyHandler; } + // TODO: (issue 1029) Add timeout to main extension loading promise, so that we always call this function + // Making this fix will fix a warning (search for issue 1029) related to the brackets 'ready' event. function _initExtensions() { - // FUTURE (JRB): As we get more fine-grained performance measurement, move this out of core application startup return Async.doInParallel(["default", "user"], function (item) { return ExtensionLoader.loadAllExtensionsInNativeDirectory( FileUtils.getNativeBracketsDirectoryPath() + "/extensions/" + item, @@ -156,8 +201,13 @@ define(function (require, exports, module) { CSSUtils : require("language/CSSUtils"), LiveDevelopment : require("LiveDevelopment/LiveDevelopment"), Inspector : require("LiveDevelopment/Inspector/Inspector"), - NativeApp : require("utils/NativeApp") + NativeApp : require("utils/NativeApp"), + doneLoading : false }; + + brackets.ready(function () { + brackets.test.doneLoading = true; + }); } function _initDragAndDropListeners() { @@ -261,7 +311,7 @@ define(function (require, exports, module) { // finish UI initialization before loading extensions ProjectManager.loadProject().done(function () { _initTest(); - _initExtensions(); + _initExtensions().always(_onBracketsReady); }); } diff --git a/src/command/CommandManager.js b/src/command/CommandManager.js index 56e1125ecd8..7d788fb240c 100644 --- a/src/command/CommandManager.js +++ b/src/command/CommandManager.js @@ -37,7 +37,14 @@ define(function (require, exports, module) { * @type Object. */ var _commands = {}; - + + /** + * Temporary copy of commands map for restoring after testing + * TODO (issue #1039): implement separate require contexts for unit tests + * @type Object. + */ + var _commandsOriginal = {}; + /** * @constructor * @private @@ -174,11 +181,23 @@ define(function (require, exports, module) { return command; } - - function _reset() { + /** + * Clear all commands for unit testing, but first make copy of commands so that + * they can be restored afterward + */ + function _testReset() { + _commandsOriginal = _commands; _commands = {}; } + /** + * Restore original commands after test and release copy + */ + function _testRestore(commands) { + _commands = _commandsOriginal; + _commandsOriginal = {}; + } + /** * Retrieves a Command object by id * @param {string} id @@ -204,8 +223,9 @@ define(function (require, exports, module) { } // Define public API - exports.register = register; - exports.execute = execute; - exports.get = get; - exports._reset = _reset; -}); \ No newline at end of file + exports.register = register; + exports.execute = execute; + exports.get = get; + exports._testReset = _testReset; + exports._testRestore = _testRestore; +}); diff --git a/src/editor/Editor.js b/src/editor/Editor.js index 336644ceff9..1f6b5980f17 100644 --- a/src/editor/Editor.js +++ b/src/editor/Editor.js @@ -314,24 +314,25 @@ define(function (require, exports, module) { // Editor supplies some standard keyboard behavior extensions of its own var codeMirrorKeyMap = { - "Tab" : _handleTabKey, + "Tab": _handleTabKey, + "Shift-Tab": "indentLess", - "Left" : function (instance) { + "Left": function (instance) { if (!_handleSoftTabNavigation(instance, -1, "moveH")) { CodeMirror.commands.goCharLeft(instance); } }, - "Right" : function (instance) { + "Right": function (instance) { if (!_handleSoftTabNavigation(instance, 1, "moveH")) { CodeMirror.commands.goCharRight(instance); } }, - "Backspace" : function (instance) { + "Backspace": function (instance) { if (!_handleSoftTabNavigation(instance, -1, "deleteH")) { CodeMirror.commands.delCharLeft(instance); } }, - "Delete" : function (instance) { + "Delete": function (instance) { if (!_handleSoftTabNavigation(instance, 1, "deleteH")) { CodeMirror.commands.delCharRight(instance); } diff --git a/src/extensions/disabled/JavaScriptInlineEditor/unittests.js b/src/extensions/disabled/JavaScriptInlineEditor/unittests.js index e48b9cc582c..4951b77c12b 100644 --- a/src/extensions/disabled/JavaScriptInlineEditor/unittests.js +++ b/src/extensions/disabled/JavaScriptInlineEditor/unittests.js @@ -44,7 +44,7 @@ define(function (require, exports, module) { var extensionPath = FileUtils.getNativeModuleDirectoryPath(module); - describe("JSQuickEdit", function () { + describe("JavaScriptInlineEditor", function () { var testPath = extensionPath + "/unittest-files", testWindow, @@ -495,7 +495,6 @@ define(function (require, exports, module) { SpecRunnerUtils.closeTestWindow(); }); -/*** it("should return the correct offsets if the file has changed", function () { var didOpen = false, gotError = false; @@ -531,8 +530,11 @@ define(function (require, exports, module) { FileIndexManager.getFileInfoList("all") .done(function (fileInfos) { + var extensionRequire = brackets.getModule('utils/ExtensionLoader').getRequireContextForExtension('JavaScriptInlineEditor'); + var JSUtilsInExtension = extensionRequire("JSUtils"); + // Look for "edit2" function - JSUtils.findMatchingFunctions("edit2", fileInfos) + JSUtilsInExtension.findMatchingFunctions("edit2", fileInfos) .done(function (result) { functions = result; }); }); }); @@ -545,8 +547,7 @@ define(function (require, exports, module) { expect(functions[0].lineEnd).toBe(13); }); }); -***/ -/*** + it("should return a newly created function in an unsaved file", function () { var didOpen = false, gotError = false; @@ -563,24 +564,31 @@ define(function (require, exports, module) { runs(function () { var doc = DocumentManager.getCurrentDocument(); - // Add a new function to the file doc.setText(doc.getText() + "\n\nfunction TESTFUNCTION() {\n return true;\n}\n"); // Look for the selector we just created - JSUtils.findMatchingFunctions("TESTFUNCTION", FileIndexManager.getFileInfoList("all")) - .done(function (result) { functions = result; }); + FileIndexManager.getFileInfoList("all") + .done(function (fileInfos) { + var extensionRequire = brackets.getModule('utils/ExtensionLoader').getRequireContextForExtension('JavaScriptInlineEditor'); + var JSUtilsInExtension = extensionRequire("JSUtils"); + + // Look for "TESTFUNCTION" function + JSUtilsInExtension.findMatchingFunctions("TESTFUNCTION", fileInfos) + .done(function (result) { + functions = result; + }); + }); }); waitsFor(function () { return functions !== null; }, "JSUtils.findMatchingFunctions() timeout", 1000); runs(function () { expect(functions.length).toBe(1); - expect(functions[0].lineStart).toBe(24); - expect(functions[0].lineEnd).toBe(26); + expect(functions[0].lineStart).toBe(33); + expect(functions[0].lineEnd).toBe(35); }); }); -***/ }); }); //describe("JS Parsing") }); diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index dd39ff0a366..527c9645b2c 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -851,11 +851,7 @@ define(function (require, exports, module) { _projectTree.jstree("create", node, position, {data: initialName}, null, skipRename); if (!skipRename) { - var $renameInput = _projectTree.find(".jstree-rename-input"), - projectTreeOffset = _projectTree.offset(), - projectTreeScroller = _projectTree.get(0), - renameInput = $renameInput.get(0), - renameInputOffset = $renameInput.offset(); + var $renameInput = _projectTree.find(".jstree-rename-input"); $renameInput.on("keydown", function (event) { // Listen for escape key on keydown, so we can remove the node in the create.jstree handler above @@ -863,22 +859,8 @@ define(function (require, exports, module) { escapeKeyPressed = true; } }); - - // make sure edit box is visible within the jstree, only scroll vertically when necessary - if (renameInputOffset.top + $renameInput.height() >= (projectTreeOffset.top + _projectTree.height())) { - // below viewport - renameInput.scrollIntoView(false); - } else if (renameInputOffset.top <= projectTreeOffset.top) { - // above viewport - renameInput.scrollIntoView(true); - } - - // left-align renameInput - if (renameInputOffset.left < 0) { - _projectTree.scrollLeft(_projectTree.scrollLeft() + renameInputOffset.left); - } else if (renameInputOffset.left + $renameInput.width() >= projectTreeOffset.left + _projectTree.width()) { - _projectTree.scrollLeft(renameInputOffset.left - projectTreeOffset.left); - } + + ViewUtils.scrollElementIntoView(_projectTree, $renameInput, true); } return result.promise(); diff --git a/src/project/SidebarView.js b/src/project/SidebarView.js index e4f1a6dba85..40a00692f75 100644 --- a/src/project/SidebarView.js +++ b/src/project/SidebarView.js @@ -23,7 +23,7 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, $, document */ +/*global define, $, document, window */ define(function (require, exports, module) { 'use strict'; @@ -75,7 +75,7 @@ define(function (require, exports, module) { if (typeof displayTriangle === "boolean") { var display = (displayTriangle) ? "block" : "none"; - $sidebar.find(".triangle-visible").css("display", display); + $sidebar.find(".sidebar-selection-triangle").css("display", display); } if (isSidebarClosed) { @@ -89,8 +89,6 @@ define(function (require, exports, module) { // event that we can just call from anywhere instead of hard-coding it. // waiting on a ProjectManager refactor to add that. $sidebar.find(".sidebar-selection").width(width); - $projectFilesContainer.triggerHandler("scroll"); - $openFilesContainer.triggerHandler("scroll"); if (width > 10) { prefs.setValue("sidebarWidth", width); @@ -101,7 +99,6 @@ define(function (require, exports, module) { var text = (isSidebarClosed) ? Strings.CMD_SHOW_SIDEBAR : Strings.CMD_HIDE_SIDEBAR; CommandManager.get(Commands.VIEW_HIDE_SIDEBAR).setName(text); } - EditorManager.resizeEditor(); } @@ -119,7 +116,6 @@ define(function (require, exports, module) { var prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaultPrefs); prefs.setValue("sidebarClosed", isSidebarClosed); - _setWidth(width, true, !isSidebarClosed); } @@ -132,7 +128,9 @@ define(function (require, exports, module) { $body = $(document.body), prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaultPrefs), sidebarWidth = prefs.getValue("sidebarWidth"), - startingSidebarPosition = sidebarWidth; + startingSidebarPosition = sidebarWidth, + animationRequest = null, + isMouseDown = false; $sidebarResizer.css("left", sidebarWidth - 1); @@ -143,59 +141,85 @@ define(function (require, exports, module) { } $sidebarResizer.on("dblclick", function () { - if ($sidebar.width() === 1) { - // mousedown is fired first. Sidebar is already toggeled open to 1px. + if ($sidebar.width() < 10) { + //mousedown is fired first. Sidebar is already toggeled open to at least 10px. _setWidth(null, true, true); + $projectFilesContainer.triggerHandler("scroll"); + $openFilesContainer.triggerHandler("scroll"); } else { - toggleSidebar(); + toggleSidebar(sidebarWidth); } }); $sidebarResizer.on("mousedown.sidebar", function (e) { - var startX = e.clientX; + var startX = e.clientX, + newWidth = Math.max(e.clientX, 0), + doResize = true; + + isMouseDown = true; + + // take away the shadows (for performance reasons during sidebarmovement) + $sidebar.find(".scroller-shadow").css("display", "none"); + $body.toggleClass("resizing"); + // check to see if we're currently in hidden mode if (isSidebarClosed) { toggleSidebar(1); } + - $mainView.on("mousemove.sidebar", function (e) { - var doResize = true, - newWidth = Math.max(e.clientX, 0); - + animationRequest = window.webkitRequestAnimationFrame(function doRedraw() { + // only run this if the mouse is down so we don't constantly loop even + // after we're done resizing. + if (!isMouseDown) { + return; + } + // if we've gone below 10 pixels on a mouse move, and the // sidebar is shrinking, hide the sidebar automatically an // unbind the mouse event. if ((startX > 10) && (newWidth < 10)) { toggleSidebar(startingSidebarPosition); $mainView.off("mousemove.sidebar"); + + // turn off the mouseup event so that it doesn't fire twice and retoggle the + // resizing class + $mainView.off("mouseup.sidebar"); $body.toggleClass("resizing"); doResize = false; - } else if (startX < 10) { - // reset startX if we're going from a snapped closed position to open - startX = startingSidebarPosition; + startX = 0; + + // force isMouseDown so that we don't keep calling requestAnimationFrame + // this keeps the sidebar from stuttering + isMouseDown = false; + } if (doResize) { - // if we've moving past 10 pixels, make the triangle visible again - // and register that the sidebar is no longer snapped closed. - var forceTriangle = null; - - if (newWidth > 10) { - forceTriangle = true; - } - - _setWidth(newWidth, false, forceTriangle); + // for right now, displayTriangle is always going to be false for _setWidth + // because we want to hide it when we move, and _setWidth only gets called + // on mousemove now. + _setWidth(newWidth, false, false); } - if (newWidth === 0) { - $mainView.off("mousemove.sidebar"); - $("body").toggleClass("resizing"); - } - + animationRequest = window.webkitRequestAnimationFrame(doRedraw); + }); + + $mainView.on("mousemove.sidebar", function (e) { + newWidth = Math.max(e.clientX, 0); + e.preventDefault(); }); $mainView.one("mouseup.sidebar", function (e) { + isMouseDown = false; + + // replace shadows and triangle + $sidebar.find(".sidebar-selection-triangle").css("display", "block"); + $sidebar.find(".scroller-shadow").css("display", "block"); + + $projectFilesContainer.triggerHandler("scroll"); + $openFilesContainer.triggerHandler("scroll"); $mainView.off("mousemove.sidebar"); $body.toggleClass("resizing"); startingSidebarPosition = $sidebar.width(); diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index 67ad7d99a81..3907bf4ca92 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -194,7 +194,52 @@ define(function (require, exports, module) { _redraw(); } - + + /** + * Finds the listItem item assocated with the file. Returns null if not found. + * @private + * @param {!FileEntry} file + * @return {HTMLLIItem} + */ + function _findListItemFromFile(file) { + var result = null; + + if (file) { + var items = $openFilesContainer.find("ul").children(); + items.each(function () { + var $listItem = $(this); + if ($listItem.data(_FILE_KEY).fullPath === file.fullPath) { + result = $listItem; + return false; + // breaks each + } + }); + } + + return result; + } + + /** + * @private + */ + function _scrollSelectedDocIntoView() { + if (FileViewController.getFileSelectionFocus() !== FileViewController.WORKING_SET_VIEW) { + return; + } + + var doc = DocumentManager.getCurrentDocument(); + if (!doc) { + return; + } + + var $selectedDoc = _findListItemFromFile(doc.file); + if (!$selectedDoc) { + return; + } + + ViewUtils.scrollElementIntoView($openFilesContainer, $selectedDoc, false); + } + /** * @private */ @@ -210,7 +255,10 @@ define(function (require, exports, module) { var items = $openFilesContainer.find("ul").children().each(function () { _updateListItemSelection(this, doc); }); - + + // Make sure selection is in view + _scrollSelectedDocIntoView(); + _fireSelectionChanged(); } @@ -229,30 +277,6 @@ define(function (require, exports, module) { _updateListSelection(); } - /** - * Finds the listItem item assocated with the file. Returns null if not found. - * @private - * @param {!FileEntry} file - * @return {HTMLLIItem} - */ - function _findListItemFromFile(file) { - var result = null; - - if (file) { - var items = $openFilesContainer.find("ul").children(); - items.each(function () { - var $listItem = $(this); - if ($listItem.data(_FILE_KEY).fullPath === file.fullPath) { - result = $listItem; - return false; - // breaks each - } - }); - } - - return result; - } - /** * @private * @param {FileEntry} file diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index d2ebaf0f818..f63809fd788 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -264,7 +264,7 @@ define(function (require, exports, module) { // Query is a string. Turn it into a case-insensitive regexp // Escape regex special chars - query = query.replace(/(\(|\)|\{|\}|\[|\]|\.|\^|\$|\||\?|\+|\*)/g, "\\$1"); + query = query.replace(/([(){}\[\].\^$|?+*\\])/g, "\\$1"); return new RegExp(query, "gi"); } diff --git a/src/thirdparty/CodeMirror2 b/src/thirdparty/CodeMirror2 index c12c3d09749..5c2f30c606f 160000 --- a/src/thirdparty/CodeMirror2 +++ b/src/thirdparty/CodeMirror2 @@ -1 +1 @@ -Subproject commit c12c3d097493c065911a3eb95e05f38e35a17ffe +Subproject commit 5c2f30c606fc40a5271992b94141d79ce49c9309 diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 64041f70c27..b80ac424be6 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -34,7 +34,19 @@ define(function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, FileUtils = require("file/FileUtils"), - Async = require("utils/Async"); + Async = require("utils/Async"), + contexts = {}; + /** + * Returns the require.js require context used to load an extension + * + * @param {!string} name, used to identify the extension + * @return {!Object} A require.js require object used to load the extension, or undefined if + * there is no require object ith that name + */ + function getRequireContextForExtension(name) { + return contexts[name]; + } + /** * Loads the extension that lives at baseUrl into its own Require.js context @@ -44,17 +56,18 @@ define(function (require, exports, module) { * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when the extension is loaded. */ - function loadExtension(name, baseUrl, entryPoint) { + function loadExtension(name, config, entryPoint) { var result = new $.Deferred(), extensionRequire = brackets.libRequire.config({ context: name, - baseUrl: baseUrl + baseUrl: config.baseUrl }); + contexts[name] = extensionRequire; - console.log("[Extension] starting to load " + baseUrl); + console.log("[Extension] starting to load " + config.baseUrl); extensionRequire([entryPoint], function () { - console.log("[Extension] finished loading " + baseUrl); + console.log("[Extension] finished loading " + config.baseUrl); result.resolve(); }); @@ -69,13 +82,13 @@ define(function (require, exports, module) { * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ - function testExtension(name, baseUrl, entryPoint) { + function testExtension(name, config, entryPoint) { var result = new $.Deferred(), extensionPath = FileUtils.getNativeBracketsDirectoryPath(); // Assumes the caller's window.location context is /test/SpecRunner.html extensionPath = extensionPath.replace("brackets/test", "brackets/src"); // convert from "test" to "src" - extensionPath += "/" + baseUrl + "/" + entryPoint + ".js"; + extensionPath += "/" + config.baseUrl + "/" + entryPoint + ".js"; var fileExists = false, statComplete = false; brackets.fs.stat(extensionPath, function (err, stat) { @@ -84,12 +97,13 @@ define(function (require, exports, module) { // unit test file exists var extensionRequire = brackets.libRequire.config({ context: name, - baseUrl: "../src/" + baseUrl + baseUrl: "../src/" + config.baseUrl, + paths: config.paths }); - console.log("[Extension] loading unit test " + baseUrl); + console.log("[Extension] loading unit test " + config.baseUrl); extensionRequire([entryPoint], function () { - console.log("[Extension] loaded unit tests " + baseUrl); + console.log("[Extension] loaded unit tests " + config.baseUrl); result.resolve(); }); } else { @@ -111,7 +125,7 @@ define(function (require, exports, module) { * @param {function} processExtension * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ - function _loadAll(directory, baseUrl, entryPoint, processExtension) { + function _loadAll(directory, config, entryPoint, processExtension) { var result = new $.Deferred(); NativeFileSystem.requestNativeFileSystem(directory, @@ -130,7 +144,11 @@ define(function (require, exports, module) { } Async.doInParallel(extensions, function (item) { - return processExtension(item, baseUrl + "/" + item, entryPoint); + var extConfig = { + baseUrl: config.baseUrl + "/" + item, + paths: config.paths + }; + return processExtension(item, extConfig, entryPoint); }).done(function () { result.resolve(); }).fail(function () { @@ -158,7 +176,7 @@ define(function (require, exports, module) { * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function loadAllExtensionsInNativeDirectory(directory, baseUrl) { - return _loadAll(directory, baseUrl, "main", loadExtension); + return _loadAll(directory, {baseUrl: baseUrl}, "main", loadExtension); } /** @@ -170,9 +188,20 @@ define(function (require, exports, module) { * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function testAllExtensionsInNativeDirectory(directory, baseUrl) { - return _loadAll(directory, baseUrl, "unittests", testExtension); + var bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), + config = { + baseUrl: baseUrl + }; + + config.paths = { + "perf": bracketsPath + "/perf", + "spec": bracketsPath + "/spec" + }; + + return _loadAll(directory, config, "unittests", testExtension); } + exports.getRequireContextForExtension = getRequireContextForExtension; exports.loadExtension = loadExtension; exports.testExtension = testExtension; exports.loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory; diff --git a/src/utils/ExtensionUtils.js b/src/utils/ExtensionUtils.js new file mode 100644 index 00000000000..063818ece5e --- /dev/null +++ b/src/utils/ExtensionUtils.js @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012 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, nomen: true, indent: 4, maxerr: 50 */ +/*global define, $ */ + +/** + * ExtensionUtils defines utility methods for implementing extensions. + */ +define(function (require, exports, module) { + 'use strict'; + + /** + * Loads a style sheet relative to the extension module. + * + * @param {!module} module Module provided by RequireJS + * @param {!string} path Relative path from the extension folder to a CSS file + * @return {!$.Promise} A promise object that is resolved if the CSS file can be loaded. + */ + function loadStyleSheet(module, path) { + var url = encodeURI(module.uri.replace("main.js", "") + path), + result = new $.Deferred(); + + // Make a request for the same file in order to record success or failure. + // The link element's onload and onerror events are not consistently supported. + $.get(url).done(function () { + var $link = $(""); + + $link.attr({ + type: "text/css", + rel: "stylesheet", + href: url + }); + + $("head").append($link[0]); + + result.resolve(); + }).fail(function (err) { + result.reject(err); + }); + + return result; + } + + exports.loadStyleSheet = loadStyleSheet; +}); diff --git a/src/utils/StringUtils.js b/src/utils/StringUtils.js index 25192f1f576..6b54d9d06c3 100644 --- a/src/utils/StringUtils.js +++ b/src/utils/StringUtils.js @@ -25,7 +25,7 @@ /*global define, $ */ /** - * Utilities functions related to string manipulation + * Utilities functions related to string manipulation * */ define(function (require, exports, module) { diff --git a/src/utils/ViewUtils.js b/src/utils/ViewUtils.js index 8b95861b059..bb8c1611cf6 100644 --- a/src/utils/ViewUtils.js +++ b/src/utils/ViewUtils.js @@ -277,13 +277,55 @@ define(function (require, exports, module) { f.apply(); }); } + + /** + * Within a scrolling DOMElement, if necessary, scroll element into viewport. + * + * To Perform the minimum amount of scrolling necessary, cases should be handled as follows: + * - element already completely in view : no scrolling + * - element above viewport : scroll view so element is at top + * - element left of viewport : scroll view so element is at left + * - element below viewport : scroll view so element is at bottom + * - element right of viewport : scroll view so element is at right + * + * Assumptions: + * - $view is a scrolling container + * + * @param {!DOMElement} $view - A jQuery scrolling container + * @param {!DOMElement} $element - A jQuery element + * @param {?boolean} scrollHorizontal - whether to also scroll horizonally + */ + function scrollElementIntoView($view, $element, scrollHorizontal) { + var viewOffset = $view.offset(), + viewScroller = $view.get(0), + element = $element.get(0), + elementOffset = $element.offset(); + + // scroll minimum amount + if (elementOffset.top + $element.height() >= (viewOffset.top + $view.height())) { + // below viewport + element.scrollIntoView(false); + } else if (elementOffset.top <= viewOffset.top) { + // above viewport + element.scrollIntoView(true); + } + + if (scrollHorizontal) { + if (elementOffset.left < 0) { + $view.scrollLeft($view.scrollLeft() + elementOffset.left); + } else if (elementOffset.left + $element.width() >= viewOffset.left + $view.width()) { + $view.scrollLeft(elementOffset.left - viewOffset.left); + } + } + } // handle all resize handlers in a single listener $(window).resize(_handleResize); // Define public API - exports.SCROLL_SHADOW_HEIGHT = SCROLL_SHADOW_HEIGHT; - exports.addScrollerShadow = addScrollerShadow; - exports.removeScrollerShadow = removeScrollerShadow; - exports.sidebarList = sidebarList; + exports.SCROLL_SHADOW_HEIGHT = SCROLL_SHADOW_HEIGHT; + exports.addScrollerShadow = addScrollerShadow; + exports.removeScrollerShadow = removeScrollerShadow; + exports.sidebarList = sidebarList; + exports.scrollElementIntoView = scrollElementIntoView; }); diff --git a/test/BootstrapReporter.js b/test/BootstrapReporter.js index 53fccbb1918..c5ebefd74f8 100644 --- a/test/BootstrapReporter.js +++ b/test/BootstrapReporter.js @@ -3,7 +3,7 @@ (function ($) { 'use strict'; - jasmine.BootstrapReporter = function (doc) { + jasmine.BootstrapReporter = function (doc, filter) { this._paramMap = {}; this.document = doc || document; this._env = jasmine.getEnv(); @@ -20,6 +20,12 @@ } this._runAll = this._paramMap.spec === "All"; + + // _topLevelFilter is applied first before the spec filter + this._topLevelFilter = filter; + + // Jasmine's runner uses the specFilter to choose which tests to run. + // This is typically a subset of all tests loaded. this._env.specFilter = this.createSpecFilter(this._paramMap.spec); this._runner = this._env.currentRunner(); @@ -44,12 +50,31 @@ this.$resultsContainer = $("#results-container"); }; + /** + * @private + * Filters specs by full name. Applies _topLevelFilter first before checking + * for a matching starting substring. + */ jasmine.BootstrapReporter.prototype.createSpecFilter = function (filterString) { + var self = this; + return function (spec) { + // filterString is undefined when no top-level suite is active (e.g. "All", "HTMLUtils", etc.) + // When undefined, all specs fail this filter and no tests are ran. This is by design. + // This setup allows the SpecRunner to load initially without automatically running all tests. + if (filterString === undefined) { + return false; + } + + if (!self._topLevelFilter(spec)) { + return false; + } + if (filterString === "All") { return true; } - return spec.getFullName().indexOf(filterString) === 0; + + return (spec.getFullName().indexOf(filterString) === 0); }; }; @@ -82,7 +107,11 @@ self = this; // count specs attached directly to this suite - count = suite.specs().length; + suite.specs().forEach(function (spec, index) { + if (self._topLevelFilter(spec)) { + count++; + } + }); // recursively count child suites suite.suites().forEach(function (child, index) { @@ -110,11 +139,22 @@ return 0; }); - this.$suiteList.append(this._createSuiteListItem(null, this._runner.specs().length)); - topLevel.forEach(function (suite, index) { - self.$suiteList.append(self._createSuiteListItem(suite, self._countSpecs(suite))); + var count = self._countSpecs(suite); + + if (count > 0) { + self.$suiteList.append(self._createSuiteListItem(suite, count)); + } + }); + + // count all speces + var allSpecsCount = 0; + $.each(this._topLevelSuiteMap, function (index, value) { + allSpecsCount += value.specCount; }); + + // add an "all" top-level suite + this.$suiteList.prepend(this._createSuiteListItem(null, allSpecsCount)); }; jasmine.BootstrapReporter.prototype._showProgressBar = function (spec) { @@ -127,9 +167,9 @@ }; jasmine.BootstrapReporter.prototype.reportRunnerStarting = function (runner) { - var i, - specs = runner.specs(), - topLevelData; + var specs = runner.specs(), + topLevelData, + self = this; // create top level suite list navigation this._createSuiteList(); @@ -144,11 +184,11 @@ this._specCount = 0; this._specCompleteCount = 0; - for (i = 0; i < specs.length; i++) { - if (this._env.specFilter(specs[i])) { - this._specCount++; + specs.forEach(function (spec, index) { + if (self._env.specFilter(spec)) { + self._specCount++; } - } + }); if (this._specCount) { this._showProgressBar(); @@ -230,13 +270,22 @@ jasmine.BootstrapReporter.prototype._updateStatus = function (spec) { var data = this._getTopLevelSuiteData(spec), - allData = this._topLevelSuiteMap.All, - results = spec.results(); + allData, + results; + + // Top-level suite data will not exist if filtered + if (!data) { + return; + } + + allData = this._topLevelSuiteMap.All; + results = spec.results(); this._updateSuiteStatus(data, results); this._updateSuiteStatus(allData, results); }; + // Jasmine calls this function for all specs, not just filtered specs. jasmine.BootstrapReporter.prototype.reportSpecResults = function (spec) { var results = spec.results(), $specLink, diff --git a/test/PerformanceTestSuite.js b/test/PerformanceTestSuite.js new file mode 100644 index 00000000000..addeefd4c52 --- /dev/null +++ b/test/PerformanceTestSuite.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012 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 */ +define(function (require, exports, module) { + 'use strict'; + + // Each suite or spec must have this.performance = true to be filtered properly + require("perf/Performance-test"); +}); \ No newline at end of file diff --git a/test/PerformanceTestSuite.json b/test/PerformanceTestSuite.json deleted file mode 100644 index 156cfa98849..00000000000 --- a/test/PerformanceTestSuite.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "specs": ["perf/Performance-test.js"] -} \ No newline at end of file diff --git a/test/SpecRunner.js b/test/SpecRunner.js index 0c0ffcedba4..53423ef21c3 100644 --- a/test/SpecRunner.js +++ b/test/SpecRunner.js @@ -26,8 +26,12 @@ // Set the baseUrl to brackets/src require.config({ - baseUrl: "../src"/*, - urlArgs: "bust=" + (new Date()).getTime() // cache busting */ + baseUrl: "../src", + paths: { + "test": "../test", + "perf": "../test/perf", + "spec": "../test/spec" + } }); define(function (require, exports, module) { @@ -38,7 +42,11 @@ define(function (require, exports, module) { ExtensionLoader = require("utils/ExtensionLoader"), FileUtils = require("file/FileUtils"), Menus = require("command/Menus"), - PerformanceReporter = require("perf/PerformanceReporter.js").PerformanceReporter; + PerformanceReporter = require("perf/PerformanceReporter").PerformanceReporter; + + // Load both top-level suites. Filtering is applied at the top-level as a filter to BootstrapReporter. + require("test/UnitTestSuite"); + require("test/PerformanceTestSuite"); var suite; @@ -119,8 +127,6 @@ define(function (require, exports, module) { currentWindowOnload(); } - jasmineEnv.addReporter(new jasmine.BootstrapReporter(document)); - $("#show-dev-tools").click(function () { brackets.app.showDeveloperTools(); }); @@ -130,8 +136,30 @@ define(function (require, exports, module) { suite = getParamMap().suite || localStorage.getItem("SpecRunner.suite") || "UnitTestSuite"; + // Create a top-level filter to show/hide performance tests + var isPerfSuite = (suite === "PerformanceTestSuite"), + performanceFilter = function (spec) { + if (spec.performance === true) { + return isPerfSuite; + } + + var suite = spec.suite; + + while (suite) { + if (suite.performance === true) { + return isPerfSuite; + } + + suite = suite.parentSuite; + } + + return !isPerfSuite; + }; + + jasmineEnv.addReporter(new jasmine.BootstrapReporter(document, performanceFilter)); + // add performance reporting - if (suite === "PerformanceTestSuite") { + if (isPerfSuite) { jasmineEnv.addReporter(new PerformanceReporter()); } @@ -139,14 +167,7 @@ define(function (require, exports, module) { $("#" + suite).closest("li").toggleClass("active", true); - var jsonResult = $.getJSON(suite + ".json"); - - jsonResult.done(function (data) { - // load specs and run jasmine - require(data.specs, function () { - jasmineEnv.execute(); - }); - }); + jasmineEnv.execute(); }; } diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js new file mode 100644 index 00000000000..cb460651e66 --- /dev/null +++ b/test/UnitTestSuite.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012 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 */ +define(function (require, exports, module) { + 'use strict'; + + require("spec/CodeHintUtils-test"); + require("spec/CommandManager-test"); + require("spec/CSSUtils-test"); + require("spec/DocumentCommandHandlers-test"); + require("spec/Editor-test"); + require("spec/EditorCommandHandlers-test"); + require("spec/FileIndexManager-test"); + require("spec/InlineEditorProviders-test"); + require("spec/KeyBindingManager-test"); + require("spec/LiveDevelopment-test"); + require("spec/LowLevelFileIO-test"); + require("spec/Menu-test"); + require("spec/MultiRangeInlineEditor-test"); + require("spec/NativeFileSystem-test"); + require("spec/PreferencesManager-test"); + require("spec/ProjectManager-test"); + require("spec/ViewUtils-test"); + require("spec/WorkingSetView-test"); +}); \ No newline at end of file diff --git a/test/UnitTestSuite.json b/test/UnitTestSuite.json deleted file mode 100644 index f9a168b17aa..00000000000 --- a/test/UnitTestSuite.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "specs": ["spec/LowLevelFileIO-test.js", - "spec/DocumentCommandHandlers-test.js", - "spec/NativeFileSystem-test.js", - "spec/PreferencesManager-test.js", - "spec/Editor-test.js", - "spec/EditorCommandHandlers-test.js", - "spec/ProjectManager-test.js", - "spec/WorkingSetView-test.js", - "spec/KeyBindingManager-test.js", - "spec/FileIndexManager-test.js", - "spec/CodeHintUtils-test.js", - "spec/CSSUtils-test.js", - "spec/InlineEditorProviders-test.js", - "spec/MultiRangeInlineEditor-test.js", - "spec/LiveDevelopment-test.js", - "spec/CommandManager-test.js", - "spec/ViewUtils-test.js", - "spec/Menu-test.js"] -} diff --git a/test/perf/Performance-test.js b/test/perf/Performance-test.js index 204f62b13e7..496359147c8 100644 --- a/test/perf/Performance-test.js +++ b/test/perf/Performance-test.js @@ -36,13 +36,15 @@ define(function (require, exports, module) { PerfUtils, // loaded from brackets.test JSLintUtils, // loaded from brackets.test DocumentManager, // loaded from brackets.test - SpecRunnerUtils = require("../spec/SpecRunnerUtils.js"), - PerformanceReporter = require("../perf/PerformanceReporter.js"); + SpecRunnerUtils = require("spec/SpecRunnerUtils"), + PerformanceReporter = require("perf/PerformanceReporter"); var jsLintPrevSetting; describe("Performance Tests", function () { + this.performance = true; + // Note: this tests assumes that the "brackets-scenario" repo is in the same folder // as the "brackets-app" // diff --git a/test/perf/PerformanceReporter.js b/test/perf/PerformanceReporter.js index eda681c6df7..d1861095908 100644 --- a/test/perf/PerformanceReporter.js +++ b/test/perf/PerformanceReporter.js @@ -27,7 +27,7 @@ define(function (require, exports, module) { 'use strict'; - var SpecRunnerUtils = require("spec/SpecRunnerUtils.js"); + var SpecRunnerUtils = require("spec/SpecRunnerUtils"); var records = {}; diff --git a/test/spec/CommandManager-test.js b/test/spec/CommandManager-test.js index 4ae6c6a00ca..fb9354b43d4 100644 --- a/test/spec/CommandManager-test.js +++ b/test/spec/CommandManager-test.js @@ -40,10 +40,11 @@ define(function (require, exports, module) { beforeEach(function () { executed = false; - CommandManager._reset(); + CommandManager._testReset(); }); afterEach(function () { + CommandManager._testRestore(); }); it("register and get a command and validate parameters", function () { diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index 179845f7e5c..874adb3bb65 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -107,10 +107,10 @@ define(function (require, exports, module) { // FIXME (issue #249): Need an event or something a little more reliable... waitsFor( - function () { - return testWindow.brackets && testWindow.brackets.test; + function isBracketsDoneLoading() { + return testWindow.brackets && testWindow.brackets.test && testWindow.brackets.test.doneLoading; }, - 5000 + 10000 ); runs(function () {