Skip to content
Browse files

Merging

  • Loading branch information...
2 parents 9e20a8c + 2ad7c7b commit ec9753c8d0702a111372796f9e5947d042182cd1 @TomMalbran TomMalbran committed
Showing with 9,153 additions and 163 deletions.
  1. +0 −3 .gitmodules
  2. +5 −4 Gruntfile.js
  3. +1 −0 package.json
  4. +26 −23 src/LiveDevelopment/Inspector/Inspector.js
  5. +196 −57 src/LiveDevelopment/LiveDevelopment.js
  6. +5 −13 src/LiveDevelopment/launch.html
  7. +12 −16 src/LiveDevelopment/main.js
  8. +4 −3 src/brackets.js
  9. +1 −0 src/command/Commands.js
  10. +2 −0 src/command/DefaultMenus.js
  11. +1 −0 src/config.json
  12. +1 −6 src/document/DocumentManager.js
  13. +1 −1 src/editor/CodeHintList.js
  14. +1 −1 src/editor/CodeHintManager.js
  15. +3 −4 src/editor/Editor.js
  16. +86 −30 src/editor/EditorCommandHandlers.js
  17. +2 −2 src/editor/InlineTextEditor.js
  18. +353 −0 src/extensibility/InstallExtensionDialog.js
  19. +422 −0 src/extensibility/Package.js
  20. +18 −0 src/extensibility/install-extension-dialog.html
  21. +606 −0 src/extensibility/node/ExtensionManagerDomain.js
  22. +1 −0 src/extensibility/node/node_modules/.bin/semver
  23. +19 −0 src/extensibility/node/node_modules/async/LICENSE
  24. +1,365 −0 src/extensibility/node/node_modules/async/README.md
  25. +982 −0 src/extensibility/node/node_modules/async/lib/async.js
  26. +46 −0 src/extensibility/node/node_modules/async/package.json
  27. +1 −0 src/extensibility/node/node_modules/fs-extra/.npmignore
  28. +5 −0 src/extensibility/node/node_modules/fs-extra/.travis.yml
  29. +67 −0 src/extensibility/node/node_modules/fs-extra/CHANGELOG.md
  30. +16 −0 src/extensibility/node/node_modules/fs-extra/LICENSE
  31. +281 −0 src/extensibility/node/node_modules/fs-extra/README.md
  32. +42 −0 src/extensibility/node/node_modules/fs-extra/lib/copy.js
  33. +53 −0 src/extensibility/node/node_modules/fs-extra/lib/create.js
  34. +102 −0 src/extensibility/node/node_modules/fs-extra/lib/index.js
  35. +6 −0 src/extensibility/node/node_modules/fs-extra/lib/mkdir.js
  36. +35 −0 src/extensibility/node/node_modules/fs-extra/lib/output.js
  37. +17 −0 src/extensibility/node/node_modules/fs-extra/lib/remove.js
  38. +1 −0 src/extensibility/node/node_modules/fs-extra/node_modules/.bin/ncp
  39. +1 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/.npmignore
  40. +4 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/.travis.yml
  41. +3 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/CHANGELOG.md
  42. +15 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/LICENSE
  43. +116 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/README.md
  44. +44 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/lib/jsonfile.js
  45. +41 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/package.json
  46. +66 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/test/jsonfile.test.js
  47. +3 −0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/test/mocha.opts
  48. 0 src/extensibility/node/node_modules/fs-extra/node_modules/jsonfile/test/resources/.gitkeep
  49. +2 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/.npmignore
  50. +5 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/.travis.yml
  51. +21 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/LICENSE
  52. +6 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/examples/pow.js
  53. +82 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/index.js
  54. +30 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/package.json
  55. +63 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/readme.markdown
  56. +38 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/chmod.js
  57. +37 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/clobber.js
  58. +28 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/mkdirp.js
  59. +32 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/perm.js
  60. +39 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/perm_sync.js
  61. +41 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/race.js
  62. +32 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/rel.js
  63. +25 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/return.js
  64. +24 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/return_sync.js
  65. +18 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/root.js
  66. +32 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/sync.js
  67. +28 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/umask.js
  68. +32 −0 src/extensibility/node/node_modules/fs-extra/node_modules/mkdirp/test/umask_sync.js
  69. +4 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/.npmignore
  70. +6 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/.travis.yml
  71. +21 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/LICENSE.md
  72. +46 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/README.md
  73. +48 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/bin/ncp
  74. 0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/hahaha
  75. +3 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/laull
  76. +210 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/lib/ncp.js
  77. +19 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/mode.js
  78. +41 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/package.json
  79. +1 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/a
  80. +1 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/b
  81. 0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/c
  82. 0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/d
  83. 0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/e
  84. 0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/f
  85. +1 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/sub/a
  86. 0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/fixtures/src/sub/b
  87. +74 −0 src/extensibility/node/node_modules/fs-extra/node_modules/ncp/test/ncp-test.js
  88. +6 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/AUTHORS
  89. +23 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/LICENSE
  90. +21 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/README.md
  91. +1 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/node_modules/graceful-fs/.npmignore
  92. +23 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/node_modules/graceful-fs/LICENSE
  93. +5 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/node_modules/graceful-fs/README.md
  94. +316 −0 ...ensibility/node/node_modules/fs-extra/node_modules/rimraf/node_modules/graceful-fs/graceful-fs.js
  95. +36 −0 ...xtensibility/node/node_modules/fs-extra/node_modules/rimraf/node_modules/graceful-fs/package.json
  96. +46 −0 ...xtensibility/node/node_modules/fs-extra/node_modules/rimraf/node_modules/graceful-fs/test/open.js
  97. +55 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/package.json
  98. +132 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/rimraf.js
  99. +10 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/test/run.sh
  100. +47 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/test/setup.sh
  101. +5 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/test/test-async.js
  102. +3 −0 src/extensibility/node/node_modules/fs-extra/node_modules/rimraf/test/test-sync.js
  103. +67 −0 src/extensibility/node/node_modules/fs-extra/package.json
  104. +85 −0 src/extensibility/node/node_modules/fs-extra/test/copy.test.js
  105. +60 −0 src/extensibility/node/node_modules/fs-extra/test/create.test.js
  106. +66 −0 src/extensibility/node/node_modules/fs-extra/test/mkdir.test.js
  107. +4 −0 src/extensibility/node/node_modules/fs-extra/test/mocha.opts
  108. +62 −0 src/extensibility/node/node_modules/fs-extra/test/output.test.js
  109. +68 −0 src/extensibility/node/node_modules/fs-extra/test/read.test.js
  110. +127 −0 src/extensibility/node/node_modules/fs-extra/test/remove.test.js
  111. +55 −0 src/extensibility/node/node_modules/request/LICENSE
  112. +342 −0 src/extensibility/node/node_modules/request/README.md
  113. +1,320 −0 src/extensibility/node/node_modules/request/index.js
