/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { Cu } = require("chrome"); const { Class } = require("sdk/core/heritage"); const { EventTarget } = require("sdk/event/target"); const { emit } = require("sdk/event/core"); const { scope, on, forget } = require("devtools/client/projecteditor/lib/helpers/event"); const prefs = require("sdk/preferences/service"); const { LocalStore } = require("devtools/client/projecteditor/lib/stores/local"); const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {}); const { Task } = require("devtools/shared/task"); const promise = require("promise"); const { TextEncoder, TextDecoder } = require("sdk/io/buffer"); const url = require("sdk/url"); const gDecoder = new TextDecoder(); const gEncoder = new TextEncoder(); /** * A Project keeps track of the opened folders using LocalStore * objects. Resources are generally requested from the project, * even though the Store is actually keeping track of them. * * * This object emits the following events: * - "refresh-complete": After all stores have been refreshed from disk. * - "store-added": When a store has been added to the project. * - "store-removed": When a store has been removed from the project. * - "resource-added": When a resource has been added to one of the stores. * - "resource-removed": When a resource has been removed from one of the stores. */ var Project = Class({ extends: EventTarget, /** * Intialize the Project. * * @param Object options * Options to be passed into Project.load function */ initialize: function (options) { this.localStores = new Map(); this.load(options); }, destroy: function () { // We are removing the store because the project never gets persisted. // There may need to be separate destroy functionality that doesn't remove // from project if this is saved to DB. this.removeAllStores(); }, toString: function () { return "[Project] " + this.name; }, /** * Load a project given metadata about it. * * @param Object options * Information about the project, containing: * id: An ID (currently unused, but could be used for saving) * name: The display name of the project * directories: An array of path strings to load */ load: function (options) { this.id = options.id; this.name = options.name || "Untitled"; let paths = new Set(options.directories.map(name => OS.Path.normalize(name))); for (let [path, store] of this.localStores) { if (!paths.has(path)) { this.removePath(path); } } for (let path of paths) { this.addPath(path); } }, /** * Refresh all project stores from disk * * @returns Promise * A promise that resolves when everything has been refreshed. */ refresh: function () { return Task.spawn(function* () { for (let [path, store] of this.localStores) { yield store.refresh(); } emit(this, "refresh-complete"); }.bind(this)); }, /** * Fetch a resource from the backing storage system for the store. * * @param string path * The path to fetch * @param Object options * "create": bool indicating whether to create a file if it does not exist. * @returns Promise * A promise that resolves with the Resource. */ resourceFor: function (path, options) { let store = this.storeContaining(path); return store.resourceFor(path, options); }, /** * Get every resource used inside of the project. * * @returns Array * A list of all Resources in all Stores. */ allResources: function () { let resources = []; for (let store of this.allStores()) { resources = resources.concat(store.allResources()); } return resources; }, /** * Get every Path used inside of the project. * * @returns generator-iterator * A list of all Stores */ allStores: function* () { for (let [path, store] of this.localStores) { yield store; } }, /** * Get every file path used inside of the project. * * @returns Array * A list of all file paths */ allPaths: function () { return [...this.localStores.keys()]; }, /** * Get the store that contains a path. * * @returns Store * The store, if any. Will return null if no store * contains the given path. */ storeContaining: function (path) { let containingStore = null; for (let store of this.allStores()) { if (store.contains(path)) { // With nested projects, the final containing store will be returned. containingStore = store; } } return containingStore; }, /** * Add a store at the current path. If a store already exists * for this path, then return it. * * @param string path * @returns LocalStore */ addPath: function (path) { if (!this.localStores.has(path)) { this.addLocalStore(new LocalStore(path)); } return this.localStores.get(path); }, /** * Remove a store for a given path. * * @param string path */ removePath: function (path) { this.removeLocalStore(this.localStores.get(path)); }, /** * Add the given Store to the project. * Fires a 'store-added' event on the project. * * @param Store store */ addLocalStore: function (store) { store.canPair = true; this.localStores.set(store.path, store); // Originally StoreCollection.addStore on(this, store, "resource-added", (resource) => { emit(this, "resource-added", resource); }); on(this, store, "resource-removed", (resource) => { emit(this, "resource-removed", resource); }); emit(this, "store-added", store); }, /** * Remove all of the Stores belonging to the project. */ removeAllStores: function () { for (let store of this.allStores()) { this.removeLocalStore(store); } }, /** * Remove the given Store from the project. * Fires a 'store-removed' event on the project. * * @param Store store */ removeLocalStore: function (store) { // XXX: tree selection should be reset if active element is affected by // the store being removed if (store) { this.localStores.delete(store.path); forget(this, store); emit(this, "store-removed", store); store.destroy(); } } }); exports.Project = Project;