diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/projecteditor/lib/stores | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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.js | 58 | ||||
-rw-r--r-- | devtools/client/projecteditor/lib/stores/local.js | 215 | ||||
-rw-r--r-- | devtools/client/projecteditor/lib/stores/moz.build | 11 | ||||
-rw-r--r-- | devtools/client/projecteditor/lib/stores/resource.js | 398 |
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; |