Sorry, we could not display the entire diff because too many files (654) changed.
View
3 .gitmodules
@@ -10,6 +10,3 @@
[submodule "src/thirdparty/mustache"]
path = src/thirdparty/mustache
url = https://github.com/janl/mustache.js.git
-[submodule "src/extensions/default/jslint/thirdparty/jslint"]
- path = src/extensions/default/jslint/thirdparty/jslint
- url = https://github.com/douglascrockford/JSLint.git
View
9 Gruntfile.js
@@ -35,7 +35,7 @@ module.exports = function (grunt) {
'!src/extensions/**/thirdparty/**/*.js',
'!src/extensions/dev/**',
'!src/extensions/disabled/**',
- '!src/extensions/**/node_modules/**/*.js',
+ '!**/node_modules/**/*.js',
'!src/**/*-min.js',
'!src/**/*.min.js'
],
@@ -50,8 +50,8 @@ module.exports = function (grunt) {
/* specs that can run in phantom.js */
specs : [
'test/spec/CommandManager-test.js',
- 'test/spec/LanguageManager-test.js',
- 'test/spec/PreferencesManager-test.js',
+ //'test/spec/LanguageManager-test.js',
+ //'test/spec/PreferencesManager-test.js',
'test/spec/ViewUtils-test.js'
]
},
@@ -77,7 +77,8 @@ module.exports = function (grunt) {
'src/thirdparty/CodeMirror2/lib/util/dialog.js',
'src/thirdparty/CodeMirror2/lib/util/searchcursor.js',
'src/thirdparty/mustache/mustache.js',
- 'src/thirdparty/path-utils/path-utils.min'
+ 'src/thirdparty/path-utils/path-utils.min',
+ 'src/thirdparty/less-1.3.0.min.js'
],
helpers : [
'test/spec/PhantomHelper.js'
View
1 package.json
@@ -1,6 +1,7 @@
{
"name": "Brackets",
"version": "0.22.0-0",
+ "apiVersion": "0.22.0",
"homepage": "http://brackets.io",
"issues": {
"url": "http://github.com/adobe/brackets/issues"
View
49 src/LiveDevelopment/Inspector/Inspector.js
@@ -151,11 +151,19 @@ define(function Inspector(require, exports, module) {
/** WebSocket reported an error */
function _onError(error) {
+ if (_connectDeferred) {
+ _connectDeferred.reject();
+ _connectDeferred = null;
+ }
$exports.triggerHandler("error", [error]);
}
/** WebSocket did open */
function _onConnect() {
+ if (_connectDeferred) {
+ _connectDeferred.resolve();
+ _connectDeferred = null;
+ }
$exports.triggerHandler("connect");
}
@@ -186,11 +194,11 @@ define(function Inspector(require, exports, module) {
/** Public Functions *****************************************************/
- /** Get the available debugger sockets from the remote debugger
+ /** Get a list of the available windows/tabs/extensions that are remote-debuggable
* @param {string} host IP or name
* @param {integer} debugger port
*/
- function getAvailableSockets(host, port) {
+ function getDebuggableWindows(host, port) {
if (!host) {
host = "127.0.0.1";
}
@@ -264,17 +272,14 @@ define(function Inspector(require, exports, module) {
}
var deferred = new $.Deferred();
_connectDeferred = deferred;
- var promise = getAvailableSockets();
+ var promise = getDebuggableWindows();
promise.done(function onGetAvailableSockets(response) {
- if (deferred.isRejected()) {
- return;
- }
var i, page;
for (i in response) {
page = response[i];
if (page.webSocketDebuggerUrl && page.url.indexOf(url) === 0) {
connect(page.webSocketDebuggerUrl);
- deferred.resolve();
+ // _connectDeferred may be resolved by onConnect or rejected by onError
return;
}
}
@@ -288,7 +293,7 @@ define(function Inspector(require, exports, module) {
/** Check if the inspector is connected */
function connected() {
- return _socket !== undefined;
+ return _socket !== undefined && _socket.readyState === WebSocket.OPEN;
}
/** Initialize the Inspector
@@ -297,25 +302,23 @@ define(function Inspector(require, exports, module) {
*/
function init(theConfig) {
exports.config = theConfig;
- var request = new XMLHttpRequest();
- request.open("GET", "LiveDevelopment/Inspector/Inspector.json");
- request.onload = function onLoad() {
- var InspectorJSON = JSON.parse(request.response);
- var i, j, domain, domainDef, command;
- for (i in InspectorJSON.domains) {
- domain = InspectorJSON.domains[i];
- exports[domain.domain] = {};
- for (j in domain.commands) {
- command = domain.commands[j];
- exports[domain.domain][command.name] = _send.bind(undefined, domain.domain + "." + command.name, command.parameters);
- }
+
+ var InspectorText = require("text!LiveDevelopment/Inspector/Inspector.json"),
+ InspectorJSON = JSON.parse(InspectorText);
+
+ var i, j, domain, domainDef, command;
+ for (i in InspectorJSON.domains) {
+ domain = InspectorJSON.domains[i];
+ exports[domain.domain] = {};
+ for (j in domain.commands) {
+ command = domain.commands[j];
+ exports[domain.domain][command.name] = _send.bind(undefined, domain.domain + "." + command.name, command.parameters);
}
- };
- request.send(null);
+ }
}
// Export public functions
- exports.getAvailableSockets = getAvailableSockets;
+ exports.getDebuggableWindows = getDebuggableWindows;
exports.on = on;
exports.off = off;
exports.disconnect = disconnect;
View
253 src/LiveDevelopment/LiveDevelopment.js
@@ -64,7 +64,8 @@ define(function LiveDevelopment(require, exports, module) {
var STATUS_ACTIVE = exports.STATUS_ACTIVE = 3;
var STATUS_OUT_OF_SYNC = exports.STATUS_OUT_OF_SYNC = 4;
- var Dialogs = require("widgets/Dialogs"),
+ var Async = require("utils/Async"),
+ Dialogs = require("widgets/Dialogs"),
DocumentManager = require("document/DocumentManager"),
EditorManager = require("editor/EditorManager"),
FileUtils = require("file/FileUtils"),
@@ -302,18 +303,19 @@ define(function LiveDevelopment(require, exports, module) {
/** Open a live document
* @param {Document} source document to open
+ * @return {jQuery.Promise} A promise that is resolved once the live
+ * document is open, and is never explicitly rejected.
*/
function _openDocument(doc, editor) {
- _closeDocument();
- _liveDocument = _createDocument(doc, editor);
-
- // Gather related CSS documents.
- // FUTURE: Gather related JS documents as well.
- _relatedDocuments = [];
- agents.css.getStylesheetURLs().forEach(function (url) {
- // FUTURE: when we get truly async file handling, we might need to prevent other
- // stuff from happening while we wait to add these listeners
+
+ function createLiveStylesheet(url) {
+ var stylesheetDeferred = $.Deferred();
+
DocumentManager.getDocumentForPath(_urlToPath(url))
+ .fail(function () {
+ // A failure to open a related file is benign
+ stylesheetDeferred.resolve();
+ })
.done(function (doc) {
if (!_liveDocument || (doc !== _liveDocument.doc)) {
_setDocInfo(doc);
@@ -323,8 +325,21 @@ define(function LiveDevelopment(require, exports, module) {
$(liveDoc).on("deleted", _handleRelatedDocumentDeleted);
}
}
+ stylesheetDeferred.resolve();
});
- });
+ return stylesheetDeferred.promise();
+ }
+
+ _closeDocument();
+ _liveDocument = _createDocument(doc, editor);
+
+ // Gather related CSS documents.
+ // FUTURE: Gather related JS documents as well.
+ _relatedDocuments = [];
+
+ return Async.doInParallel(agents.css.getStylesheetURLs(),
+ createLiveStylesheet,
+ false); // don't fail fast
}
/** Unload the agents */
@@ -423,11 +438,16 @@ define(function LiveDevelopment(require, exports, module) {
var editor = EditorManager.getCurrentFullEditor(),
status = STATUS_ACTIVE;
- _openDocument(doc, editor);
- if (doc.isDirty && _classForDocument(doc) !== CSSDocument) {
- status = STATUS_OUT_OF_SYNC;
- }
- _setStatus(status);
+ // Note: the following promise is never explicitly rejected, so there
+ // is no failure handler. If _openDocument is changed so that rejection
+ // is possible, failure should be managed accordingly.
+ _openDocument(doc, editor)
+ .done(function () {
+ if (doc.isDirty && _classForDocument(doc) !== CSSDocument) {
+ status = STATUS_OUT_OF_SYNC;
+ }
+ _setStatus(status);
+ });
}
/** Triggered by Inspector.detached */
@@ -437,27 +457,44 @@ define(function LiveDevelopment(require, exports, module) {
// However, the link refers to the Chrome Extension API, it may not apply 100% to the Inspector API
}
- /** Triggered by Inspector.connect */
- function _onConnect(event) {
- $(Inspector.Inspector).on("detached", _onDetached);
-
- // Load agents
- _setStatus(STATUS_LOADING_AGENTS);
- var promises = loadAgents();
- $.when.apply(undefined, promises).done(_onLoad).fail(_onError);
-
- // Load the right document (some agents are waiting for the page's load event)
- var doc = _getCurrentDocument();
- if (doc) {
- Inspector.Page.navigate(doc.root.url);
- } else {
- Inspector.Page.reload();
+ // WebInspector Event: Page.frameNavigated
+ function _onFrameNavigated(event, res) {
+ // res = {frame}
+ var url = res.frame.url,
+ baseUrl,
+ baseUrlRegExp;
+
+ // Only check domain of root frame (with undefined parentId)
+ if (res.frame.parentId) {
+ return;
+ }
+
+ // Any local file is OK
+ if (url.match(/^file:\/\//i) || !_serverProvider) {
+ return;
+ }
+
+ // Need base url to build reg exp
+ baseUrl = _serverProvider.getBaseUrl();
+ if (!baseUrl) {
+ return;
+ }
+
+ // Test that url is within site
+ baseUrlRegExp = new RegExp("^" + StringUtils.regexEscape(baseUrl), "i");
+ if (!url.match(baseUrlRegExp)) {
+ // No longer in site, so terminate live dev, but don't close browser window
+ Inspector.disconnect();
+ _setStatus(STATUS_INACTIVE);
+ _serverProvider = null;
}
}
/** Triggered by Inspector.disconnect */
function _onDisconnect(event) {
$(Inspector.Inspector).off("detached", _onDetached);
+ $(Inspector.Page).off("frameNavigated.DOMAgent", _onFrameNavigated);
+
unloadAgents();
_closeDocument();
_setStatus(STATUS_INACTIVE);
@@ -515,10 +552,11 @@ define(function LiveDevelopment(require, exports, module) {
// helper function that actually does the launch once we are sure we have
// a doc and the server for that doc is up and running.
function doLaunchAfterServerReady() {
- var url = doc.root.url;
+ var targetUrl = doc.root.url;
+ var interstitialUrl = launcherUrl + "?" + encodeURIComponent(targetUrl);
_setStatus(STATUS_CONNECTING);
- Inspector.connectToURL(url).done(result.resolve).fail(function onConnectFail(err) {
+ Inspector.connectToURL(interstitialUrl).done(result.resolve).fail(function onConnectFail(err) {
if (err === "CANCEL") {
result.reject(err);
return;
@@ -555,17 +593,9 @@ define(function LiveDevelopment(require, exports, module) {
retryCount++;
if (!browserStarted && exports.status !== STATUS_ERROR) {
- url = launcherUrl + "?" + encodeURIComponent(url);
-
- // If err === FileError.ERR_NOT_FOUND, it means a remote debugger connection
- // is available, but the requested URL is not loaded in the browser. In that
- // case we want to launch the live browser (to open the url in a new tab)
- // without using the --remote-debugging-port flag. This works around issues
- // on Windows where Chrome can't be opened more than once with the
- // --remote-debugging-port flag set.
NativeApp.openLiveBrowser(
- url,
- err !== NativeFileError.ERR_NOT_FOUND
+ interstitialUrl,
+ true // enable remote debugging
)
.done(function () {
browserStarted = true;
@@ -597,7 +627,7 @@ define(function LiveDevelopment(require, exports, module) {
if (exports.status !== STATUS_ERROR) {
window.setTimeout(function retryConnect() {
- Inspector.connectToURL(url).done(result.resolve).fail(onConnectFail);
+ Inspector.connectToURL(interstitialUrl).done(result.resolve).fail(onConnectFail);
}, 500);
}
});
@@ -632,14 +662,36 @@ define(function LiveDevelopment(require, exports, module) {
return promise;
}
- /** Close the Connection */
+ /**
+ * Close the connection and the associated window asynchronously
+ *
+ * @return {jQuery.Promise} Resolves once the connection is closed
+ */
function close() {
+ var deferred = $.Deferred();
+
+ /*
+ * Finish closing the live development connection, including setting
+ * the status accordingly.
+ */
+ function cleanup() {
+ _setStatus(STATUS_INACTIVE);
+ _serverProvider = null;
+ deferred.resolve();
+ }
+
if (Inspector.connected()) {
- Inspector.Runtime.evaluate("window.open('', '_self').close();");
+ var timer = window.setTimeout(cleanup, 5000); // 5 seconds
+ Inspector.Runtime.evaluate("window.open('', '_self').close();", function (response) {
+ Inspector.disconnect();
+ window.clearTimeout(timer);
+ cleanup();
+ });
+ } else {
+ cleanup();
}
- Inspector.disconnect();
- _setStatus(STATUS_INACTIVE);
- _serverProvider = null;
+
+ return deferred.promise();
}
/** Enable highlighting */
@@ -664,11 +716,89 @@ define(function LiveDevelopment(require, exports, module) {
agents.highlight.redraw();
}
}
+
+ /** Triggered by Inspector.connect */
+ function _onConnect(event) {
+
+ /*
+ * Create a promise that resolves when the interstitial page has
+ * finished loading.
+ *
+ * @return {jQuery.Promise}
+ */
+ function waitForInterstitialPageLoad() {
+ var deferred = $.Deferred(),
+ keepPolling = true,
+ timer = window.setTimeout(function () {
+ keepPolling = false;
+ deferred.reject();
+ }, 10000); // 10 seconds
+
+ /*
+ * Asynchronously check to see if the interstitial page has
+ * finished loading; if not, check again until timing out.
+ */
+ function pollInterstitialPage() {
+ if (keepPolling && Inspector.connected()) {
+ Inspector.Runtime.evaluate("window.isBracketsLiveDevelopmentInterstitialPageLoaded", function (response) {
+ var result = response.result;
+
+ if (result.type === "boolean" && result.value) {
+ window.clearTimeout(timer);
+ deferred.resolve();
+ } else {
+ window.setTimeout(pollInterstitialPage, 100);
+ }
+ });
+ } else {
+ deferred.reject();
+ }
+ }
+
+ pollInterstitialPage();
+ return deferred.promise();
+ }
+
+ /*
+ * Load agents and navigate to the target document once the
+ * interstitial page has finished loading.
+ */
+ function onInterstitialPageLoad() {
+ // Load agents
+ _setStatus(STATUS_LOADING_AGENTS);
+ var promises = loadAgents();
+ $.when.apply(undefined, promises).done(_onLoad).fail(_onError);
+
+ // Load the right document (some agents are waiting for the page's load event)
+ var doc = _getCurrentDocument();
+ if (doc) {
+ Inspector.Page.navigate(doc.root.url);
+ } else {
+ close();
+ }
+ }
+
+ $(Inspector.Inspector).on("detached", _onDetached);
+ $(Inspector.Page).on("frameNavigated.DOMAgent", _onFrameNavigated);
+
+ waitForInterstitialPageLoad()
+ .fail(function () {
+ close();
+ Dialogs.showModalDialog(
+ Dialogs.DIALOG_ID_ERROR,
+ Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
+ Strings.LIVE_DEV_LOADING_ERROR_MESSAGE
+ );
+ })
+ .done(onInterstitialPageLoad);
+ }
/** Triggered by a document change from the DocumentManager */
function _onDocumentChange() {
var doc = _getCurrentDocument(),
- status = STATUS_ACTIVE;
+ status = STATUS_ACTIVE,
+ promise;
+
if (!doc) {
return;
}
@@ -678,18 +808,23 @@ define(function LiveDevelopment(require, exports, module) {
if (agents.network && agents.network.wasURLRequested(doc.url)) {
_closeDocument();
var editor = EditorManager.getCurrentFullEditor();
- _openDocument(doc, editor);
+ promise = _openDocument(doc, editor);
} else {
if (exports.config.experimental || _isHtmlFileExt(doc.extension)) {
- close();
- window.setTimeout(open);
+ promise = close().done(open);
+ } else {
+ promise = $.Deferred().resolve();
}
}
- if (doc.isDirty && _classForDocument(doc) !== CSSDocument) {
- status = STATUS_OUT_OF_SYNC;
- }
- _setStatus(status);
+ promise
+ .fail(close)
+ .done(function () {
+ if (doc.isDirty && _classForDocument(doc) !== CSSDocument) {
+ status = STATUS_OUT_OF_SYNC;
+ }
+ _setStatus(status);
+ });
}
}
@@ -782,6 +917,9 @@ define(function LiveDevelopment(require, exports, module) {
// Register user defined server provider
var userServerProvider = new UserServerProvider();
LiveDevServerManager.registerProvider(userServerProvider, 99);
+
+ // Initialize exports.status
+ _setStatus(STATUS_INACTIVE);
}
function _setServerProvider(serverProvider) {
@@ -792,6 +930,7 @@ define(function LiveDevelopment(require, exports, module) {
exports._pathToUrl = _pathToUrl;
exports._urlToPath = _urlToPath;
exports._setServerProvider = _setServerProvider;
+ exports.launcherUrl = launcherUrl;
// Export public functions
exports.agents = agents;
View
18 src/LiveDevelopment/launch.html
@@ -29,18 +29,10 @@
<script type="application/javascript">
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global window: true */
-
- (function () {
- "use strict";
-
- if (!window.location.search) {
- return;
- }
- var url = decodeURIComponent(window.location.search.slice(1));
- window.setTimeout(function () {
- window.location.href = url;
- }, 2500);
- }());
+
+ function handleLoad () {
+ window.isBracketsLiveDevelopmentInterstitialPageLoaded = true;
+ }
</script>
<style type="text/css">
html, body {
@@ -78,7 +70,7 @@
}
</style>
</head>
-<body>
+<body onload="handleLoad()">
<div id="loading-image">
<div id="spinner"></div>
</div>
View
28 src/LiveDevelopment/main.js
@@ -23,7 +23,7 @@
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
-/*global brackets, define, $, less, window, XMLHttpRequest */
+/*global brackets, define, $, less, window */
/**
* main integrates LiveDevelopment into Brackets
@@ -47,9 +47,9 @@ define(function main(require, exports, module) {
PreferencesManager = require("preferences/PreferencesManager"),
Dialogs = require("widgets/Dialogs"),
UrlParams = require("utils/UrlParams").UrlParams,
- Strings = require("strings");
+ Strings = require("strings"),
+ ExtensionUtils = require("utils/ExtensionUtils");
- var PREFERENCES_CLIENT_ID = PreferencesManager.getClientId(module.id);
var prefs;
var params = new UrlParams();
var config = {
@@ -78,17 +78,13 @@ define(function main(require, exports, module) {
/** Load Live Development LESS Style */
function _loadStyles() {
- var request = new XMLHttpRequest();
- request.open("GET", "LiveDevelopment/main.less", true);
- request.onload = function onLoad(event) {
- var parser = new less.Parser();
- parser.parse(request.responseText, function onParse(err, tree) {
- console.assert(!err, err);
- $("<style>" + tree.toCSS() + "</style>")
- .appendTo(window.document.head);
- });
- };
- request.send(null);
+ var lessText = require("text!LiveDevelopment/main.less"),
+ parser = new less.Parser();
+
+ parser.parse(lessText, function onParse(err, tree) {
+ console.assert(!err, err);
+ ExtensionUtils.addEmbeddedStyleSheet(tree.toCSS());
+ });
}
/**
@@ -220,9 +216,9 @@ define(function main(require, exports, module) {
});
// init prefs
- prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, {highlight: true});
+ prefs = PreferencesManager.getPreferenceStorage(module, {highlight: true});
//TODO: Remove preferences migration code
- PreferencesManager.handleClientIdChange(prefs, "com.adobe.brackets.live-development", {highlight: true});
+ PreferencesManager.handleClientIdChange(prefs, "com.adobe.brackets.live-development");
config.highlight = prefs.getValue("highlight");
View
7 src/brackets.js
@@ -103,12 +103,12 @@ define(function (require, exports, module) {
require("help/HelpCommandHandlers");
require("search/FindInFiles");
require("search/FindReplace");
+ require("extensibility/InstallExtensionDialog");
PerfUtils.addMeasurement("brackets module dependencies resolved");
// Local variables
- var params = new UrlParams(),
- PREFERENCES_CLIENT_ID = PreferencesManager.getClientId(module.id);
+ var params = new UrlParams();
// read URL params
params.parse();
@@ -144,6 +144,7 @@ define(function (require, exports, module) {
NativeApp : require("utils/NativeApp"),
ExtensionUtils : ExtensionUtils,
UpdateNotification : require("utils/UpdateNotification"),
+ InstallExtensionDialog : require("extensibility/InstallExtensionDialog"),
extensions : {}, // place for extensions to hang modules for unit tests
doneLoading : false
};
@@ -199,7 +200,7 @@ define(function (require, exports, module) {
// the samples folder on first launch), open it automatically. (We explicitly check for the
// samples folder in case this is the first time we're launching Brackets after upgrading from
// an old version that might not have set the "afterFirstLaunch" pref.)
- var prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID),
+ var prefs = PreferencesManager.getPreferenceStorage(module),
deferred = new $.Deferred();
//TODO: Remove preferences migration code
PreferencesManager.handleClientIdChange(prefs, "com.adobe.brackets.startup");
View
1 src/command/Commands.js
@@ -47,6 +47,7 @@ define(function (require, exports, module) {
exports.FILE_LIVE_HIGHLIGHT = "file.previewHighlight";
exports.FILE_PROJECT_SETTINGS = "file.projectSettings";
exports.FILE_RENAME = "file.rename";
+ exports.FILE_INSTALL_EXTENSION = "file.installExtension";
exports.FILE_QUIT = "file.quit"; // string must MATCH string in native code (brackets_extensions)
// EDIT
View
2 src/command/DefaultMenus.js
@@ -56,6 +56,8 @@ define(function (require, exports, module) {
menu.addMenuItem(Commands.FILE_LIVE_FILE_PREVIEW);
menu.addMenuItem(Commands.FILE_LIVE_HIGHLIGHT);
menu.addMenuItem(Commands.FILE_PROJECT_SETTINGS);
+ menu.addMenuDivider();
+ menu.addMenuItem(Commands.FILE_INSTALL_EXTENSION);
// supress redundant quit menu item on mac
if (brackets.platform !== "mac" && !brackets.inBrowser) {
View
1 src/config.json
@@ -15,6 +15,7 @@
},
"name": "Brackets",
"version": "0.22.0-0",
+ "apiVersion": "0.22.0",
"homepage": "http://brackets.io",
"issues": {
"url": "http://github.com/adobe/brackets/issues"
View
7 src/document/DocumentManager.js
@@ -96,11 +96,6 @@ define(function (require, exports, module) {
LanguageManager = require("language/LanguageManager");
/**
- * Unique PreferencesManager clientID
- */
- var PREFERENCES_CLIENT_ID = PreferencesManager.getClientId(module.id);
-
- /**
* @private
* @see DocumentManager.getCurrentDocument()
*/
@@ -1261,7 +1256,7 @@ define(function (require, exports, module) {
exports.notifyPathNameChanged = notifyPathNameChanged;
// Setup preferences
- _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID);
+ _prefs = PreferencesManager.getPreferenceStorage(module);
//TODO: Remove preferences migration code
PreferencesManager.handleClientIdChange(_prefs, "com.adobe.brackets.DocumentManager");
View
2 src/editor/CodeHintList.js
@@ -191,7 +191,7 @@ define(function (require, exports, module) {
this.handleClose();
}
} else {
- $.each(this.hints, function (index, item) {
+ this.hints.forEach(function (item, index) {
if (index > self.maxResults) {
return false;
}
View
2 src/editor/CodeHintManager.js
@@ -398,7 +398,7 @@ define(function (require, exports, module) {
var mode = editor.getModeForSelection(),
enabledProviders = _getProvidersForMode(mode);
- $.each(enabledProviders, function (index, item) {
+ enabledProviders.forEach(function (item, index) {
if (item.provider.hasHints(editor, lastChar)) {
sessionProvider = item.provider;
return false;
View
7 src/editor/Editor.js
@@ -72,13 +72,12 @@ define(function (require, exports, module) {
TokenUtils = require("utils/TokenUtils"),
ViewUtils = require("utils/ViewUtils");
- var PREFERENCES_CLIENT_ID = PreferencesManager.getClientId(module.id),
- defaultPrefs = { useTabChar: false, tabSize: 4, indentUnit: 4, closeBrackets: false };
+ var defaultPrefs = { useTabChar: false, tabSize: 4, indentUnit: 4, closeBrackets: false };
/** Editor preferences */
- var _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID);
+ var _prefs = PreferencesManager.getPreferenceStorage(module, defaultPrefs);
//TODO: Remove preferences migration code
- PreferencesManager.handleClientIdChange(_prefs, "com.adobe.brackets.Editor", defaultPrefs);
+ PreferencesManager.handleClientIdChange(_prefs, "com.adobe.brackets.Editor");
/** @type {boolean} Global setting: When inserting new text, use tab characters? (instead of spaces) */
var _useTabChar = _prefs.getValue("useTabChar");
View
116 src/editor/EditorCommandHandlers.js
@@ -48,21 +48,69 @@ define(function (require, exports, module) {
/**
* @private
+ * Creates regular expressions for multiple line comment prefixes
+ * @param {!Array.<string>} prefixes - the line comment prefixes
+ * @return {Array.<RegExp>}
+ */
+ function _createLineExpressions(prefixes) {
+ var lineExp = [];
+ prefixes.forEach(function (prefix) {
+ lineExp.push(new RegExp("^\\s*" + StringUtils.regexEscape(prefix)));
+ });
+ return lineExp;
+ }
+
+ /**
+ * @private
+ * Returns true if any regular expression matches the given string
+ * @param {!string} string - where to look
+ * @param {!Array.<RegExp>} expressions - what to look
+ * @return {boolean}
+ */
+ function _matchExpressions(string, expressions) {
+ return expressions.some(function (exp) {
+ return string.match(exp);
+ });
+ }
+
+ /**
+ * @private
+ * Returns the line comment prefix that best matches the string. Since there might be line comment prefixes
+ * that are prefixes of other line comment prefixes, it searches throught all and returns the longest line
+ * comment prefix that matches the string.
+ * @param {!string} string - where to look
+ * @param {!Array.<RegExp>} expressions - the line comment regular expressions
+ * @param {!Array.<string>} prefixes - the line comment prefixes
+ * @return {string}
+ */
+ function _getLinePrefix(string, expressions, prefixes) {
+ var result = null;
+ expressions.forEach(function (exp, index) {
+ if (string.match(exp) && ((result && result.length < prefixes[index].length) || !result)) {
+ result = prefixes[index];
+ }
+ });
+ return result;
+ }
+
+ /**
+ * @private
* Searchs for an uncommented line between startLine and endLine
* @param {!Editor} editor
* @param {!number} startLine - valid line inside the document
* @param {!number} endLine - valid line inside the document
+ * @param {!Array.<string>} lineExp - an array of line comment prefixes regular expressions
* @return {boolean} true if there is at least one uncommented line
*/
- function _containsUncommented(editor, startLine, endLine, prefix) {
- var lineExp = new RegExp("^\\s*" + StringUtils.regexEscape(prefix));
+ function _containsUncommented(editor, startLine, endLine, lineExp) {
var containsUncommented = false;
var i;
var line;
+
for (i = startLine; i <= endLine; i++) {
line = editor.document.getLine(i);
- // A line is commented out if it starts with 0-N whitespace chars, then "//"
- if (!line.match(lineExp) && line.match(/\S/)) {
+ // A line is commented out if it starts with 0-N whitespace chars, then a line comment prefix
+ if (line.match(/\S/) && !_matchExpressions(line, lineExp)) {
containsUncommented = true;
break;
}
@@ -77,12 +125,16 @@ define(function (require, exports, module) {
* If all non-whitespace lines are already commented out, then we uncomment; otherwise we comment
* out. Commenting out adds the prefix at column 0 of every line. Uncommenting removes the first prefix
* on each line (if any - empty lines might not have one).
+ *
+ * @param {!Editor} editor
+ * @param {!Array.<string>} prefixes, e.g. ["//"]
*/
- function lineCommentPrefix(editor, prefix) {
- var doc = editor.document;
- var sel = editor.getSelection();
- var startLine = sel.start.line;
- var endLine = sel.end.line;
+ function lineCommentPrefix(editor, prefixes) {
+ var doc = editor.document,
+ sel = editor.getSelection(),
+ startLine = sel.start.line,
+ endLine = sel.end.line,
+ lineExp = _createLineExpressions(prefixes);
// Is a range of text selected? (vs just an insertion pt)
var hasSelection = (startLine !== endLine) || (sel.start.ch !== sel.end.ch);
@@ -95,31 +147,35 @@ define(function (require, exports, module) {
// Decide if we're commenting vs. un-commenting
// Are there any non-blank lines that aren't commented out? (We ignore blank lines because
// some editors like Sublime don't comment them out)
- var containsUncommented = _containsUncommented(editor, startLine, endLine, prefix);
+ var containsUncommented = _containsUncommented(editor, startLine, endLine, lineExp);
var i;
var line;
+ var prefix;
+ var commentI;
var updateSelection = false;
// Make the edit
doc.batchOperation(function () {
if (containsUncommented) {
- // Comment out - prepend "//" to each line
+ // Comment out - prepend the first prefix to each line
for (i = startLine; i <= endLine; i++) {
- doc.replaceRange(prefix, {line: i, ch: 0});
+ doc.replaceRange(prefixes[0], {line: i, ch: 0});
}
- // Make sure selection includes "//" that was added at start of range
+ // Make sure selection includes the prefix that was added at start of range
if (sel.start.ch === 0 && hasSelection) {
updateSelection = true;
}
-
+
} else {
- // Uncomment - remove first "//" on each line (if any)
+ // Uncomment - remove the prefix on each line (if any)
for (i = startLine; i <= endLine; i++) {
- line = doc.getLine(i);
- var commentI = line.indexOf(prefix);
- if (commentI !== -1) {
+ line = doc.getLine(i);
+ prefix = _getLinePrefix(line, lineExp, prefixes);
+
+ if (prefix) {
+ commentI = line.indexOf(prefix);
doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + prefix.length});
}
}
@@ -206,9 +262,9 @@ define(function (require, exports, module) {
* @param {!Editor} editor
* @param {!string} prefix, e.g. "<!--"
* @param {!string} suffix, e.g. "-->"
- * @param {?string} linePrefix, e.g. "//"
+ * @param {!Array.<string>} linePrefixes, e.g. ["//"]
*/
- function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefix) {
+ function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefixes) {
var doc = editor.document,
sel = editor.getSelection(),
@@ -217,7 +273,7 @@ define(function (require, exports, module) {
endCtx = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.end.line, ch: sel.end.ch}),
prefixExp = new RegExp("^" + StringUtils.regexEscape(prefix), "g"),
suffixExp = new RegExp(StringUtils.regexEscape(suffix) + "$", "g"),
- lineExp = linePrefix ? new RegExp("^" + StringUtils.regexEscape(linePrefix)) : null,
+ lineExp = _createLineExpressions(linePrefixes),
prefixPos = null,
suffixPos = null,
canComment = false,
@@ -233,13 +289,13 @@ define(function (require, exports, module) {
}
// Check if we should just do a line uncomment (if all lines in the selection are commented).
- if (lineExp && (ctx.token.string.match(lineExp) || endCtx.token.string.match(lineExp))) {
+ if (lineExp.length && (_matchExpressions(ctx.token.string, lineExp) || _matchExpressions(endCtx.token.string, lineExp))) {
var startCtxIndex = editor.indexFromPos({line: ctx.pos.line, ch: ctx.token.start});
var endCtxIndex = editor.indexFromPos({line: endCtx.pos.line, ch: endCtx.token.start + endCtx.token.string.length});
// Find if we aren't actually inside a block-comment
result = true;
- while (result && ctx.token.string.match(lineExp)) {
+ while (result && _matchExpressions(ctx.token.string, lineExp)) {
result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);
}
@@ -255,7 +311,7 @@ define(function (require, exports, module) {
}
// Find if all the lines are line-commented.
- if (!_containsUncommented(editor, sel.start.line, endLine, linePrefix)) {
+ if (!_containsUncommented(editor, sel.start.line, endLine, lineExp)) {
lineUncomment = true;
// Block-comment in all the other cases
@@ -327,7 +383,7 @@ define(function (require, exports, module) {
return;
} else if (lineUncomment) {
- lineCommentPrefix(editor, linePrefix);
+ lineCommentPrefix(editor, linePrefixes);
} else {
doc.batchOperation(function () {
@@ -437,11 +493,11 @@ define(function (require, exports, module) {
result = result && _findNextBlockComment(ctx, selEnd, prefixExp);
if (className === "comment" || result || isLineSelection) {
- blockCommentPrefixSuffix(editor, prefix, suffix);
+ blockCommentPrefixSuffix(editor, prefix, suffix, []);
} else {
// Set the new selection and comment it
editor.setSelection(selStart, selEnd);
- blockCommentPrefixSuffix(editor, prefix, suffix);
+ blockCommentPrefixSuffix(editor, prefix, suffix, []);
// Restore the old selection taking into account the prefix change
if (isMultipleLine) {
@@ -469,8 +525,8 @@ define(function (require, exports, module) {
var language = editor.getLanguageForSelection();
if (language.hasBlockCommentSyntax()) {
- // getLineCommentPrefix returns null if no line comment syntax is defined
- blockCommentPrefixSuffix(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix(), language.getLineCommentPrefix());
+ // getLineCommentPrefixes always return an array, and will be empty if no line comment syntax is defined
+ blockCommentPrefixSuffix(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix(), language.getLineCommentPrefixes());
}
}
@@ -487,7 +543,7 @@ define(function (require, exports, module) {
var language = editor.getLanguageForSelection();
if (language.hasLineCommentSyntax()) {
- lineCommentPrefix(editor, language.getLineCommentPrefix());
+ lineCommentPrefix(editor, language.getLineCommentPrefixes());
} else if (language.hasBlockCommentSyntax()) {
lineCommentPrefixSuffix(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix());
}
View
4 src/editor/InlineTextEditor.js
@@ -64,8 +64,8 @@ define(function (require, exports, module) {
var $dirtyIndicators = $(".inlineEditorHolder .dirty-indicator"),
$indicator;
- $.each($dirtyIndicators, function (index, indicator) {
- $indicator = $(indicator);
+ $dirtyIndicators.each(function (index, indicator) {
+ $indicator = $(this);
if ($indicator.data("fullPath") === doc.file.fullPath) {
_showDirtyIndicator($indicator, doc.isDirty);
}
View
353 src/extensibility/InstallExtensionDialog.js
@@ -0,0 +1,353 @@
+/*
+ * 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, window, $, PathUtils, Mustache, document */
+
+define(function (require, exports, module) {
+ "use strict";
+
+ require("thirdparty/path-utils/path-utils.min");
+
+ var Dialogs = require("widgets/Dialogs"),
+ StringUtils = require("utils/StringUtils"),
+ Strings = require("strings"),
+ Commands = require("command/Commands"),
+ CommandManager = require("command/CommandManager"),
+ KeyEvent = require("utils/KeyEvent"),
+ Package = require("extensibility/Package"),
+ InstallDialogTemplate = require("text!extensibility/install-extension-dialog.html");
+
+ var STATE_CLOSED = 0,
+ STATE_START = 1,
+ STATE_VALID_URL = 2,
+ STATE_INSTALLING = 3,
+ STATE_INSTALLED = 4,
+ STATE_INSTALL_FAILED = 5,
+ STATE_INSTALL_CANCELLED = 6;
+
+ /**
+ * @constructor
+ * Creates a new extension installer dialog.
+ * @param {{install: function(url), cancel: function()}} installer The installer backend to use.
+ */
+ function InstallExtensionDialog(installer) {
+ this._installer = installer;
+ this._state = STATE_CLOSED;
+ }
+
+ /** @type {jQuery} The dialog root. */
+ InstallExtensionDialog.prototype.$dlg = null;
+
+ /** @type {jQuery} The url input field. */
+ InstallExtensionDialog.prototype.$url = null;
+
+ /** @type {jQuery} The ok button. */
+ InstallExtensionDialog.prototype.$okButton = null;
+
+ /** @type {jQuery} The cancel button. */
+ InstallExtensionDialog.prototype.$cancelButton = null;
+
+ /** @type {jQuery} The area containing the url input label and field. */
+ InstallExtensionDialog.prototype.$inputArea = null;
+
+ /** @type {jQuery} The area containing the installation message and spinner. */
+ InstallExtensionDialog.prototype.$msgArea = null;
+
+ /** @type {jQuery} The span containing the installation message. */
+ InstallExtensionDialog.prototype.$msg = null;
+
+ /** @type {jQuery} The installation progress spinner. */
+ InstallExtensionDialog.prototype.$spinner = null;
+
+ /** @type {$.Deferred} A deferred that's resolved/rejected when the dialog is closed and
+ something has/hasn't been installed successfully. */
+ InstallExtensionDialog.prototype._dialogDeferred = null;
+
+
+ /** @type {{install: function(url), cancel: function()}} installer The installer backend for this dialog. */
+ InstallExtensionDialog.prototype._installer = null;
+
+ /** @type {number} The current state of the dialog; one of the STATE_* constants above. */
+ InstallExtensionDialog.prototype._state = null;
+
+ /**
+ * @private
+ * Transitions the dialog into a new state as the installation proceeds.
+ * @param {number} newState The state to transition into; one of the STATE_* variables.
+ */
+ InstallExtensionDialog.prototype._enterState = function (newState) {
+ var url,
+ msg,
+ self = this,
+ prevState = this._state;
+
+ // Store the new state up front in case some of the processing below ends up changing
+ // the state again immediately.
+ this._state = newState;
+
+ switch (newState) {
+ case STATE_START:
+ // This should match the default appearance of the dialog when it first opens.
+ this.$spinner.removeClass("spin");
+ this.$msgArea.hide();
+ this.$inputArea.show();
+ this.$okButton
+ .attr("disabled", "disabled")
+ .text(Strings.INSTALL);
+ break;
+
+ case STATE_VALID_URL:
+ this.$okButton.removeAttr("disabled");
+ break;
+
+ case STATE_INSTALLING:
+ url = this.$url.val();
+ this.$inputArea.hide();
+ this.$msg.text(StringUtils.format(Strings.INSTALLING_FROM, url));
+ this.$spinner.addClass("spin");
+ this.$msgArea.show();
+ this.$okButton.attr("disabled", "disabled");
+ this._installer.install(url)
+ .done(function () {
+ self._enterState(STATE_INSTALLED);
+ })
+ .fail(function (err) {
+ // Ignore expected case: the Promise is failed when we programmatically cancel
+ // In all other cases, record the error and transition to error display UI
+ if (!(err === "CANCELED" && self._state === STATE_INSTALL_CANCELLED)) {
+ self._errorMessage = Package.formatError(err);
+ self._enterState(STATE_INSTALL_FAILED);
+ }
+ });
+ break;
+
+ case STATE_INSTALLED:
+ case STATE_INSTALL_FAILED:
+ case STATE_INSTALL_CANCELLED:
+ if (newState === STATE_INSTALL_CANCELLED) {
+ // TODO: do we need to wait for acknowledgement? That will require adding a new
+ // "waiting for cancelled" state.
+ var success = this._installer.cancel();
+ console.assert(success);
+ }
+
+ this.$spinner.removeClass("spin");
+ if (newState === STATE_INSTALLED) {
+ msg = Strings.INSTALL_SUCCEEDED;
+ } else if (newState === STATE_INSTALL_FAILED) {
+ // TODO: nicer formatting, especially for validation errors where there might be > 1 error code
+ msg = Strings.INSTALL_FAILED + "\n" + this._errorMessage;
+ } else {
+ msg = Strings.INSTALL_CANCELLED;
+ }
+ this.$msg.text(msg);
+ this.$okButton
+ .removeAttr("disabled")
+ .text(Strings.CLOSE);
+ this.$cancelButton.hide();
+ break;
+
+ case STATE_CLOSED:
+ $(document.body).off(".installDialog");
+
+ // Only resolve as successful if we actually installed something.
+ Dialogs.cancelModalDialogIfOpen("install-extension-dialog");
+ if (prevState === STATE_INSTALLED) {
+ this._dialogDeferred.resolve();
+ } else {
+ this._dialogDeferred.reject();
+ }
+ break;
+ }
+ };
+
+ /**
+ * @private
+ * Handle a click on the Cancel button, which either cancels an ongoing installation (leaving
+ * the dialog open), or closes the dialog if no installation is in progress.
+ */
+ InstallExtensionDialog.prototype._handleCancel = function () {
+ if (this._state === STATE_INSTALLING) {
+ this._enterState(STATE_INSTALL_CANCELLED);
+ } else {
+ this._enterState(STATE_CLOSED);
+ }
+ };
+
+ /**
+ * @private
+ * Handle a click on the default button, which is "Install" while we're waiting for the
+ * user to enter a URL, and "Close" once we've successfully finished installation.
+ */
+ InstallExtensionDialog.prototype._handleOk = function () {
+ if (this._state === STATE_INSTALLED ||
+ this._state === STATE_INSTALL_FAILED ||
+ this._state === STATE_INSTALL_CANCELLED) {
+ // In these end states, this is a "Close" button: just close the dialog and indicate
+ // success.
+ this._enterState(STATE_CLOSED);
+ } else if (this._state === STATE_VALID_URL) {
+ this._enterState(STATE_INSTALLING);
+ }
+ };
+
+ /**
+ * @private
+ * Handle key up events on the document. We use this to detect the Esc key.
+ */
+ InstallExtensionDialog.prototype._handleKeyUp = function (e) {
+ if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+ this._handleCancel();
+ }
+ };
+
+ /**
+ * @private
+ * Handle typing in the URL field.
+ */
+ InstallExtensionDialog.prototype._handleUrlInput = function () {
+ var url = this.$url.val(),
+ valid = (url !== "");
+ if (!valid && this._state === STATE_VALID_URL) {
+ this._enterState(STATE_START);
+ } else if (valid && this._state === STATE_START) {
+ this._enterState(STATE_VALID_URL);
+ }
+ };
+
+ /**
+ * @private
+ * Sets the installer backend.
+ * @param {{install: function(string): $.Promise, cancel: function()}} installer
+ * The installer backend object to use. Must define two functions:
+ * install(url): takes the URL of the extension to install, and returns a promise
+ * that's resolved or rejected when the installation succeeds or fails.
+ * cancel(): cancels an ongoing installation.
+ */
+ InstallExtensionDialog.prototype._setInstaller = function (installer) {
+ this._installer = installer;
+ };
+
+ /**
+ * @private
+ * Returns the jQuery objects for various dialog fileds. For unit testing only.
+ * @return {object} fields An object containing "dlg", "okButton", "cancelButton", and "url" fields.
+ */
+ InstallExtensionDialog.prototype._getFields = function () {
+ return {
+ $dlg: this.$dlg,
+ $okButton: this.$okButton,
+ $cancelButton: this.$cancelButton,
+ $url: this.$url
+ };
+ };
+
+ /**
+ * @private
+ * Closes the dialog if it's not already closed. For unit testing only.
+ */
+ InstallExtensionDialog.prototype._close = function () {
+ if (this._state !== STATE_CLOSED) {
+ this._enterState(STATE_CLOSED);
+ }
+ };
+
+ /**
+ * Initialize and show the dialog.
+ * @return {$.Promise} A promise object that will be resolved when the selected extension
+ * has finished installing, or rejected if the dialog is cancelled.
+ */
+ InstallExtensionDialog.prototype.show = function () {
+ if (this._state !== STATE_CLOSED) {
+ // Somehow the dialog got invoked twice. Just ignore this.
+ return;
+ }
+
+ // We ignore the promise returned by showModalDialogUsingTemplate, since we're managing the
+ // lifecycle of the dialog ourselves.
+ Dialogs.showModalDialogUsingTemplate(
+ Mustache.render(InstallDialogTemplate, Strings),
+ null,
+ null,
+ false
+ );
+
+ this.$dlg = $(".install-extension-dialog.instance");
+ this.$url = this.$dlg.find(".url").focus();
+ this.$okButton = this.$dlg.find(".dialog-button[data-button-id='ok']");
+ this.$cancelButton = this.$dlg.find(".dialog-button[data-button-id='cancel']");
+ this.$inputArea = this.$dlg.find(".input-field");
+ this.$msgArea = this.$dlg.find(".message-field");
+ this.$msg = this.$msgArea.find(".message");
+ this.$spinner = this.$msgArea.find(".spinner");
+
+ this.$okButton.on("click", this._handleOk.bind(this));
+ this.$cancelButton.on("click", this._handleCancel.bind(this));
+ this.$url.on("input", this._handleUrlInput.bind(this));
+ $(document.body).on("keyup.installDialog", this._handleKeyUp.bind(this));
+
+ this._enterState(STATE_START);
+
+ this._dialogDeferred = new $.Deferred();
+ return this._dialogDeferred.promise();
+ };
+
+ function RealInstaller() { }
+ RealInstaller.prototype.install = function (url) {
+ if (this.pendingInstall) {
+ console.error("Extension installation already pending");
+ return { promise: new $.Deferred().reject("DOWNLOAD_ID_IN_USE").promise() };
+ }
+ this.pendingInstall = Package.installFromURL(url);
+
+ // Store now since we'll null pendingInstall immediately if the promise was resolved synchronously
+ var promise = this.pendingInstall.promise;
+
+ var self = this;
+ this.pendingInstall.promise.always(function () {
+ self.pendingInstall = null;
+ });
+
+ return promise;
+ };
+ RealInstaller.prototype.cancel = function () {
+ return this.pendingInstall.cancel();
+ };
+
+ /**
+ * @private
+ * Show a dialog that allows the user to enter the URL of an extension ZIP file to install.
+ * @return {$.Promise} A promise object that will be resolved when the selected extension
+ * has finished installing, or rejected if the dialog is cancelled.
+ */
+ function _showDialog(installer) {
+ var dlg = new InstallExtensionDialog(new RealInstaller());
+ return dlg.show();
+ }
+
+ CommandManager.register(Strings.CMD_INSTALL_EXTENSION, Commands.FILE_INSTALL_EXTENSION, _showDialog);
+
+ // Exposed for unit testing only
+ exports._Dialog = InstallExtensionDialog;
+});
View
422 src/extensibility/Package.js
@@ -0,0 +1,422 @@
+/*
+ * 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, regexp: true,
+indent: 4, maxerr: 50 */
+/*global define, $, brackets, PathUtils */
+
+/* Functions for working with extension packages */
+
+define(function (require, exports, module) {
+ "use strict";
+
+ var AppInit = require("utils/AppInit"),
+ FileUtils = require("file/FileUtils"),
+ StringUtils = require("utils/StringUtils"),
+ Strings = require("strings"),
+ ExtensionLoader = require("utils/ExtensionLoader"),
+ NodeConnection = require("utils/NodeConnection");
+
+ var Errors = {
+ ERROR_LOADING: "ERROR_LOADING",
+ MALFORMED_URL: "MALFORMED_URL",
+ UNSUPPORTED_PROTOCOL: "UNSUPPORTED_PROTOCOL"
+ };
+
+ /**
+ * @const
+ * Amount of time to wait before automatically rejecting the connection
+ * deferred. If we hit this timeout, we'll never have a node connection
+ * for the installer in this run of Brackets.
+ */
+ var NODE_CONNECTION_TIMEOUT = 30000; // 30 seconds - TODO: share with StaticServer?
+
+ /**
+ * @private
+ * @type{jQuery.Deferred.<NodeConnection>}
+ * A deferred which is resolved with a NodeConnection or rejected if
+ * we are unable to connect to Node.
+ */
+ var _nodeConnectionDeferred = $.Deferred();
+
+ /**
+ * @type {number} Used to generate unique download ids
+ */
+ var _uniqueId = 0;
+
+ /**
+ * TODO: can this go away now that we never call it directly?
+ *
+ * Validates the package at the given path. The actual validation is
+ * handled by the Node server.
+ *
+ * The promise is resolved with an object:
+ * { errors: Array.<{string}>, metadata: { name:string, version:string, ... } }
+ * metadata is pulled straight from package.json and will be undefined
+ * if there are errors or null if the extension did not include package.json.
+ *
+ * @param {string} Absolute path to the package zip file
+ * @return {$.Promise} A promise that is resolved with information about the package
+ */
+ function validate(path) {
+ var d = new $.Deferred();
+ _nodeConnectionDeferred.done(function (nodeConnection) {
+ if (nodeConnection.connected()) {
+ nodeConnection.domains.extensionManager.validate(path)
+ .done(function (result) {
+
+ // Convert the errors into properly localized strings
+ var i,
+ errors = result.errors;
+
+ for (i = 0; i < errors.length; i++) {
+ var formatArguments = errors[i];
+ formatArguments[0] = Strings[formatArguments[0]];
+ errors[i] = StringUtils.format.apply(window, formatArguments);
+ }
+
+ d.resolve({
+ errors: errors,
+ metadata: result.metadata
+ });
+ })
+ .fail(function (error) {
+ d.reject(error);
+ });
+ } else {
+ d.reject();
+ }
+ })
+ .fail(function (error) {
+ d.reject(error);
+ });
+ return d.promise();
+ }
+
+ /**
+ * Validates and installs the package at the given path. Validation and
+ * installation is handled by the Node process.
+ *
+ * The extension will be installed into the user's extensions directory.
+ * If the user already has the extension installed, it will instead go
+ * into their disabled extensions directory.
+ *
+ * The promise is resolved with an object:
+ * { errors: Array.<{string}>, metadata: { name:string, version:string, ... },
+ * disabledReason:string, installedTo:string, commonPrefix:string }
+ * metadata is pulled straight from package.json and is likely to be undefined
+ * if there are errors. It is null if there was no package.json.
+ *
+ * disabledReason is either null or the reason the extension was installed disabled.
+ *
+ * @param {string} Absolute path to the package zip file
+ * @return {promise} A promise that is resolved with information about the package
+ * (which may include errors, in which case the extension was disabled), or
+ * rejected with an error object.
+ */
+ function install(path) {
+ var d = new $.Deferred();
+ _nodeConnectionDeferred.done(function (nodeConnection) {
+ if (nodeConnection.connected()) {
+ var destinationDirectory = ExtensionLoader.getUserExtensionPath();
+ var disabledDirectory = destinationDirectory.replace(/\/user$/, "/disabled");
+ nodeConnection.domains.extensionManager.install(path, destinationDirectory, {
+ disabledDirectory: disabledDirectory,
+ apiVersion: brackets.metadata.apiVersion
+ })
+ .done(function (result) {
+ // If there were errors or the extension is disabled, we don't
+ // try to load it so we're ready to return
+ if (result.errors.length > 0 || result.disabledReason) {
+ d.resolve(result);
+ } else {
+ // This was a new extension and everything looked fine.
+ // We load it into Brackets right away.
+ ExtensionLoader.loadExtension(result.name, {
+ baseUrl: result.installedTo
+ }, "main").then(function () {
+ d.resolve(result);
+ }, function () {
+ d.reject(Errors.ERROR_LOADING);
+ });
+ }
+ })
+ .fail(function (error) {
+ d.reject(error);
+ });
+ } else {
+ d.reject();
+ }
+ })
+ .fail(function (error) {
+ d.reject(error);
+ });
+ return d.promise();
+ }
+
+
+
+ /**
+ * Special case handling to make the common case of downloading from GitHub easier; modifies 'urlInfo' as
+ * needed. Converts a bare GitHub repo URL to the corresponding master ZIP URL; or if given a direct
+ * master ZIP URL already, sets a nicer download filename (both cases use the repo name).
+ *
+ * @param {{url:string, parsed:Array.<string>, filenameHint:string}} urlInfo
+ */
+ function githubURLFilter(urlInfo) {
+ if (urlInfo.parsed.hostname === "github.com" || urlInfo.parsed.hostname === "www.github.com") {
+ // Is it a URL to the root of a repo? (/user/repo)
+ var match = /^\/[^\/?]+\/([^\/?]+)(\/?)$/.exec(urlInfo.parsed.pathname);
+ if (match) {
+ if (!match[2]) {
+ urlInfo.url += "/";
+ }
+ urlInfo.url += "archive/master.zip";
+ urlInfo.filenameHint = match[1];
+
+ } else {
+ // Is it a URL directly to the repo's 'master.zip'? (/user/repo/archive/master.zip)
+ match = /^\/[^\/?]+\/([^\/?]+)\/archive\/master.zip$/.exec(urlInfo.parsed.pathname);
+ if (match) {
+ urlInfo.filenameHint = match[1];
+ }
+ }
+ }
+ }
+
+ /**
+ * Downloads from the given URL to a temporary location. On success, resolves with the local path
+ * of the downloaded file. On failure, rejects with an error object.
+ *
+ * @param {string} url URL of the file to be downloaded
+ * @param {number} downloadId Unique number to identify this request
+ * @return {$.Promise}
+ */
+ function download(url, downloadId) {
+ var d = new $.Deferred();
+ _nodeConnectionDeferred.done(function (connection) {
+ // Validate URL
+ // TODO: PathUtils fails to parse URLs that are missing the protocol part (e.g. starts immediately with "www...")
+ var parsed = PathUtils.parseUrl(url);
+ if (!parsed.hostname) { // means PathUtils failed to parse at all
+ d.reject(Errors.MALFORMED_URL);
+ return d.promise();
+ }
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
+ d.reject(Errors.UNSUPPORTED_PROTOCOL);
+ return d.promise();
+ }
+
+ var urlInfo = { url: url, parsed: parsed, filenameHint: parsed.filename };
+ githubURLFilter(urlInfo);
+
+ // Decide download destination
+ var filename = urlInfo.filenameHint;
+ filename = filename.replace(/[^a-zA-Z0-9_\- \(\)\.]/g, "_"); // make sure it's a valid filename
+ if (!filename) { // in case of URL ending in "/"
+ filename = "extension.zip";
+ }
+
+ var tempDownloadFolder = brackets.app.getApplicationSupportDirectory() + "/extensions/";
+ var localPath = tempDownloadFolder + filename;
+
+ // Download the bits (using Node since brackets-shell doesn't support binary file IO)
+ var r = connection.domains.extensionManager.downloadFile(downloadId, urlInfo.url, localPath);
+ r.done(function (result) {
+ d.resolve(localPath);
+ }).fail(function (err) {
+ d.reject(err);
+ });
+ })
+ .fail(function (error) {
+ d.reject(error);
+ });
+ return d.promise();
+ }
+
+ /**
+ * Attempts to synchronously cancel the given pending download. This may not be possible, e.g.
+ * if the download has already finished.
+ *
+ * @param {number} downloadId Identifier previously passed to download()
+ * @return {boolean}
+ */
+ function cancelDownload(downloadId) {
+ // TODO: if we're still waiting on the NodeConnection, how do we cancel?
+ console.assert(_nodeConnectionDeferred.isResolved());
+ var success;
+ _nodeConnectionDeferred.done(function (connection) {
+ success = connection.domains.extensionManager.abortDownload(downloadId);
+ });
+ return success;
+ }
+
+
+ /**
+ * On success, resolves with an extension metadata object; at that point, the extension has already
+ * started running in Brackets. On failure (including validation errors), rejects with an error object.
+ *
+ * The error information may be an array of error objects (for valdiation errors), or a single error
+ * object. An individual error object consists of either a string error code OR an array where the first
+ * entry is the error code and the remaining entries are further info. The error code string is one of
+ * either ExtensionsDomain.Errors or Package.Errors.
+ * TODO: if top level value is an array it's ambiguous (multiple error objects or one?)
+ *
+ * Use formatError() to convert a single error object to a friendly, localized error message.
+ *
+ * @return {{promise: $.Promise, cancel: function():boolean}}
+ */
+ function installFromURL(url) {
+ var STATE_DOWNLOADING = 1,
+ STATE_INSTALLING = 2,
+ STATE_SUCCEEDED = 3,
+ STATE_FAILED = 4;
+
+ var d = new $.Deferred();
+ var state = STATE_DOWNLOADING;
+
+ var downloadId = (_uniqueId++);
+ download(url, downloadId)
+ .done(function (localPath) {
+ state = STATE_INSTALLING;
+
+ // TOOD: need to clean up ZIP from temp location
+
+ install(localPath)
+ .done(function (result) {
+ if (result.errors && result.errors.length > 0) {
+ // Validation errors
+ state = STATE_FAILED;
+ d.reject(result.errors);
+ } else if (result.disabledReason) {
+ // Extension valid but left disabled (wrong API version, extension name collision, etc.)
+ state = STATE_FAILED;
+ d.reject(result.disabledReason);
+ } else {
+ // Success! Extension is now running in Brackets
+ state = STATE_SUCCEEDED;
+ d.resolve(result);
+ }
+ })
+ .fail(function (err) {
+ // File IO errors, internal error in install()/validate(), or extension startup crashed
+ state = STATE_FAILED;
+ d.reject(err);
+ })
+ .always(function () {
+ // Whether success or failure, we can delete the original downloaded ZIP file now
+ brackets.fs.unlink(localPath, function (err) {
+ // ignore errors
+ });
+ });
+ })
+ .fail(function (err) {
+ // Download errors
+ state = STATE_FAILED;
+ d.reject(err);
+ });
+
+ return {
+ promise: d.promise(),
+ _downloadId: downloadId,
+ cancel: function () {
+ if (state === STATE_DOWNLOADING) {
+ return cancelDownload(this._downloadId);
+ } else {
+ return false;
+ }
+ }
+ };
+ }
+
+ /**
+ * Converts an error object as returned by install() or installFromURL() into a flattened, localized string.
+ * @param {string|Array.<string>} error
+ * @return {string}
+ */
+ function formatError(error) {
+ function localize(key) {
+ return Strings[key] || Strings.UNKNOWN_ERROR;
+ }
+
+ if (Array.isArray(error)) {
+ error[0] = localize(error[0]);
+ return StringUtils.format.apply(window, error);
+ } else {
+ return localize(error);
+ }
+ }
+
+
+ /**
+ * Allows access to the deferred that manages the node connection. This
+ * is *only* for unit tests. Messing with this not in testing will
+ * potentially break everything.
+ *
+ * @private
+ * @return {jQuery.Deferred} The deferred that manages the node connection
+ */
+ function _getNodeConnectionDeferred() {
+ return _nodeConnectionDeferred;
+ }
+
+ // Initializes node connection
+ // TODO: duplicates code from StaticServer
+ // TODO: can this be done lazily?
+ AppInit.appReady(function () {
+ // Start up the node connection, which is held in the
+ // _nodeConnectionDeferred module variable. (Use
+ // _nodeConnectionDeferred.done() to access it.
+ var connectionTimeout = setTimeout(function () {
+ console.error("[Extensions] Timed out while trying to connect to node");
+ _nodeConnectionDeferred.reject();
+ }, NODE_CONNECTION_TIMEOUT);
+
+ var _nodeConnection = new NodeConnection();
+ _nodeConnection.connect(true).then(function () {
+ var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/ExtensionManagerDomain";
+
+ _nodeConnection.loadDomains(domainPath, true)
+ .then(
+ function () {
+ clearTimeout(connectionTimeout);
+ _nodeConnectionDeferred.resolve(_nodeConnection);
+ },
+ function () { // Failed to connect
+ console.error("[Extensions] Failed to connect to node", arguments);
+ clearTimeout(connectionTimeout);
+ _nodeConnectionDeferred.reject();
+ }
+ );
+ });
+ });
+
+ // For unit tests only
+ exports._getNodeConnectionDeferred = _getNodeConnectionDeferred;
+
+ exports.installFromURL = installFromURL;
+ exports.validate = validate;
+ exports.install = install;
+ exports.formatError = formatError;
+});
View
18 src/extensibility/install-extension-dialog.html
@@ -0,0 +1,18 @@
+<div class="install-extension-dialog modal">
+ <div class="modal-header">
+ <h1 class="dialog-title">{{INSTALL_EXTENSION_TITLE}}</h1>
+ </div>
+ <div class="modal-body">
+ <div class="field-container input-field">
+ <label>{{INSTALL_EXTENSION_LABEL}}: <input type="text" placeholder="{{INSTALL_EXTENSION_HINT}}" class="url" /></label>
+ </div>
+ <div class="field-container message-field">
+ <span class="message"></span>
+ <span class="spinner"></span>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="dialog-button btn primary" data-button-id="ok" disabled>{{INSTALL}}</button>
+ <button class="dialog-button btn" data-button-id="cancel">{{CANCEL}}</button>
+ </div>
+</div>
View
606 src/extensibility/node/ExtensionManagerDomain.js
@@ -0,0 +1,606 @@
+/*
+ * 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, node: true, nomen: true,
+indent: 4, maxerr: 50 */
+
+"use strict";
+
+var unzip = require("unzip"),
+ semver = require("semver"),
+ path = require("path"),
+ http = require("http"),
+ request = require("request"),
+ fs = require("fs-extra");
+
+
+var Errors = {
+ NOT_FOUND_ERR: "NOT_FOUND_ERR",
+ INVALID_ZIP_FILE: "INVALID_ZIP_FILE", // {0} is path to ZIP file
+ INVALID_PACKAGE_JSON: "INVALID_PACKAGE_JSON", // {0} is JSON parse error, {1} is path to ZIP file
+ MISSING_PACKAGE_NAME: "MISSING_PACKAGE_NAME", // {0} is path to ZIP file
+ BAD_PACKAGE_NAME: "BAD_PACKAGE_NAME", // {0} is the name
+ MISSING_PACKAGE_VERSION: "MISSING_PACKAGE_VERSION", // {0} is path to ZIP file
+ INVALID_VERSION_NUMBER: "INVALID_VERSION_NUMBER", // {0} is version string in JSON, {1} is path to ZIP file
+ API_NOT_COMPATIBLE: "API_NOT_COMPATIBLE",
+ MISSING_MAIN: "MISSING_MAIN", // {0} is path to ZIP file
+ NO_DISABLED_DIRECTORY: "NO_DISABLED_DIRECTORY",
+ ALREADY_INSTALLED: "ALREADY_INSTALLED",
+ DOWNLOAD_ID_IN_USE: "DOWNLOAD_ID_IN_USE",
+ DOWNLOAD_TARGET_EXISTS: "DOWNLOAD_TARGET_EXISTS", // {0} is the download target file
+ BAD_HTTP_STATUS: "BAD_HTTP_STATUS", // {0} is the HTTP status code
+ NO_SERVER_RESPONSE: "NO_SERVER_RESPONSE",
+ CANCELED: "CANCELED"
+};
+
+/**
+ * Maps unique download ID to info about the pending download. No entry if download no longer pending.
+ * outStream is only present if we've started receiving the body.
+ * @type {Object.<string, {request:!http.ClientRequest, callback:!function(string, string), localPath:string, outStream:?fs.WriteStream}>}
+ */
+var pendingDownloads = {};
+
+/**
+ * Returns true if the name presented is acceptable as a package name. This enforces the
+ * requirement as presented in the CommonJS spec: http://wiki.commonjs.org/wiki/Packages/1.0
+ *
+ * @param {string} Name to test
+ * @return {boolean} true if the name is valid
+ */
+function validateName(name) {