diff --git a/src/extensibility/ExtensionManagerDialog.js b/src/extensibility/ExtensionManagerDialog.js index 64d4b712ec0..308810900b4 100644 --- a/src/extensibility/ExtensionManagerDialog.js +++ b/src/extensibility/ExtensionManagerDialog.js @@ -321,6 +321,7 @@ define(function (require, exports, module) { } models.push(new ExtensionManagerViewModel.InstalledViewModel()); + models.push(new ExtensionManagerViewModel.DefaultViewModel()); function updateSearchDisabled() { var model = models[_activeTabIndex], diff --git a/src/extensibility/ExtensionManagerView.js b/src/extensibility/ExtensionManagerView.js index 2a981ff6c68..e6b25659b34 100644 --- a/src/extensibility/ExtensionManagerView.js +++ b/src/extensibility/ExtensionManagerView.js @@ -336,10 +336,10 @@ define(function (require, exports, module) { context.removalAllowed = this.model.source === "installed" && !context.failedToStart && !hasPendingAction; - context.disablingAllowed = this.model.source === "installed" && - !context.disabled && !hasPendingAction; - context.enablingAllowed = this.model.source === "installed" && - context.disabled && !hasPendingAction; + var isDefaultOrInstalled = this.model.source === "default" || this.model.source === "installed"; + var isDefaultAndTheme = this.model.source === "default" && context.metadata.theme; + context.disablingAllowed = isDefaultOrInstalled && !isDefaultAndTheme && !context.disabled && !hasPendingAction; + context.enablingAllowed = isDefaultOrInstalled && !isDefaultAndTheme && context.disabled && !hasPendingAction; // Copy over helper functions that we share with the registry app. ["lastVersionDate", "authorInfo"].forEach(function (helper) { diff --git a/src/extensibility/ExtensionManagerViewModel.js b/src/extensibility/ExtensionManagerViewModel.js index cb86d1327ee..73577e8fa90 100644 --- a/src/extensibility/ExtensionManagerViewModel.js +++ b/src/extensibility/ExtensionManagerViewModel.js @@ -80,6 +80,12 @@ define(function (require, exports, module) { */ ExtensionManagerViewModel.prototype.SOURCE_INSTALLED = "installed"; + /** + * @type {string} + * Constant indicating that this model/view should initialize from the list of default bundled extensions. + */ + ExtensionManagerViewModel.prototype.SOURCE_DEFAULT = "default"; + /** * @type {Object} * The current set of extensions managed by this model. Same as ExtensionManager.extensions. @@ -509,6 +515,65 @@ define(function (require, exports, module) { return entry; }; + /** + * Model for displaying default extensions that come bundled with Brackets + */ + function DefaultViewModel() { + ExtensionManagerViewModel.call(this); + } + + // Inheritance setup + DefaultViewModel.prototype = Object.create(ExtensionManagerViewModel.prototype); + DefaultViewModel.prototype.constructor = DefaultViewModel; + + /** + * Add SOURCE_DEFAULT to DefaultViewModel + */ + DefaultViewModel.prototype.source = ExtensionManagerViewModel.prototype.SOURCE_DEFAULT; + + /** + * Initializes the model from the set of default extensions, sorted alphabetically by id + * @return {$.Promise} a promise that's resolved when we're done initializing. + */ + DefaultViewModel.prototype._initializeFromSource = function () { + var self = this; + this.extensions = ExtensionManager.extensions; + this.sortedFullSet = Object.keys(this.extensions) + .filter(function (key) { + return self.extensions[key].installInfo && + self.extensions[key].installInfo.locationType === ExtensionManager.LOCATION_DEFAULT; + }); + this._sortFullSet(); + this._setInitialFilter(); + return new $.Deferred().resolve().promise(); + }; + + /** + * @private + * Re-sorts the current full set + */ + DefaultViewModel.prototype._sortFullSet = function () { + var self = this; + this.sortedFullSet = this.sortedFullSet.sort(function (key1, key2) { + var metadata1 = self.extensions[key1].installInfo.metadata, + metadata2 = self.extensions[key2].installInfo.metadata, + id1 = (metadata1.title || metadata1.name).toLocaleLowerCase(), + id2 = (metadata2.title || metadata2.name).toLocaleLowerCase(); + return id1.localeCompare(id2); + }); + }; + + /** + * @private + * Finds the default extension metadata by id. If there is no default extension matching the given id, + * this returns `null`. + * @param {string} id of the theme extension + * @return {Object?} extension metadata or null if there's no matching extension + */ + DefaultViewModel.prototype._getEntry = function (id) { + return this.extensions[id] ? this.extensions[id].installInfo : null; + }; + /** * The model for the ExtensionManagerView that is responsible for handling registry-based theme extensions. * This extends ExtensionManagerViewModel. @@ -573,4 +638,5 @@ define(function (require, exports, module) { exports.RegistryViewModel = RegistryViewModel; exports.ThemesViewModel = ThemesViewModel; exports.InstalledViewModel = InstalledViewModel; + exports.DefaultViewModel = DefaultViewModel; }); \ No newline at end of file diff --git a/src/extensibility/Package.js b/src/extensibility/Package.js index 721ef3a2d05..d49647748a1 100644 --- a/src/extensibility/Package.js +++ b/src/extensibility/Package.js @@ -43,6 +43,11 @@ define(function (require, exports, module) { description: Strings.DESCRIPTION_PROXY }); + var PREF_EXTENSIONS_DEFAULT_DISABLED = "extensions.default.disabled"; + PreferencesManager.definePreference(PREF_EXTENSIONS_DEFAULT_DISABLED, "array", [], { + description: Strings.DESCRIPTION_DISABLED_DEFAULT_EXTENSIONS + }); + var Errors = { ERROR_LOADING: "ERROR_LOADING", MALFORMED_URL: "MALFORMED_URL", @@ -436,6 +441,23 @@ define(function (require, exports, module) { }); } + /** + * This function manages the PREF_EXTENSIONS_DEFAULT_DISABLED preference + * holding an array of default extension paths that should not be loaded + * on Brackets startup + */ + function toggleDefaultExtension(path, enabled) { + var arr = PreferencesManager.get(PREF_EXTENSIONS_DEFAULT_DISABLED); + if (!Array.isArray(arr)) { arr = []; } + var io = arr.indexOf(path); + if (enabled === true && io !== -1) { + arr.splice(io, 1); + } else if (enabled === false && io === -1) { + arr.push(path); + } + PreferencesManager.set(PREF_EXTENSIONS_DEFAULT_DISABLED, arr); + } + /** * Disables the extension at the given path. * @@ -446,12 +468,19 @@ define(function (require, exports, module) { function disable(path) { var result = new $.Deferred(), file = FileSystem.getFileForPath(path + "/.disabled"); + + var defaultExtensionPath = ExtensionLoader.getDefaultExtensionPath(); + if (file.fullPath.indexOf(defaultExtensionPath) === 0) { + toggleDefaultExtension(path, false); + result.resolve(); + return result.promise(); + } + file.write("", function (err) { if (err) { - result.reject(err); - } else { - result.resolve(); + return result.reject(err); } + result.resolve(); }); return result.promise(); } @@ -466,14 +495,25 @@ define(function (require, exports, module) { function enable(path) { var result = new $.Deferred(), file = FileSystem.getFileForPath(path + "/.disabled"); - file.unlink(function (err) { - if (err) { - result.reject(err); - return; - } + + function afterEnable() { ExtensionLoader.loadExtension(FileUtils.getBaseName(path), { baseUrl: path }, "main") .done(result.resolve) .fail(result.reject); + } + + var defaultExtensionPath = ExtensionLoader.getDefaultExtensionPath(); + if (file.fullPath.indexOf(defaultExtensionPath) === 0) { + toggleDefaultExtension(path, true); + afterEnable(); + return result.promise(); + } + + file.unlink(function (err) { + if (err) { + return result.reject(err); + } + afterEnable(); }); return result.promise(); } diff --git a/src/htmlContent/extension-manager-dialog.html b/src/htmlContent/extension-manager-dialog.html index f222edae186..6ba0197ecc1 100644 --- a/src/htmlContent/extension-manager-dialog.html +++ b/src/htmlContent/extension-manager-dialog.html @@ -6,7 +6,21 @@