summaryrefslogtreecommitdiffstats
path: root/devtools/client/projecteditor/lib/stores
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/projecteditor/lib/stores
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/projecteditor/lib/stores')
-rw-r--r--devtools/client/projecteditor/lib/stores/base.js58
-rw-r--r--devtools/client/projecteditor/lib/stores/local.js215
-rw-r--r--devtools/client/projecteditor/lib/stores/moz.build11
-rw-r--r--devtools/client/projecteditor/lib/stores/resource.js398
4 files changed, 682 insertions, 0 deletions
diff --git a/devtools/client/projecteditor/lib/stores/base.js b/devtools/client/projecteditor/lib/stores/base.js
new file mode 100644
index 000000000..ef9495c77
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/base.js
@@ -0,0 +1,58 @@
+/* -*- 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 { Cc, Ci, Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("promise");
+
+/**
+ * A Store object maintains a collection of Resource objects stored in a tree.
+ *
+ * The Store class should not be instantiated directly. Instead, you should
+ * use a class extending it - right now this is only a LocalStore.
+ *
+ * Events:
+ * This object emits the 'resource-added' and 'resource-removed' events.
+ */
+var Store = Class({
+ extends: EventTarget,
+
+ /**
+ * Should be called during initialize() of a subclass.
+ */
+ initStore: function () {
+ this.resources = new Map();
+ },
+
+ refresh: function () {
+ return promise.resolve();
+ },
+
+ /**
+ * Return a sorted Array of all Resources in the Store
+ */
+ allResources: function () {
+ var resources = [];
+ function addResource(resource) {
+ resources.push(resource);
+ resource.childrenSorted.forEach(addResource);
+ }
+ addResource(this.root);
+ return resources;
+ },
+
+ notifyAdd: function (resource) {
+ emit(this, "resource-added", resource);
+ },
+
+ notifyRemove: function (resource) {
+ emit(this, "resource-removed", resource);
+ }
+});
+
+exports.Store = Store;
diff --git a/devtools/client/projecteditor/lib/stores/local.js b/devtools/client/projecteditor/lib/stores/local.js
new file mode 100644
index 000000000..1f782dadf
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/local.js
@@ -0,0 +1,215 @@
+/* -*- 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 { Cc, Ci, Cu, ChromeWorker } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const { emit } = require("sdk/event/core");
+const { Store } = require("devtools/client/projecteditor/lib/stores/base");
+const { Task } = require("devtools/shared/task");
+const promise = require("promise");
+const Services = require("Services");
+const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
+const { FileResource } = require("devtools/client/projecteditor/lib/stores/resource");
+
+const CHECK_LINKED_DIRECTORY_DELAY = 5000;
+const SHOULD_LIVE_REFRESH = true;
+// XXX: Ignores should be customizable
+const IGNORE_REGEX = /(^\.)|(\~$)|(^node_modules$)/;
+
+/**
+ * A LocalStore object maintains a collection of Resource objects
+ * from the file system.
+ *
+ * This object emits the following events:
+ * - "resource-added": When a resource is added
+ * - "resource-removed": When a resource is removed
+ */
+var LocalStore = Class({
+ extends: Store,
+
+ defaultCategory: "js",
+
+ initialize: function(path) {
+ this.initStore();
+ this.path = OS.Path.normalize(path);
+ this.rootPath = this.path;
+ this.displayName = this.path;
+ this.root = this._forPath(this.path);
+ this.notifyAdd(this.root);
+ this.refreshLoop = this.refreshLoop.bind(this);
+ this.refreshLoop();
+ },
+
+ destroy: function() {
+ clearTimeout(this._refreshTimeout);
+
+ if (this._refreshDeferred) {
+ this._refreshDeferred.reject("destroy");
+ }
+ if (this.worker) {
+ this.worker.terminate();
+ }
+
+ this._refreshTimeout = null;
+ this._refreshDeferred = null;
+ this.worker = null;
+
+ if (this.root) {
+ forget(this, this.root);
+ this.root.destroy();
+ }
+ },
+
+ toString: function() { return "[LocalStore:" + this.path + "]" },
+
+ /**
+ * Return a FileResource object for the given path. If a FileInfo
+ * is provided the resource will use it, otherwise the FileResource
+ * might not have full information until the next refresh.
+ *
+ * The following parameters are passed into the FileResource constructor
+ * See resource.js for information about them
+ *
+ * @param String path
+ * @param FileInfo info
+ * @returns Resource
+ */
+ _forPath: function(path, info=null) {
+ if (this.resources.has(path)) {
+ return this.resources.get(path);
+ }
+
+ let resource = FileResource(this, path, info);
+ this.resources.set(path, resource);
+ return resource;
+ },
+
+ /**
+ * Return a promise that resolves to a fully-functional FileResource
+ * within this project. This will hit the disk for stat info.
+ * options:
+ *
+ * create: If true, a resource will be created even if the underlying
+ * file doesn't exist.
+ */
+ resourceFor: function(path, options) {
+ path = OS.Path.normalize(path);
+
+ if (this.resources.has(path)) {
+ return promise.resolve(this.resources.get(path));
+ }
+
+ if (!this.contains(path)) {
+ return promise.reject(new Error(path + " does not belong to " + this.path));
+ }
+
+ return Task.spawn(function*() {
+ let parent = yield this.resourceFor(OS.Path.dirname(path));
+
+ let info;
+ try {
+ info = yield OS.File.stat(path);
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ if (!options.create) {
+ throw ex;
+ }
+ }
+
+ let resource = this._forPath(path, info);
+ parent.addChild(resource);
+ return resource;
+ }.bind(this));
+ },
+
+ refreshLoop: function() {
+ // XXX: Once Bug 958280 adds a watch function, will not need to forever loop here.
+ this.refresh().then(() => {
+ if (SHOULD_LIVE_REFRESH) {
+ this._refreshTimeout = setTimeout(this.refreshLoop,
+ CHECK_LINKED_DIRECTORY_DELAY);
+ }
+ });
+ },
+
+ _refreshTimeout: null,
+ _refreshDeferred: null,
+
+ /**
+ * Refresh the directory structure.
+ */
+ refresh: function(path=this.rootPath) {
+ if (this._refreshDeferred) {
+ return this._refreshDeferred.promise;
+ }
+ this._refreshDeferred = promise.defer();
+
+ let worker = this.worker = new ChromeWorker("chrome://devtools/content/projecteditor/lib/helpers/readdir.js");
+ let start = Date.now();
+
+ worker.onmessage = evt => {
+ // console.log("Directory read finished in " + ( Date.now() - start ) +"ms", evt);
+ for (path in evt.data) {
+ let info = evt.data[path];
+ info.path = path;
+
+ let resource = this._forPath(path, info);
+ resource.info = info;
+ if (info.isDir) {
+ let newChildren = new Set();
+ for (let childPath of info.children) {
+ childInfo = evt.data[childPath];
+ newChildren.add(this._forPath(childPath, childInfo));
+ }
+ resource.setChildren(newChildren);
+ }
+ resource.info.children = null;
+ }
+
+ worker = null;
+ this._refreshDeferred.resolve();
+ this._refreshDeferred = null;
+ };
+ worker.onerror = ex => {
+ console.error(ex);
+ worker = null;
+ this._refreshDeferred.reject(ex);
+ this._refreshDeferred = null;
+ }
+ worker.postMessage({ path: this.rootPath, ignore: IGNORE_REGEX });
+ return this._refreshDeferred.promise;
+ },
+
+ /**
+ * Returns true if the given path would be a child of the store's
+ * root directory.
+ */
+ contains: function(path) {
+ path = OS.Path.normalize(path);
+ let thisPath = OS.Path.split(this.rootPath);
+ let thatPath = OS.Path.split(path)
+
+ if (!(thisPath.absolute && thatPath.absolute)) {
+ throw new Error("Contains only works with absolute paths.");
+ }
+
+ if (thisPath.winDrive && (thisPath.winDrive != thatPath.winDrive)) {
+ return false;
+ }
+
+ if (thatPath.components.length <= thisPath.components.length) {
+ return false;
+ }
+
+ for (let i = 0; i < thisPath.components.length; i++) {
+ if (thisPath.components[i] != thatPath.components[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+});
+exports.LocalStore = LocalStore;
diff --git a/devtools/client/projecteditor/lib/stores/moz.build b/devtools/client/projecteditor/lib/stores/moz.build
new file mode 100644
index 000000000..5a6becd92
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'base.js',
+ 'local.js',
+ 'resource.js',
+)
diff --git a/devtools/client/projecteditor/lib/stores/resource.js b/devtools/client/projecteditor/lib/stores/resource.js
new file mode 100644
index 000000000..53e3e7348
--- /dev/null
+++ b/devtools/client/projecteditor/lib/stores/resource.js
@@ -0,0 +1,398 @@
+/* -*- 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+const { TextEncoder, TextDecoder } = require("sdk/io/buffer");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const URL = require("sdk/url");
+const promise = require("promise");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const { Task } = require("devtools/shared/task");
+
+const gDecoder = new TextDecoder();
+const gEncoder = new TextEncoder();
+
+/**
+ * A Resource is a single file-like object that can be respresented
+ * as a file for ProjectEditor.
+ *
+ * The Resource class is not exported, and should not be instantiated
+ * Instead, you should use the FileResource class that extends it.
+ *
+ * This object emits the following events:
+ * - "children-changed": When a child has been added or removed.
+ * See setChildren.
+ * - "deleted": When the resource has been deleted.
+ */
+var Resource = Class({
+ extends: EventTarget,
+
+ refresh: function () { return promise.resolve(this); },
+ destroy: function () { },
+ delete: function () { },
+
+ setURI: function (uri) {
+ if (typeof (uri) === "string") {
+ uri = URL.URL(uri);
+ }
+ this.uri = uri;
+ },
+
+ /**
+ * Is there more than 1 child Resource?
+ */
+ get hasChildren() { return this.children && this.children.size > 0; },
+
+ /**
+ * Is this Resource the root (top level for the store)?
+ */
+ get isRoot() {
+ return !this.parent;
+ },
+
+ /**
+ * Sorted array of children for display
+ */
+ get childrenSorted() {
+ if (!this.hasChildren) {
+ return [];
+ }
+
+ return [...this.children].sort((a, b)=> {
+ // Put directories above files.
+ if (a.isDir !== b.isDir) {
+ return b.isDir;
+ }
+ return a.basename.toLowerCase() > b.basename.toLowerCase();
+ });
+ },
+
+ /**
+ * Set the children set of this Resource, and notify of any
+ * additions / removals that happened in the change.
+ */
+ setChildren: function (newChildren) {
+ let oldChildren = this.children || new Set();
+ let change = false;
+
+ for (let child of oldChildren) {
+ if (!newChildren.has(child)) {
+ change = true;
+ child.parent = null;
+ this.store.notifyRemove(child);
+ }
+ }
+
+ for (let child of newChildren) {
+ if (!oldChildren.has(child)) {
+ change = true;
+ child.parent = this;
+ this.store.notifyAdd(child);
+ }
+ }
+
+ this.children = newChildren;
+ if (change) {
+ emit(this, "children-changed", this);
+ }
+ },
+
+ /**
+ * Add a resource to children set and notify of the change.
+ *
+ * @param Resource resource
+ */
+ addChild: function (resource) {
+ this.children = this.children || new Set();
+
+ resource.parent = this;
+ this.children.add(resource);
+ this.store.notifyAdd(resource);
+ emit(this, "children-changed", this);
+ return resource;
+ },
+
+ /**
+ * Checks if current object has child with specific name.
+ *
+ * @param string name
+ */
+ hasChild: function (name) {
+ for (let child of this.children) {
+ if (child.basename === name) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Remove a resource to children set and notify of the change.
+ *
+ * @param Resource resource
+ */
+ removeChild: function (resource) {
+ resource.parent = null;
+ this.children.remove(resource);
+ this.store.notifyRemove(resource);
+ emit(this, "children-changed", this);
+ return resource;
+ },
+
+ /**
+ * Return a set with children, children of children, etc -
+ * gathered recursively.
+ *
+ * @returns Set<Resource>
+ */
+ allDescendants: function () {
+ let set = new Set();
+
+ function addChildren(item) {
+ if (!item.children) {
+ return;
+ }
+
+ for (let child of item.children) {
+ set.add(child);
+ }
+ }
+
+ addChildren(this);
+ for (let item of set) {
+ addChildren(item);
+ }
+
+ return set;
+ },
+});
+
+/**
+ * A FileResource is an implementation of Resource for a File System
+ * backing. This is exported, and should be used instead of Resource.
+ */
+var FileResource = Class({
+ extends: Resource,
+
+ /**
+ * @param Store store
+ * @param String path
+ * @param FileInfo info
+ * https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info
+ */
+ initialize: function (store, path, info) {
+ this.store = store;
+ this.path = path;
+
+ this.setURI(URL.URL(URL.fromFilename(path)));
+ this._lastReadModification = undefined;
+
+ this.info = info;
+ this.parent = null;
+ },
+
+ toString: function () {
+ return "[FileResource:" + this.path + "]";
+ },
+
+ destroy: function () {
+ if (this._refreshDeferred) {
+ this._refreshDeferred.reject();
+ }
+ this._refreshDeferred = null;
+ },
+
+ /**
+ * Fetch and cache information about this particular file.
+ * https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File_for_the_main_thread#OS.File.stat
+ *
+ * @returns Promise
+ * Resolves once the File.stat has finished.
+ */
+ refresh: function () {
+ if (this._refreshDeferred) {
+ return this._refreshDeferred.promise;
+ }
+ this._refreshDeferred = promise.defer();
+ OS.File.stat(this.path).then(info => {
+ this.info = info;
+ if (this._refreshDeferred) {
+ this._refreshDeferred.resolve(this);
+ this._refreshDeferred = null;
+ }
+ });
+ return this._refreshDeferred.promise;
+ },
+
+ /**
+ * Return the trailing name component of this Resource
+ */
+ get basename() {
+ return this.path.replace(/\/+$/, "").replace(/\\/g, "/").replace(/.*\//, "");
+ },
+
+ /**
+ * A string to be used when displaying this Resource in views
+ */
+ get displayName() {
+ return this.basename + (this.isDir ? "/" : "");
+ },
+
+ /**
+ * Is this FileResource a directory? Rather than checking children
+ * here, we use this.info. So this could return a false negative
+ * if there was no info passed in on constructor and the first
+ * refresh hasn't yet finished.
+ */
+ get isDir() {
+ if (!this.info) { return false; }
+ return this.info.isDir && !this.info.isSymLink;
+ },
+
+ /**
+ * Read the file as a string asynchronously.
+ *
+ * @returns Promise
+ * Resolves with the text of the file.
+ */
+ load: function () {
+ return OS.File.read(this.path).then(bytes => {
+ return gDecoder.decode(bytes);
+ });
+ },
+
+ /**
+ * Delete the file from the filesystem
+ *
+ * @returns Promise
+ * Resolves when the file is deleted
+ */
+ delete: function () {
+ emit(this, "deleted", this);
+ if (this.isDir) {
+ return OS.File.removeDir(this.path);
+ } else {
+ return OS.File.remove(this.path);
+ }
+ },
+
+ /**
+ * Add a text file as a child of this FileResource.
+ * This instance must be a directory.
+ *
+ * @param string name
+ * The filename (path will be generated based on this.path).
+ * string initial
+ * The content to write to the new file.
+ * @returns Promise
+ * Resolves with the new FileResource once it has
+ * been written to disk.
+ * Rejected if this is not a directory.
+ */
+ createChild: function (name, initial = "") {
+ if (!this.isDir) {
+ return promise.reject(new Error("Cannot add child to a regular file"));
+ }
+
+ let newPath = OS.Path.join(this.path, name);
+
+ let buffer = initial ? gEncoder.encode(initial) : "";
+ return OS.File.writeAtomic(newPath, buffer, {
+ noOverwrite: true
+ }).then(() => {
+ return this.store.refresh();
+ }).then(() => {
+ let resource = this.store.resources.get(newPath);
+ if (!resource) {
+ throw new Error("Error creating " + newPath);
+ }
+ return resource;
+ });
+ },
+
+ /**
+ * Rename the file from the filesystem
+ *
+ * @returns Promise
+ * Resolves with the renamed FileResource.
+ */
+ rename: function (oldName, newName) {
+ let oldPath = OS.Path.join(this.path, oldName);
+ let newPath = OS.Path.join(this.path, newName);
+
+ return OS.File.move(oldPath, newPath).then(() => {
+ return this.store.refresh();
+ }).then(() => {
+ let resource = this.store.resources.get(newPath);
+ if (!resource) {
+ throw new Error("Error creating " + newPath);
+ }
+ return resource;
+ });
+ },
+
+ /**
+ * Write a string to this file.
+ *
+ * @param string content
+ * @returns Promise
+ * Resolves once it has been written to disk.
+ * Rejected if there is an error
+ */
+ save: Task.async(function* (content) {
+ // XXX: writeAtomic was losing permissions after saving on OSX
+ // return OS.File.writeAtomic(this.path, buffer, { tmpPath: this.path + ".tmp" });
+ let buffer = gEncoder.encode(content);
+ let path = this.path;
+ let file = yield OS.File.open(path, {truncate: true});
+ yield file.write(buffer);
+ yield file.close();
+ }),
+
+ /**
+ * Attempts to get the content type from the file.
+ */
+ get contentType() {
+ if (this._contentType) {
+ return this._contentType;
+ }
+ if (this.isDir) {
+ return "x-directory/normal";
+ }
+ try {
+ this._contentType = mimeService.getTypeFromFile(new FileUtils.File(this.path));
+ } catch (ex) {
+ if (ex.name !== "NS_ERROR_NOT_AVAILABLE" &&
+ ex.name !== "NS_ERROR_FAILURE") {
+ console.error(ex, this.path);
+ }
+ this._contentType = null;
+ }
+ return this._contentType;
+ },
+
+ /**
+ * A string used when determining the type of Editor to open for this.
+ * See editors.js -> EditorTypeForResource.
+ */
+ get contentCategory() {
+ const NetworkHelper = require("devtools/shared/webconsole/network-helper");
+ let category = NetworkHelper.mimeCategoryMap[this.contentType];
+ // Special treatment for manifest.webapp.
+ if (!category && this.basename === "manifest.webapp") {
+ return "json";
+ }
+ return category || "txt";
+ }
+});
+
+exports.FileResource = FileResource;