Bug 987089 - Land ProjectEditor in browser/devtools part 1;r=paul

This commit is contained in:
Brian Grinstead 2014-05-20 12:25:16 -05:00
parent 385e12261f
commit bd42dd8121
51 changed files with 4618 additions and 0 deletions

View File

@ -7,6 +7,11 @@ browser.jar:
content/browser/devtools/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml)
content/browser/devtools/markup-view.css (markupview/markup-view.css)
content/browser/devtools/projecteditor.xul (projecteditor/chrome/content/projecteditor.xul)
content/browser/devtools/readdir.js (projecteditor/lib/helpers/readdir.js)
content/browser/devtools/projecteditor-loader.xul (projecteditor/chrome/content/projecteditor-loader.xul)
content/browser/devtools/projecteditor-test.html (projecteditor/chrome/content/projecteditor-test.html)
content/browser/devtools/projecteditor-loader.js (projecteditor/chrome/content/projecteditor-loader.js)
content/browser/devtools/netmonitor.xul (netmonitor/netmonitor.xul)
content/browser/devtools/netmonitor.css (netmonitor/netmonitor.css)
content/browser/devtools/netmonitor-controller.js (netmonitor/netmonitor-controller.js)

View File

@ -13,6 +13,7 @@ DIRS += [
'fontinspector',
'framework',
'inspector',
'projecteditor',
'layoutview',
'markupview',
'netmonitor',

View File

@ -0,0 +1,14 @@
# 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/.
projecteditor_lib_FILES = $(wildcard $(srcdir)/lib/*)
projecteditor_lib_DEST = $(FINAL_TARGET)/modules/devtools/projecteditor
INSTALL_TARGETS += projecteditor_lib
# To copy the sample directory into modules/devtools/projecteditor
# projecteditor_sample_FILES = $(wildcard $(srcdir)/test/samples/*)
# projecteditor_sample_DEST = $(FINAL_TARGET)/modules/devtools/projecteditor/samples
# INSTALL_TARGETS += projecteditor_sample
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,157 @@
const Cu = Components.utils;
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const require = devtools.require;
const promise = require("projecteditor/helpers/promise");
const ProjectEditor = require("projecteditor/projecteditor");
const SAMPLE_PATH = buildTempDirectoryStructure();
const SAMPLE_NAME = "DevTools Content";
const SAMPLE_PROJECT_URL = "http://mozilla.org";
const SAMPLE_ICON = "chrome://browser/skin/devtools/tool-options.svg";
/**
* Create a workspace for working on projecteditor, available at
* chrome://browser/content/devtools/projecteditor-loader.xul.
* This emulates the integration points that the app manager uses.
*/
document.addEventListener("DOMContentLoaded", function onDOMReady(e) {
document.removeEventListener("DOMContentLoaded", onDOMReady, false);
let iframe = document.getElementById("projecteditor-iframe");
window.projecteditor = ProjectEditor.ProjectEditor(iframe);
projecteditor.on("onEditorCreated", (editor) => {
console.log("editor created: " + editor);
});
projecteditor.on("onEditorDestroyed", (editor) => {
console.log("editor destroyed: " + editor);
});
projecteditor.on("onEditorSave", (editor, resource) => {
console.log("editor saved: " + editor, resource.path);
});
projecteditor.on("onTreeSelected", (resource) => {
console.log("tree selected: " + resource.path);
});
projecteditor.on("onEditorLoad", (editor) => {
console.log("editor loaded: " + editor);
});
projecteditor.on("onEditorActivated", (editor) => {
console.log("editor focused: " + editor);
});
projecteditor.on("onEditorDeactivated", (editor) => {
console.log("editor blur: " + editor);
});
projecteditor.on("onEditorChange", (editor) => {
console.log("editor changed: " + editor);
});
projecteditor.on("onEditorCursorActivity", (editor) => {
console.log("editor cursor activity: " + editor);
});
projecteditor.on("onCommand", (cmd) => {
console.log("Command: " + cmd);
});
projecteditor.loaded.then(() => {
projecteditor.setProjectToAppPath(SAMPLE_PATH, {
name: SAMPLE_NAME,
iconUrl: SAMPLE_ICON,
projectOverviewURL: SAMPLE_PROJECT_URL
}).then(() => {
let allResources = projecteditor.project.allResources();
console.log("All resources have been loaded", allResources, allResources.map(r=>r.basename).join("|"));
});
});
}, false);
/**
* Build a temporary directory as a workspace for this loader
* https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
*/
function buildTempDirectoryStructure() {
// First create (and remove) the temp dir to discard any changes
let TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
TEMP_DIR.remove(true);
// Now rebuild our fake project.
TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "css"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "data"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "img", "icons"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "js"], true);
let htmlFile = FileUtils.getFile("TmpD", ["ProjectEditor", "index.html"]);
htmlFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(htmlFile, [
'<!DOCTYPE html>',
'<html lang="en">',
' <head>',
' <meta charset="utf-8" />',
' <title>ProjectEditor Temp File</title>',
' <link rel="stylesheet" href="style.css" />',
' </head>',
' <body id="home">',
' <p>ProjectEditor Temp File</p>',
' </body>',
'</html>'].join("\n")
);
let readmeFile = FileUtils.getFile("TmpD", ["ProjectEditor", "README.md"]);
readmeFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(readmeFile, [
'## Readme'
].join("\n")
);
let licenseFile = FileUtils.getFile("TmpD", ["ProjectEditor", "LICENSE"]);
licenseFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(licenseFile, [
'/* 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/. */'
].join("\n")
);
let cssFile = FileUtils.getFile("TmpD", ["ProjectEditor", "css", "styles.css"]);
cssFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(cssFile, [
'body {',
' background: red;',
'}'
].join("\n")
);
FileUtils.getFile("TmpD", ["ProjectEditor", "js", "script.js"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "fake.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "16x16.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "32x32.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "128x128.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "vector.svg"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
return TEMP_DIR.path;
}
// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
function writeToFile(file, data) {
let defer = promise.defer();
var ostream = FileUtils.openSafeFileOutputStream(file)
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var istream = converter.convertToInputStream(data);
// The last argument (the callback) is optional.
NetUtil.asyncCopy(istream, ostream, function(status) {
if (!Components.isSuccessCode(status)) {
// Handle error!
console.log("ERROR WRITING TEMP FILE", status);
}
});
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!DOCTYPE window [
<!ENTITY % toolboxDTD SYSTEM "chrome://browser/locale/devtools/toolbox.dtd" >
%toolboxDTD;
]>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript;version=1.8" src="projecteditor-loader.js"></script>
<commandset id="toolbox-commandset">
<command id="projecteditor-cmd-close" oncommand="window.close();"/>
</commandset>
<keyset id="projecteditor-keyset">
<key id="projecteditor-key-close"
key="&closeCmd.key;"
command="projecteditor-cmd-close"
modifiers="accel"/>
</keyset>
<iframe id="projecteditor-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
</window>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<head>
<meta charset='utf-8' />
</head>
<body>
<style type="text/css">
html { height: 100%; }
body {display: flex; padding: 0; margin: 0; min-height: 100%; }
iframe {flex: 1; border: 0;}
</style>
<iframe id='projecteditor-iframe'></iframe>
</body>
</html>

View File

@ -0,0 +1,88 @@
<?xml version="1.0"?>
<!-- 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/. -->
<?xml-stylesheet href="chrome://browser/skin/devtools/light-theme.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/projecteditor/projecteditor.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/debugger.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/markup-view.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/markup-view.css" type="text/css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<!DOCTYPE window [
<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
%scratchpadDTD;
<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
%editMenuStrings;
<!ENTITY % sourceEditorStrings SYSTEM "chrome://browser/locale/devtools/sourceeditor.dtd">
%sourceEditorStrings;
]>
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="theme-body">
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
<commandset id="projecteditor-commandset" />
<commandset id="editMenuCommands"/>
<keyset id="projecteditor-keyset" />
<keyset id="editMenuKeys"/>
<!-- Eventually we want to let plugins declare their own menu items.
Wait unti app manager lands to deal with this integration point.
-->
<menubar id="projecteditor-menubar">
<menu id="file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
<menupopup id="file-menu-popup" />
</menu>
<menu id="edit-menu" label="&editMenu.label;"
accesskey="&editMenu.accesskey;">
<menupopup id="edit-menu-popup">
<menuitem id="menu_undo"/>
<menuitem id="menu_redo"/>
<menuseparator/>
<menuitem id="menu_cut"/>
<menuitem id="menu_copy"/>
<menuitem id="menu_paste"/>
<menuseparator/>
<menuitem id="menu_selectAll"/>
<menuseparator/>
<menuitem id="menu_find"/>
<menuitem id="menu_findAgain"/>
</menupopup>
</menu>
</menubar>
<popupset>
<menupopup id="directory-menu-popup">
</menupopup>
</popupset>
<deck id="main-deck" flex="1">
<vbox flex="1" id="source-deckitem">
<hbox id="sources-body" flex="1">
<vbox width="250">
<vbox id="sources" flex="1">
</vbox>
<toolbar id="project-toolbar" class="devtools-toolbar" hidden="true"></toolbar>
</vbox>
<splitter id="source-editor-splitter" class="devtools-side-splitter"/>
<vbox id="shells" flex="4">
<toolbar id="projecteditor-toolbar" class="devtools-toolbar">
<hbox id="plugin-toolbar-left"/>
<spacer flex="1"/>
<hbox id="plugin-toolbar-right"/>
</toolbar>
<box id="shells-deck-container" flex="4"></box>
<toolbar id="projecteditor-toolbar-bottom" class="devtools-toolbar">
</toolbar>
</vbox>
</hbox>
</vbox>
</deck>
</page>

View File

@ -0,0 +1,263 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 promise = require("projecteditor/helpers/promise");
const Editor = require("devtools/sourceeditor/editor");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
/**
* ItchEditor is extended to implement an editor, which is the main view
* that shows up when a file is selected. This object should not be used
* directly - use TextEditor for a basic code editor.
*/
var ItchEditor = Class({
extends: EventTarget,
/**
* A boolean specifying if the toolbar above the editor should be hidden.
*/
hidesToolbar: false,
toString: function() {
return this.label || "";
},
emit: function(name, ...args) {
emit(this, name, ...args);
},
/**
* Initialize the editor with a single document. This should be called
* by objects extending this object with:
* ItchEditor.prototype.initialize.apply(this, arguments)
*/
initialize: function(document) {
this.doc = document;
this.label = "";
this.elt = this.doc.createElement("vbox");
this.elt.setAttribute("flex", "1");
this.elt.editor = this;
this.toolbar = this.doc.querySelector("#projecteditor-toolbar");
},
/**
* Sets the visibility of the element that shows up above the editor
* based on the this.hidesToolbar property.
*/
setToolbarVisibility: function() {
if (this.hidesToolbar) {
this.toolbar.setAttribute("hidden", "true");
} else {
this.toolbar.removeAttribute("hidden");
}
},
/**
* Load a single resource into the editor.
*
* @param Resource resource
* The single file / item that is being dealt with (see stores/base)
* @returns Promise
* A promise that is resolved once the editor has loaded the contents
* of the resource.
*/
load: function(resource) {
return promise.resolve();
},
/**
* Clean up the editor. This can have different meanings
* depending on the type of editor.
*/
destroy: function() {
},
/**
* Give focus to the editor. This can have different meanings
* depending on the type of editor.
*
* @returns Promise
* A promise that is resolved once the editor has been focused.
*/
focus: function() {
return promise.resolve();
}
});
exports.ItchEditor = ItchEditor;
/**
* The main implementation of the ItchEditor class. The TextEditor is used
* when editing any sort of plain text file, and can be created with different
* modes for syntax highlighting depending on the language.
*/
var TextEditor = Class({
extends: ItchEditor,
/**
* Extra keyboard shortcuts to use with the editor. Shortcuts defined
* within projecteditor should be triggered when they happen in the editor, and
* they would usually be swallowed without registering them.
* See "devtools/sourceeditor/editor" for more information.
*/
get extraKeys() {
let extraKeys = {};
// Copy all of the registered keys into extraKeys object, to notify CodeMirror
// that it should be ignoring these keys
[...this.doc.querySelectorAll("#projecteditor-keyset key")].forEach((key) => {
let keyUpper = key.getAttribute("key").toUpperCase();
let toolModifiers = key.getAttribute("modifiers");
let modifiers = {
alt: toolModifiers.contains("alt"),
shift: toolModifiers.contains("shift")
};
// On the key press, we will dispatch the event within projecteditor.
extraKeys[Editor.accel(keyUpper, modifiers)] = () => {
let event = this.doc.createEvent('Event');
event.initEvent('command', true, true);
let command = this.doc.querySelector("#" + key.getAttribute("command"));
command.dispatchEvent(event);
};
});
return extraKeys;
},
initialize: function(document, mode=Editor.modes.text) {
ItchEditor.prototype.initialize.apply(this, arguments);
this.label = mode.name;
this.editor = new Editor({
mode: mode,
lineNumbers: true,
extraKeys: this.extraKeys,
themeSwitching: false
});
// Trigger editor specific events on `this`
this.editor.on("change", (...args) => {
this.emit("change", ...args);
});
this.editor.on("cursorActivity", (...args) => {
this.emit("cursorActivity", ...args);
});
this.appended = this.editor.appendTo(this.elt);
},
/**
* Clean up the editor. This can have different meanings
* depending on the type of editor.
*/
destroy: function() {
this.editor.destroy();
this.editor = null;
},
/**
* Load a single resource into the text editor.
*
* @param Resource resource
* The single file / item that is being dealt with (see stores/base)
* @returns Promise
* A promise that is resolved once the text editor has loaded the
* contents of the resource.
*/
load: function(resource) {
// Wait for the editor.appendTo and resource.load before proceeding.
// They can run in parallel.
return promise.all([
resource.load(),
this.appended
]).then(([resourceContents])=> {
this.editor.setText(resourceContents);
this.editor.setClean();
this.emit("load");
}, console.error);
},
/**
* Save the resource based on the current state of the editor
*
* @param Resource resource
* The single file / item to be saved
* @returns Promise
* A promise that is resolved once the resource has been
* saved.
*/
save: function(resource) {
return resource.save(this.editor.getText()).then(() => {
this.editor.setClean();
this.emit("save", resource);
});
},
/**
* Give focus to the code editor.
*
* @returns Promise
* A promise that is resolved once the editor has been focused.
*/
focus: function() {
return this.appended.then(() => {
this.editor.focus();
});
}
});
/**
* Wrapper for TextEditor using JavaScript syntax highlighting.
*/
function JSEditor(document) {
return TextEditor(document, Editor.modes.js);
}
/**
* Wrapper for TextEditor using CSS syntax highlighting.
*/
function CSSEditor(document) {
return TextEditor(document, Editor.modes.css);
}
/**
* Wrapper for TextEditor using HTML syntax highlighting.
*/
function HTMLEditor(document) {
return TextEditor(document, Editor.modes.html);
}
/**
* Get the type of editor that can handle a particular resource.
* @param Resource resource
* The single file that is going to be opened.
* @returns Type:Editor
* The type of editor that can handle this resource. The
* return value is a constructor function.
*/
function EditorTypeForResource(resource) {
const categoryMap = {
"txt": TextEditor,
"html": HTMLEditor,
"xml": HTMLEditor,
"css": CSSEditor,
"js": JSEditor,
"json": JSEditor
};
return categoryMap[resource.contentCategory] || TextEditor;
}
exports.TextEditor = TextEditor;
exports.JSEditor = JSEditor;
exports.CSSEditor = CSSEditor;
exports.HTMLEditor = HTMLEditor;
exports.EditorTypeForResource = EditorTypeForResource;

View File

@ -0,0 +1,86 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
/**
* This file wraps EventEmitter objects to provide functions to forget
* all events bound on a certain object.
*/
const { Class } = require("sdk/core/heritage");
/**
* The Scope object is used to keep track of listeners.
* This object is not exported.
*/
var Scope = Class({
on: function(target, event, handler) {
this.listeners = this.listeners || [];
this.listeners.push({
target: target,
event: event,
handler: handler
});
target.on(event, handler);
},
off: function(t, e, h) {
if (!this.listeners) return;
this.listeners = this.listeners.filter(({ target, event, handler }) => {
return !(target === t && event === e && handler === h);
});
target.off(event, handler);
},
clear: function(clearTarget) {
if (!this.listeners) return;
this.listeners = this.listeners.filter(({ target, event, handler }) => {
if (target === clearTarget) {
target.off(event, handler);
return false;
}
return true;
});
},
destroy: function() {
if (!this.listeners) return;
this.listeners.forEach(({ target, event, handler }) => {
target.off(event, handler);
});
this.listeners = undefined;
}
});
var scopes = new WeakMap();
function scope(owner) {
if (!scopes.has(owner)) {
let scope = new Scope(owner);
scopes.set(owner, scope);
return scope;
}
return scopes.get(owner);
}
exports.scope = scope;
exports.on = function on(owner, target, event, handler) {
if (!target) return;
scope(owner).on(target, event, handler);
}
exports.off = function off(owner, target, event, handler) {
if (!target) return;
scope(owner).off(target, event, handler);
}
exports.forget = function forget(owner, target) {
scope(owner).clear(target);
}
exports.done = function done(owner) {
scope(owner).destroy();
scopes.delete(owner);
}

View File

@ -0,0 +1,116 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
/**
* This file contains helper functions for showing OS-specific
* file and folder pickers.
*/
const { Cu, Cc, Ci } = require("chrome");
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
const promise = require("projecteditor/helpers/promise");
const { merge } = require("sdk/util/object");
const { getLocalizedString } = require("projecteditor/helpers/l10n");
/**
* Show a file / folder picker.
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
*
* @param object options
* Additional options for setting the source. Supported options:
* - directory: string, The path to default opening
* - defaultName: string, The filename including extension that
* should be suggested to the user as a default
* - window: DOMWindow, The filename including extension that
* should be suggested to the user as a default
* - title: string, The filename including extension that
* should be suggested to the user as a default
* - mode: int, The type of picker to open.
*
* @return promise
* A promise that is resolved with the full path
* after the file has been picked.
*/
function showPicker(options) {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
if (options.directory) {
try {
fp.displayDirectory = FileUtils.File(options.directory);
} catch(ex) {
console.warn(ex);
}
}
if (options.defaultName) {
fp.defaultString = options.defaultName;
}
fp.init(options.window, options.title, options.mode);
let deferred = promise.defer();
fp.open({
done: function(res) {
if (res === Ci.nsIFilePicker.returnOK || res === Ci.nsIFilePicker.returnReplace) {
deferred.resolve(fp.file.path);
} else {
deferred.reject();
}
}
});
return deferred.promise;
}
exports.showPicker = showPicker;
/**
* Show a save dialog
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
*
* @param object options
* Additional options as specified in showPicker
*
* @return promise
* A promise that is resolved when the save dialog has closed
*/
function showSave(options) {
return showPicker(merge({
title: getLocalizedString("projecteditor.selectFileLabel"),
mode: Ci.nsIFilePicker.modeSave
}, options));
}
exports.showSave = showSave;
/**
* Show a file open dialog
*
* @param object options
* Additional options as specified in showPicker
*
* @return promise
* A promise that is resolved when the file has been opened
*/
function showOpen(options) {
return showPicker(merge({
title: getLocalizedString("projecteditor.openFileLabel"),
mode: Ci.nsIFilePicker.modeOpen
}, options));
}
exports.showOpen = showOpen;
/**
* Show a folder open dialog
*
* @param object options
* Additional options as specified in showPicker
*
* @return promise
* A promise that is resolved when the folder has been opened
*/
function showOpenFolder(options) {
return showPicker(merge({
title: getLocalizedString("projecteditor.openFolderLabel"),
mode: Ci.nsIFilePicker.modeGetFolder
}, options));
}
exports.showOpenFolder = showOpenFolder;

View File

@ -0,0 +1,25 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
/**
* This file contains helper functions for internationalizing projecteditor strings
*/
const { Cu, Cc, Ci } = require("chrome");
const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
const ITCHPAD_STRINGS_URI = "chrome://browser/locale/devtools/projecteditor.properties";
const L10N = new ViewHelpers.L10N(ITCHPAD_STRINGS_URI).stringBundle;
function getLocalizedString (name) {
try {
return L10N.GetStringFromName(name);
} catch (ex) {
console.log("Error reading '" + name + "'");
throw new Error("l10n error with " + name);
}
}
exports.getLocalizedString = getLocalizedString;

View File

@ -0,0 +1,11 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
/**
* This helper is a quick way to require() the Promise object from Promise.jsm.
*/
const { Cu } = require("chrome");
module.exports = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;

View File

@ -0,0 +1,89 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
importScripts("resource://gre/modules/osfile.jsm");
/**
* This file is meant to be loaded in a worker using:
* new ChromeWorker("chrome://browser/content/devtools/readdir.js");
*
* Read a local directory inside of a web woker
*
* @param {string} path
* window to inspect
* @param {RegExp|string} ignore
* A pattern to ignore certain files. This is
* called with file.name.match(ignore).
* @param {Number} maxDepth
* How many directories to recurse before stopping.
* Directories with depth > maxDepth will be ignored.
*/
function readDir(path, ignore, maxDepth = Infinity) {
let ret = {};
let set = new Set();
let info = OS.File.stat(path);
set.add({
path: path,
name: info.name,
isDir: info.isDir,
isSymLink: info.isSymLink,
depth: 0
});
for (let info of set) {
let children = [];
if (info.isDir && !info.isSymLink) {
if (info.depth > maxDepth) {
continue;
}
let iterator = new OS.File.DirectoryIterator(info.path);
try {
for (let child in iterator) {
if (ignore && child.name.match(ignore)) {
continue;
}
children.push(child.path);
set.add({
path: child.path,
name: child.name,
isDir: child.isDir,
isSymLink: child.isSymLink,
depth: info.depth + 1
});
}
} finally {
iterator.close();
}
}
ret[info.path] = {
name: info.name,
isDir: info.isDir,
isSymLink: info.isSymLink,
depth: info.depth,
children: children,
};
}
return ret;
};
onmessage = function (event) {
try {
let {path, ignore, depth} = event.data;
let message = readDir(path, ignore, depth);
postMessage(message);
} catch(ex) {
console.log(ex);
}
};

View File

@ -0,0 +1,44 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 promise = require("projecteditor/helpers/promise");
const { ItchEditor } = require("projecteditor/editors");
var AppProjectEditor = Class({
extends: ItchEditor,
hidesToolbar: true,
initialize: function(document, host) {
ItchEditor.prototype.initialize.apply(this, arguments);
this.appended = promise.resolve();
this.host = host;
this.label = "app-manager";
},
destroy: function() {
this.elt.remove();
this.elt = null;
},
load: function(resource) {
this.elt.textContent = "";
let {appManagerOpts} = this.host.project;
let iframe = this.iframe = this.elt.ownerDocument.createElement("iframe");
iframe.setAttribute("flex", "1");
iframe.setAttribute("src", appManagerOpts.projectOverviewURL);
this.elt.appendChild(iframe);
// Wait for other `appended` listeners before emitting load.
this.appended.then(() => {
this.emit("load");
});
}
});
exports.AppProjectEditor = AppProjectEditor;

View File

@ -0,0 +1,47 @@
const { Cu } = require("chrome");
const { Class } = require("sdk/core/heritage");
const { EventTarget } = require("sdk/event/target");
const { emit } = require("sdk/event/core");
const promise = require("projecteditor/helpers/promise");
var { registerPlugin, Plugin } = require("projecteditor/plugins/core");
const { AppProjectEditor } = require("./app-project-editor");
var AppManagerRenderer = Class({
extends: Plugin,
isAppManagerProject: function() {
return !!this.host.project.appManagerOpts;
},
editorForResource: function(resource) {
if (!resource.parent && this.isAppManagerProject()) {
return AppProjectEditor;
}
},
onAnnotate: function(resource, editor, elt) {
if (resource.parent || !this.isAppManagerProject()) {
return;
}
let {appManagerOpts} = this.host.project;
let doc = elt.ownerDocument;
let image = doc.createElement("image");
let label = doc.createElement("label");
label.className = "project-name-label";
image.className = "project-image";
let name = appManagerOpts.name || resource.basename;
let url = appManagerOpts.iconUrl || "icon-sample.png";
label.textContent = name;
image.setAttribute("src", url);
elt.innerHTML = "";
elt.appendChild(image);
elt.appendChild(label);
return true;
}
});
exports.AppManagerRenderer = AppManagerRenderer;
registerPlugin(AppManagerRenderer);

View File

@ -0,0 +1,83 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
// This is the core plugin API.
const { Class } = require("sdk/core/heritage");
var Plugin = Class({
initialize: function(host) {
this.host = host;
this.init(host);
},
destroy: function(host) { },
init: function(host) {},
showForCategories: function(elt, categories) {
this._showFor = this._showFor || [];
let set = new Set(categories);
this._showFor.push({
elt: elt,
categories: new Set(categories)
});
if (this.host.currentEditor) {
this.onEditorActivated(this.host.currentEditor);
} else {
elt.classList.add("plugin-hidden");
}
},
priv: function(item) {
if (!this._privData) {
this._privData = new WeakMap();
}
if (!this._privData.has(item)) {
this._privData.set(item, {});
}
return this._privData.get(item);
},
onTreeSelected: function(resource) {},
// Editor state lifetime...
onEditorCreated: function(editor) {},
onEditorDestroyed: function(editor) {},
onEditorActivated: function(editor) {
if (this._showFor) {
let category = editor.category;
for (let item of this._showFor) {
if (item.categories.has(category)) {
item.elt.classList.remove("plugin-hidden");
} else {
item.elt.classList.add("plugin-hidden");
}
}
}
},
onEditorDeactivated: function(editor) {
if (this._showFor) {
for (let item of this._showFor) {
item.elt.classList.add("plugin-hidden");
}
}
},
onEditorLoad: function(editor) {},
onEditorSave: function(editor) {},
onEditorChange: function(editor) {},
onEditorCursorActivity: function(editor) {},
});
exports.Plugin = Plugin;
function registerPlugin(constr) {
exports.registeredPlugins.push(constr);
}
exports.registerPlugin = registerPlugin;
exports.registeredPlugins = [];

View File

@ -0,0 +1,38 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { Class } = require("sdk/core/heritage");
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
const { getLocalizedString } = require("projecteditor/helpers/l10n");
var DeletePlugin = Class({
extends: Plugin,
init: function(host) {
this.host.addCommand({
id: "cmd-delete"
});
this.host.createMenuItem({
parent: "#directory-menu-popup",
label: getLocalizedString("projecteditor.deleteLabel"),
command: "cmd-delete"
});
},
onCommand: function(cmd) {
if (cmd === "cmd-delete") {
let tree = this.host.projectTree;
let resource = tree.getSelectedResource();
let parent = resource.parent;
tree.deleteResource(resource).then(() => {
this.host.project.refresh();
})
}
}
});
exports.DeletePlugin = DeletePlugin;
registerPlugin(DeletePlugin);

View File

@ -0,0 +1,43 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { Class } = require("sdk/core/heritage");
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
const { emit } = require("sdk/event/core");
var DirtyPlugin = Class({
extends: Plugin,
onEditorSave: function(editor) { this.onEditorChange(editor); },
onEditorLoad: function(editor) { this.onEditorChange(editor); },
onEditorChange: function(editor) {
// Only run on a TextEditor
if (!editor || !editor.editor) {
return;
}
// Dont' force a refresh unless the dirty state has changed...
let priv = this.priv(editor);
let clean = editor.editor.isClean();
if (priv.isClean !== clean) {
let resource = editor.shell.resource;
emit(resource, "label-change", resource);
priv.isClean = clean;
}
},
onAnnotate: function(resource, editor, elt) {
if (editor && editor.editor && !editor.editor.isClean()) {
elt.textContent = '*' + resource.displayName;
return true;
}
}
});
exports.DirtyPlugin = DirtyPlugin;
registerPlugin(DirtyPlugin);

View File

@ -0,0 +1,41 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 promise = require("projecteditor/helpers/promise");
const { ItchEditor } = require("projecteditor/editors");
var ImageEditor = Class({
extends: ItchEditor,
initialize: function(document) {
ItchEditor.prototype.initialize.apply(this, arguments);
this.label = "image";
this.appended = promise.resolve();
},
load: function(resource) {
let image = this.doc.createElement("image");
image.className = "editor-image";
image.setAttribute("src", resource.uri);
let box1 = this.doc.createElement("box");
box1.appendChild(image);
let box2 = this.doc.createElement("box");
box2.setAttribute("flex", 1);
this.elt.appendChild(box1);
this.elt.appendChild(box2);
this.appended.then(() => {
this.emit("load");
});
}
});
exports.ImageEditor = ImageEditor;

View File

@ -0,0 +1,28 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 promise = require("projecteditor/helpers/promise");
const { ImageEditor } = require("./image-editor");
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
var ImageEditorPlugin = Class({
extends: Plugin,
editorForResource: function(node) {
if (node.contentCategory === "image") {
return ImageEditor;
}
},
init: function(host) {
}
});
exports.ImageEditorPlugin = ImageEditorPlugin;
registerPlugin(ImageEditorPlugin);

View File

@ -0,0 +1,29 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
var { Class } = require("sdk/core/heritage");
var { registerPlugin, Plugin } = require("projecteditor/plugins/core");
var LoggingPlugin = Class({
extends: Plugin,
// Editor state lifetime...
onEditorCreated: function(editor) { console.log("editor created: " + editor) },
onEditorDestroyed: function(editor) { console.log("editor destroyed: " + editor )},
onEditorSave: function(editor) { console.log("editor saved: " + editor) },
onEditorLoad: function(editor) { console.log("editor loaded: " + editor) },
onEditorActivated: function(editor) { console.log("editor activated: " + editor )},
onEditorDeactivated: function(editor) { console.log("editor deactivated: " + editor )},
onEditorChange: function(editor) { console.log("editor changed: " + editor )},
onCommand: function(cmd) { console.log("Command: " + cmd); }
});
exports.LoggingPlugin = LoggingPlugin;
registerPlugin(LoggingPlugin);

View File

@ -0,0 +1,90 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { Class } = require("sdk/core/heritage");
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
const { getLocalizedString } = require("projecteditor/helpers/l10n");
// Handles the new command.
var NewFile = Class({
extends: Plugin,
init: function(host) {
this.host.createMenuItem({
parent: "#file-menu-popup",
label: getLocalizedString("projecteditor.newLabel"),
command: "cmd-new",
key: "key-new"
});
this.host.createMenuItem({
parent: "#directory-menu-popup",
label: getLocalizedString("projecteditor.newLabel"),
command: "cmd-new"
});
this.command = this.host.addCommand({
id: "cmd-new",
key: getLocalizedString("projecteditor.new.commandkey"),
modifiers: "accel"
});
},
onCommand: function(cmd) {
if (cmd === "cmd-new") {
let tree = this.host.projectTree;
let resource = tree.getSelectedResource();
parent = resource.isDir ? resource : resource.parent;
sibling = resource.isDir ? null : resource;
if (!("createChild" in parent)) {
return;
}
let extension = sibling ? sibling.contentCategory : parent.store.defaultCategory;
let template = "untitled{1}." + extension;
let name = this.suggestName(parent, template);
tree.promptNew(name, parent, sibling).then(name => {
// XXX: sanitize bad file names.
// If the name is already taken, just add/increment a number.
if (this.hasChild(parent, name)) {
let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
template = matches[1] + "{1}" + matches[3] + matches[4];
name = this.suggestName(parent, template, parseInt(matches[2]) || 2);
}
return parent.createChild(name);
}).then(resource => {
tree.selectResource(resource);
this.host.currentEditor.focus();
}).then(null, console.error);
}
},
suggestName: function(parent, template, start=1) {
let i = start;
let name;
do {
name = template.replace("\{1\}", i === 1 ? "" : i);
i++;
} while (this.hasChild(parent, name));
return name;
},
hasChild: function(resource, name) {
for (let child of resource.children) {
if (child.basename === name) {
return true;
}
}
return false;
}
})
exports.NewFile = NewFile;
registerPlugin(NewFile);

View File

@ -0,0 +1,89 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { Class } = require("sdk/core/heritage");
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
const picker = require("projecteditor/helpers/file-picker");
const { getLocalizedString } = require("projecteditor/helpers/l10n");
// Handles the save command.
var SavePlugin = Class({
extends: Plugin,
init: function(host) {
this.host.addCommand({
id: "cmd-saveas",
key: getLocalizedString("projecteditor.save.commandkey"),
modifiers: "accel shift"
});
this.host.addCommand({
id: "cmd-save",
key: getLocalizedString("projecteditor.save.commandkey"),
modifiers: "accel"
});
// Wait until we can add things into the app manager menu
// this.host.createMenuItem({
// parent: "#file-menu-popup",
// label: "Save",
// command: "cmd-save",
// key: "key-save"
// });
// this.host.createMenuItem({
// parent: "#file-menu-popup",
// label: "Save As",
// command: "cmd-saveas",
// });
},
onCommand: function(cmd) {
if (cmd === "cmd-save") {
this.save();
} else if (cmd === "cmd-saveas") {
this.saveAs();
}
},
saveAs: function() {
let editor = this.host.currentEditor;
let project = this.host.resourceFor(editor);
let resource;
picker.showSave({
window: this.host.window,
directory: project && project.parent ? project.parent.path : null,
defaultName: project ? project.basename : null,
}).then(path => {
return this.createResource(path);
}).then(res => {
resource = res;
return this.saveResource(editor, resource);
}).then(() => {
this.host.openResource(resource);
}).then(null, console.error);
},
save: function() {
let editor = this.host.currentEditor;
let resource = this.host.resourceFor(editor);
if (!resource) {
return this.saveAs();
}
return this.saveResource(editor, resource);
},
createResource: function(path) {
return this.host.project.resourceFor(path, { create: true })
},
saveResource: function(editor, resource) {
return editor.save(resource);
}
})
exports.SavePlugin = SavePlugin;
registerPlugin(SavePlugin);

View File

@ -0,0 +1,105 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 promise = require("projecteditor/helpers/promise");
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
/**
* Print information about the currently opened file
* and the state of the current editor
*/
var StatusBarPlugin = Class({
extends: Plugin,
init: function() {
this.box = this.host.createElement("hbox", {
parent: "#projecteditor-toolbar-bottom"
});
this.activeMode = this.host.createElement("label", {
parent: this.box,
class: "projecteditor-basic-display"
});
this.cursorPosition = this.host.createElement("label", {
parent: this.box,
class: "projecteditor-basic-display"
});
this.fileLabel = this.host.createElement("label", {
parent: "#plugin-toolbar-left",
class: "projecteditor-file-label"
});
},
destroy: function() {
},
/**
* Print information about the current state of the editor
*
* @param Editor editor
*/
render: function(editor, resource) {
if (!resource || resource.isDir) {
this.fileLabel.textContent = "";
this.cursorPosition.value = "";
return;
}
this.fileLabel.textContent = resource.basename;
this.activeMode.value = editor.toString();
if (editor.editor) {
let cursorStart = editor.editor.getCursor("start");
let cursorEnd = editor.editor.getCursor("end");
if (cursorStart.line === cursorEnd.line && cursorStart.ch === cursorEnd.ch) {
this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch;
} else {
this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch + " | " +
cursorEnd.line + " " + cursorEnd.ch;
}
} else {
this.cursorPosition.value = "";
}
},
/**
* Print the current file name
*
* @param Resource resource
*/
onTreeSelected: function(resource) {
if (!resource || resource.isDir) {
this.fileLabel.textContent = "";
return;
}
this.fileLabel.textContent = resource.basename;
},
onEditorDeactivated: function(editor) {
this.fileLabel.textContent = "";
this.cursorPosition.value = "";
},
onEditorChange: function(editor, resource) {
this.render(editor, resource);
},
onEditorCursorActivity: function(editor, resource) {
this.render(editor, resource);
},
onEditorActivated: function(editor, resource) {
this.render(editor, resource);
},
});
exports.StatusBarPlugin = StatusBarPlugin;
registerPlugin(StatusBarPlugin);

View File

@ -0,0 +1,239 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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("projecteditor/helpers/event");
const prefs = require("sdk/preferences/service");
const { LocalStore } = require("projecteditor/stores/local");
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
const promise = require("projecteditor/helpers/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.
*/
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();
}
}.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<Resource>
* 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<Store>
* 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 generator-iterator<string>
* A list of all file paths
*/
allPaths: function*() {
for (let [path, store] of this.localStores) {
yield path;
}
},
/**
* 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;

View File

@ -0,0 +1,594 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { Project } = require("projecteditor/project");
const { ProjectTreeView } = require("projecteditor/tree");
const { ShellDeck } = require("projecteditor/shells");
const { Resource } = require("projecteditor/stores/resource");
const { registeredPlugins } = require("projecteditor/plugins/core");
const { EventTarget } = require("sdk/event/target");
const { on, forget } = require("projecteditor/helpers/event");
const { emit } = require("sdk/event/core");
const { merge } = require("sdk/util/object");
const promise = require("projecteditor/helpers/promise");
const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
const { DOMHelpers } = Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const ITCHPAD_URL = "chrome://browser/content/devtools/projecteditor.xul";
// Enabled Plugins
require("projecteditor/plugins/dirty/lib/dirty");
require("projecteditor/plugins/delete/lib/delete");
require("projecteditor/plugins/new/lib/new");
require("projecteditor/plugins/save/lib/save");
require("projecteditor/plugins/image-view/lib/plugin");
require("projecteditor/plugins/app-manager/lib/plugin");
require("projecteditor/plugins/status-bar/lib/plugin");
// Uncomment to enable logging.
// require("projecteditor/plugins/logging/lib/logging");
/**
* This is the main class tying together an instance of the ProjectEditor.
* The frontend is contained inside of this.iframe, which loads projecteditor.xul.
*
* Usage:
* let projecteditor = new ProjectEditor(frame);
* projecteditor.loaded.then((projecteditor) => {
* // Ready to use.
* });
*
* Responsible for maintaining:
* - The list of Plugins for this instance.
* - The ShellDeck, which includes all Shells for opened Resources
* -- Shells take in a Resource, and construct the appropriate Editor
* - The Project, which includes all Stores for this instance
* -- Stores manage all Resources starting from a root directory
* --- Resources are a representation of a file on disk
* - The ProjectTreeView that builds the UI for interacting with the
* project.
*
* This object emits the following events:
* - "onEditorDestroyed": When editor is destroyed
* - "onEditorSave": When editor is saved
* - "onEditorLoad": When editor is loaded
* - "onEditorActivated": When editor is activated
* - "onEditorChange": When editor is changed
* - "onEditorCursorActivity": When there is cursor activity in a text editor
* - "onCommand": When a command happens
* - "onEditorDestroyed": When editor is destroyed
*
* The events can be bound like so:
* projecteditor.on("onEditorCreated", (editor) => { });
*/
var ProjectEditor = Class({
extends: EventTarget,
/**
* Initialize ProjectEditor, and load into an iframe if specified.
*
* @param Iframe iframe
* The iframe to inject the DOM into. If this is not
* specified, then this.load(frame) will need to be called
* before accessing ProjectEditor.
*/
initialize: function(iframe) {
this._onTreeSelected = this._onTreeSelected.bind(this);
this._onEditorCreated = this._onEditorCreated.bind(this);
this._onEditorActivated = this._onEditorActivated.bind(this);
this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
this._updateEditorMenuItems = this._updateEditorMenuItems.bind(this);
if (iframe) {
this.load(iframe);
}
},
/**
* Load the instance inside of a specified iframe.
* This can be called more than once, and it will return the promise
* from the first call.
*
* @param Iframe iframe
* The iframe to inject the projecteditor DOM into
* @returns Promise
* A promise that is resolved once the iframe has been
* loaded.
*/
load: function(iframe) {
if (this.loaded) {
return this.loaded;
}
let deferred = promise.defer();
this.loaded = deferred.promise;
this.iframe = iframe;
let domReady = () => {
this._onLoad();
deferred.resolve(this);
};
let domHelper = new DOMHelpers(this.iframe.contentWindow);
domHelper.onceDOMReady(domReady);
this.iframe.setAttribute("src", ITCHPAD_URL);
return this.loaded;
},
/**
* Build the projecteditor DOM inside of this.iframe.
*/
_onLoad: function() {
this.document = this.iframe.contentDocument;
this.window = this.iframe.contentWindow;
this._buildSidebar();
this.window.addEventListener("unload", this.destroy.bind(this));
// Editor management
this.shells = new ShellDeck(this, this.document);
this.shells.on("editor-created", this._onEditorCreated);
this.shells.on("editor-activated", this._onEditorActivated);
this.shells.on("editor-deactivated", this._onEditorDeactivated);
let shellContainer = this.document.querySelector("#shells-deck-container");
shellContainer.appendChild(this.shells.elt);
let popup = this.document.querySelector("#edit-menu-popup");
popup.addEventListener("popupshowing", this.updateEditorMenuItems);
// We are not allowing preset projects for now - rebuild a fresh one
// each time.
this.setProject(new Project({
id: "",
name: "",
directories: [],
openFiles: []
}));
this._initCommands();
this._initPlugins();
},
/**
* Create the project tree sidebar that lists files.
*/
_buildSidebar: function() {
this.projectTree = new ProjectTreeView(this.document, {
resourceVisible: this.resourceVisible.bind(this),
resourceFormatter: this.resourceFormatter.bind(this)
});
this.projectTree.on("selection", this._onTreeSelected);
let sourcesBox = this.document.querySelector("#sources");
sourcesBox.appendChild(this.projectTree.elt);
},
/**
* Set up listeners for commands to dispatch to all of the plugins
*/
_initCommands: function() {
this.commands = this.document.querySelector("#projecteditor-commandset");
this.commands.addEventListener("command", (evt) => {
evt.stopPropagation();
evt.preventDefault();
this.pluginDispatch("onCommand", evt.target.id, evt.target);
});
},
/**
* Initialize each plugin in registeredPlugins
*/
_initPlugins: function() {
this._plugins = [];
for (let plugin of registeredPlugins) {
try {
this._plugins.push(plugin(this));
} catch(ex) {
console.exception(ex);
}
}
this.pluginDispatch("lateInit");
},
/**
* Enable / disable necessary menu items using globalOverlay.js.
*/
_updateEditorMenuItems: function() {
this.window.goUpdateGlobalEditMenuItems();
this.window.goUpdateGlobalEditMenuItems();
let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_findAgain'];
commands.forEach(this.window.goUpdateCommand);
},
/**
* Destroy all objects on the iframe unload event.
*/
destroy: function() {
this._plugins.forEach(plugin => { plugin.destroy(); });
this.project.allResources().forEach((resource) => {
let editor = this.editorFor(resource);
if (editor) {
editor.destroy();
}
});
forget(this, this.project);
this.project.destroy();
this.project = null;
this.projectTree.destroy();
this.projectTree = null;
},
/**
* Set the current project viewed by the projecteditor.
*
* @param Project project
* The project to set.
*/
setProject: function(project) {
if (this.project) {
forget(this, this.project);
}
this.project = project;
this.projectTree.setProject(project);
// Whenever a store gets removed, clean up any editors that
// exist for resources within it.
on(this, project, "store-removed", (store) => {
store.allResources().forEach((resource) => {
let editor = this.editorFor(resource);
if (editor) {
editor.destroy();
}
});
});
},
/**
* Set the current project viewed by the projecteditor to a single path,
* used by the app manager.
*
* @param string path
* The file path to set
* @param Object opts
* Custom options used by the project. See plugins/app-manager.
* @param Promise
* Promise that is resolved once the project is ready to be used.
*/
setProjectToAppPath: function(path, opts = {}) {
this.project.appManagerOpts = opts;
this.project.removeAllStores();
this.project.addPath(path);
return this.project.refresh();
},
/**
* Open a resource in a particular shell.
*
* @param Resource resource
* The file to be opened.
*/
openResource: function(resource) {
this.shells.open(resource);
this.projectTree.selectResource(resource);
},
/**
* When a node is selected in the tree, open its associated editor.
*
* @param Resource resource
* The file that has been selected
*/
_onTreeSelected: function(resource) {
// Don't attempt to open a directory that is not the root element.
if (resource.isDir && resource.parent) {
return;
}
this.pluginDispatch("onTreeSelected", resource);
this.openResource(resource);
},
/**
* Create an xul element with options
*
* @param string type
* The tag name of the element to create.
* @param Object options
* "command": DOMNode or string ID of a command element.
* "parent": DOMNode or selector of parent to append child to.
* anything other keys are set as an attribute as the element.
* @returns DOMElement
* The element that has been created.
*/
createElement: function(type, options) {
let elt = this.document.createElement(type);
let parent;
for (let opt in options) {
if (opt === "command") {
let command = typeof(options.command) === "string" ? options.command : options.command.id;
elt.setAttribute("command", command);
} else if (opt === "parent") {
continue;
} else {
elt.setAttribute(opt, options[opt]);
}
}
if (options.parent) {
let parent = options.parent;
if (typeof(parent) === "string") {
parent = this.document.querySelector(parent);
}
parent.appendChild(elt);
}
return elt;
},
/**
* Create a "menuitem" xul element with options
*
* @param Object options
* See createElement for available options.
* @returns DOMElement
* The menuitem that has been created.
*/
createMenuItem: function(options) {
return this.createElement("menuitem", options);
},
/**
* Add a command to the projecteditor document.
* This method is meant to be used with plugins.
*
* @param Object definition
* key: a key/keycode string. Example: "f".
* id: Unique ID. Example: "find".
* modifiers: Key modifiers. Example: "accel".
* @returns DOMElement
* The command element that has been created.
*/
addCommand: function(definition) {
let command = this.document.createElement("command");
command.setAttribute("id", definition.id);
if (definition.key) {
let key = this.document.createElement("key");
key.id = "key_" + definition.id;
let keyName = definition.key;
if (keyName.startsWith("VK_")) {
key.setAttribute("keycode", keyName);
} else {
key.setAttribute("key", keyName);
}
key.setAttribute("modifiers", definition.modifiers);
key.setAttribute("command", definition.id);
this.document.getElementById("projecteditor-keyset").appendChild(key);
}
command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
this.document.getElementById("projecteditor-commandset").appendChild(command);
return command;
},
/**
* Get the instance of a plugin registered with a certain type.
*
* @param Type pluginType
* The type, such as SavePlugin
* @returns Plugin
* The plugin instance matching the specified type.
*/
getPlugin: function(pluginType) {
for (let plugin of this.plugins) {
if (plugin.constructor === pluginType) {
return plugin;
}
}
return null;
},
/**
* Get all plugin instances active for the current project
*
* @returns [Plugin]
*/
get plugins() {
if (!this._plugins) {
console.log("plugins requested before _plugins was set");
return [];
}
// Could filter further based on the type of project selected,
// but no need right now.
return this._plugins;
},
/**
* Dispatch an onEditorCreated event, and listen for other events specific
* to this editor instance.
*
* @param Editor editor
* The new editor instance.
*/
_onEditorCreated: function(editor) {
this.pluginDispatch("onEditorCreated", editor);
this._editorListenAndDispatch(editor, "change", "onEditorChange");
this._editorListenAndDispatch(editor, "cursorActivity", "onEditorCursorActivity");
this._editorListenAndDispatch(editor, "load", "onEditorLoad");
this._editorListenAndDispatch(editor, "save", "onEditorSave");
},
/**
* Dispatch an onEditorActivated event and finish setting up once the
* editor is ready to use.
*
* @param Editor editor
* The editor instance, which is now appended in the document.
* @param Resource resource
* The resource used by the editor
*/
_onEditorActivated: function(editor, resource) {
editor.setToolbarVisibility();
this.pluginDispatch("onEditorActivated", editor, resource);
},
/**
* Dispatch an onEditorDactivated event once an editor loses focus
*
* @param Editor editor
* The editor instance, which is no longer active.
* @param Resource resource
* The resource used by the editor
*/
_onEditorDeactivated: function(editor, resource) {
this.pluginDispatch("onEditorDeactivated", editor, resource);
},
/**
* Call a method on all plugins that implement the method.
* Also emits the same handler name on `this`.
*
* @param string handler
* Which function name to call on plugins.
* @param ...args args
* All remaining parameters are passed into the handler.
*/
pluginDispatch: function(handler, ...args) {
// XXX: Memory leak when console.log an Editor here
// console.log("DISPATCHING EVENT TO PLUGIN", handler, args);
emit(this, handler, ...args);
this.plugins.forEach(plugin => {
try {
if (handler in plugin) plugin[handler](...args);
} catch(ex) {
console.error(ex);
}
})
},
/**
* Listen to an event on the editor object and dispatch it
* to all plugins that implement the associated method
*
* @param Editor editor
* Which editor to listen to
* @param string event
* Which editor event to listen for
* @param string handler
* Which plugin method to call
*/
_editorListenAndDispatch: function(editor, event, handler) {
/// XXX: Uncommenting this line also causes memory leak.
// console.log("Binding listen and dispatch", editor);
editor.on(event, (...args) => {
this.pluginDispatch(handler, editor, this.resourceFor(editor), ...args);
});
},
/**
* Find a shell for a resource.
*
* @param Resource resource
* The file to be opened.
* @returns Shell
*/
shellFor: function(resource) {
return this.shells.shellFor(resource);
},
/**
* Returns the Editor for a given resource.
*
* @param Resource resource
* The file to check.
* @returns Editor
* Instance of the editor for this file.
*/
editorFor: function(resource) {
let shell = this.shellFor(resource);
return shell ? shell.editor : shell;
},
/**
* Returns a resource for the given editor
*
* @param Editor editor
* The editor to check
* @returns Resource
* The resource associated with this editor
*/
resourceFor: function(editor) {
if (editor && editor.shell && editor.shell.resource) {
return editor.shell.resource;
}
return null;
},
/**
* Decide whether a given resource should be hidden in the tree.
*
* @param Resource resource
* The resource in the tree
* @returns Boolean
* True if the node should be visible, false if hidden.
*/
resourceVisible: function(resource) {
return true;
},
/**
* Format the given node for display in the resource tree view.
*
* @param Resource resource
* The file to be opened.
* @param DOMNode elt
* The element in the tree to render into.
*/
resourceFormatter: function(resource, elt) {
let editor = this.editorFor(resource);
let renderedByPlugin = false;
// Allow plugins to override default templating of resource in tree.
this.plugins.forEach(plugin => {
if (!plugin.onAnnotate) {
return;
}
if (plugin.onAnnotate(resource, editor, elt)) {
renderedByPlugin = true;
}
});
// If no plugin wants to handle it, just use a string from the resource.
if (!renderedByPlugin) {
elt.textContent = resource.displayName;
}
},
get sourcesVisible() {
return this.sourceToggle.hasAttribute("pane-collapsed");
},
get currentShell() {
return this.shells.currentShell;
},
get currentEditor() {
return this.shells.currentEditor;
},
});
exports.ProjectEditor = ProjectEditor;

View File

@ -0,0 +1,210 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { EditorTypeForResource } = require("projecteditor/editors");
const NetworkHelper = require("devtools/toolkit/webconsole/network-helper");
/**
* The Shell is the object that manages the editor for a single resource.
* It is in charge of selecting the proper Editor (text/image/plugin-defined)
* and instantiating / appending the editor.
* This object is not exported, it is just used internally by the ShellDeck.
*
* This object has a promise `editorAppended`, that will resolve once the editor
* is ready to be used.
*/
var Shell = Class({
extends: EventTarget,
/**
* @param ProjectEditor host
* @param Resource resource
*/
initialize: function(host, resource) {
this.host = host;
this.doc = host.document;
this.resource = resource;
this.elt = this.doc.createElement("vbox");
this.elt.shell = this;
let constructor = this._editorTypeForResource();
this.editor = constructor(this.doc, this.host);
this.editor.shell = this;
this.editorAppended = this.editor.appended;
let loadDefer = promise.defer();
this.editor.on("load", () => {
loadDefer.resolve();
});
this.editorLoaded = loadDefer.promise;
this.elt.appendChild(this.editor.elt);
},
/**
* Start loading the resource. The 'load' event happens as
* a result of this function, so any listeners to 'editorAppended'
* need to be added before calling this.
*/
load: function() {
this.editor.load(this.resource);
},
/**
* Make sure the correct editor is selected for the resource.
* @returns Type:Editor
*/
_editorTypeForResource: function() {
let resource = this.resource;
let constructor = EditorTypeForResource(resource);
if (this.host.plugins) {
this.host.plugins.forEach(plugin => {
if (plugin.editorForResource) {
let pluginEditor = plugin.editorForResource(resource);
if (pluginEditor) {
constructor = pluginEditor;
}
}
});
}
return constructor;
}
});
/**
* The ShellDeck is in charge of managing the list of active Shells for
* the current ProjectEditor instance (aka host).
*
* This object emits the following events:
* - "editor-created": When an editor is initially created
* - "editor-activated": When an editor is ready to use
* - "editor-deactivated": When an editor is ready to use
*/
var ShellDeck = Class({
extends: EventTarget,
/**
* @param ProjectEditor host
* @param Document document
*/
initialize: function(host, document) {
this.doc = document;
this.host = host;
this.deck = this.doc.createElement("deck");
this.deck.setAttribute("flex", "1");
this.elt = this.deck;
this.shells = new Map();
this._activeShell = null;
},
/**
* Open a resource in a Shell. Will create the Shell
* if it doesn't exist yet.
*
* @param Resource resource
* The file to be opened
* @returns Shell
*/
open: function(defaultResource) {
let shell = this.shellFor(defaultResource);
if (!shell) {
shell = this._createShell(defaultResource);
this.shells.set(defaultResource, shell);
}
this.selectShell(shell);
return shell;
},
/**
* Create a new Shell for a resource. Called by `open`.
*
* @returns Shell
*/
_createShell: function(defaultResource) {
let shell = Shell(this.host, defaultResource);
shell.editorAppended.then(() => {
this.shells.set(shell.resource, shell);
emit(this, "editor-created", shell.editor);
if (this.currentShell === shell) {
this.selectShell(shell);
}
});
shell.load();
this.deck.appendChild(shell.elt);
return shell;
},
/**
* Select a given shell and open its editor.
* Will fire editor-deactivated on the old selected Shell (if any),
* and editor-activated on the new one once it is ready
*
* @param Shell shell
*/
selectShell: function(shell) {
// Don't fire another activate if this is already the active shell
if (this._activeShell != shell) {
if (this._activeShell) {
emit(this, "editor-deactivated", this._activeShell.editor, this._activeShell.resource);
}
this.deck.selectedPanel = shell.elt;
this._activeShell = shell;
shell.editorLoaded.then(() => {
// Handle case where another shell has been requested before this
// one is finished loading.
if (this._activeShell === shell) {
emit(this, "editor-activated", shell.editor, shell.resource);
}
});
}
},
/**
* Find a Shell for a Resource.
*
* @param Resource resource
* @returns Shell
*/
shellFor: function(resource) {
return this.shells.get(resource);
},
/**
* The currently active Shell. Note: the editor may not yet be available
* on the current shell. Best to wait for the 'editor-activated' event
* instead.
*
* @returns Shell
*/
get currentShell() {
return this._activeShell;
},
/**
* The currently active Editor, or null if it is not ready.
*
* @returns Editor
*/
get currentEditor() {
let shell = this.currentShell;
return shell ? shell.editor : null;
},
});
exports.ShellDeck = ShellDeck;

View File

@ -0,0 +1,58 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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("projecteditor/helpers/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;

View File

@ -0,0 +1,219 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 } = Cu.import("resource://gre/modules/osfile.jsm", {});
const { emit } = require("sdk/event/core");
const { Store } = require("projecteditor/stores/base");
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
const promise = require("projecteditor/helpers/promise");
const { on, forget } = require("projecteditor/helpers/event");
const { FileResource } = require("projecteditor/stores/resource");
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
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.window = Services.appShell.hiddenDOMWindow;
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() {
if (this.window) {
this.window.clearTimeout(this._refreshTimeout);
}
if (this._refreshDeferred) {
this._refreshDeferred.reject("destroy");
}
if (this.worker) {
this.worker.terminate();
}
this._refreshTimeout = null;
this._refreshDeferred = null;
this.window = 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);
throw new Task.Result(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 = this.window.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://browser/content/devtools/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;

View File

@ -0,0 +1,340 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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("projecteditor/helpers/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 } = Cu.import("resource://gre/modules/Task.jsm", {});
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.
*/
var Resource = Class({
extends: EventTarget,
refresh: function() { return promise.resolve(this) },
setURI: function(uri) {
if (typeof(uri) === "string") {
uri = URL.URL(uri);
}
this.uri = uri;
},
/**
* Return the trailing name component of this.uri.
*/
get basename() { return this.uri.path.replace(/\/+$/, '').replace(/\\/g,'/').replace( /.*\//, '' ); },
/**
* Is there more than 1 child Resource?
*/
get hasChildren() { return this.children && this.children.size > 0; },
/**
* 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;
},
/**
* 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;
},
/**
* 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);
});
},
/**
* 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;
});
},
/**
* 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: function(content) {
let buffer = gEncoder.encode(content);
let path = this.path;
// XXX: writeAtomic was losing permissions after saving on OSX
// return OS.File.writeAtomic(this.path, buffer, { tmpPath: this.path + ".tmp" });
return Task.spawn(function*() {
let pfh = yield OS.File.open(path, {truncate: true});
yield pfh.write(buffer);
yield pfh.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/toolkit/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;

View File

@ -0,0 +1,557 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 { emit } = require("sdk/event/core");
const { EventTarget } = require("sdk/event/target");
const { merge } = require("sdk/util/object");
const promise = require("projecteditor/helpers/promise");
const { InplaceEditor } = require("devtools/shared/inplace-editor");
const { on, forget } = require("projecteditor/helpers/event");
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
const HTML_NS = "http://www.w3.org/1999/xhtml";
/**
* ResourceContainer is used as the view of a single Resource in
* the tree. It is not exported.
*/
var ResourceContainer = Class({
/**
* @param ProjectTreeView tree
* @param Resource resource
*/
initialize: function(tree, resource) {
this.tree = tree;
this.resource = resource;
this.elt = null;
this.expander = null;
this.children = null;
let doc = tree.doc;
this.elt = doc.createElementNS(HTML_NS, "li");
this.elt.classList.add("child");
this.line = doc.createElementNS(HTML_NS, "div");
this.line.classList.add("child");
this.line.classList.add("side-menu-widget-item");
this.line.setAttribute("theme", "dark");
this.line.setAttribute("tabindex", "0");
this.elt.appendChild(this.line);
this.highlighter = doc.createElementNS(HTML_NS, "span");
this.highlighter.classList.add("highlighter");
this.line.appendChild(this.highlighter);
this.expander = doc.createElementNS(HTML_NS, "span");
this.expander.className = "arrow expander";
this.expander.setAttribute("open", "");
this.line.appendChild(this.expander);
this.icon = doc.createElementNS(HTML_NS, "span");
this.line.appendChild(this.icon);
this.label = doc.createElementNS(HTML_NS, "span");
this.label.className = "file-label";
this.line.appendChild(this.label);
this.line.addEventListener("contextmenu", (ev) => {
this.select();
this.openContextMenu(ev);
}, false);
this.children = doc.createElementNS(HTML_NS, "ul");
this.children.classList.add("children");
this.elt.appendChild(this.children);
this.line.addEventListener("click", (evt) => {
if (!this.selected) {
this.select();
this.expanded = true;
evt.stopPropagation();
}
}, false);
this.expander.addEventListener("click", (evt) => {
this.expanded = !this.expanded;
this.select();
evt.stopPropagation();
}, true);
this.update();
},
destroy: function() {
this.elt.remove();
this.expander.remove();
this.icon.remove();
this.highlighter.remove();
this.children.remove();
this.label.remove();
this.elt = this.expander = this.icon = this.highlighter = this.children = this.label = null;
},
/**
* Open the context menu when right clicking on the view.
* XXX: We could pass this to plugins to allow themselves
* to be register/remove items from the context menu if needed.
*
* @param Event e
*/
openContextMenu: function(ev) {
ev.preventDefault();
let popup = this.tree.doc.getElementById("directory-menu-popup");
popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
},
/**
* Update the view based on the current state of the Resource.
*/
update: function() {
let visible = this.tree.options.resourceVisible ?
this.tree.options.resourceVisible(this.resource) :
true;
this.elt.hidden = !visible;
this.tree.options.resourceFormatter(this.resource, this.label);
this.icon.className = "file-icon";
let contentCategory = this.resource.contentCategory;
let baseName = this.resource.basename || "";
if (!this.resource.parent) {
this.icon.classList.add("icon-none");
} else if (this.resource.isDir) {
this.icon.classList.add("icon-folder");
} else if (baseName.endsWith(".manifest") || baseName.endsWith(".webapp")) {
this.icon.classList.add("icon-manifest");
} else if (contentCategory === "js") {
this.icon.classList.add("icon-js");
} else if (contentCategory === "css") {
this.icon.classList.add("icon-css");
} else if (contentCategory === "html") {
this.icon.classList.add("icon-html");
} else if (contentCategory === "image") {
this.icon.classList.add("icon-img");
} else {
this.icon.classList.add("icon-file");
}
this.expander.style.visibility = this.resource.hasChildren ? "visible" : "hidden";
},
/**
* Select this view in the ProjectTreeView.
*/
select: function() {
this.tree.selectContainer(this);
},
/**
* @returns Boolean
* Is this view currently selected
*/
get selected() {
return this.line.classList.contains("selected");
},
/**
* Set the selected state in the UI.
*/
set selected(v) {
if (v) {
this.line.classList.add("selected");
} else {
this.line.classList.remove("selected");
}
},
/**
* @returns Boolean
* Are any children visible.
*/
get expanded() {
return !this.elt.classList.contains("tree-collapsed");
},
/**
* Set the visiblity state of children.
*/
set expanded(v) {
if (v) {
this.elt.classList.remove("tree-collapsed");
this.expander.setAttribute("open", "");
} else {
this.expander.removeAttribute("open");
this.elt.classList.add("tree-collapsed");
}
}
});
/**
* TreeView is a view managing a list of children.
* It is not to be instantiated directly - only extended.
* Use ProjectTreeView instead.
*/
var TreeView = Class({
extends: EventTarget,
/**
* @param Document document
* @param Object options
* - resourceFormatter: a function(Resource, DOMNode)
* that renders the resource into the view
* - resourceVisible: a function(Resource) -> Boolean
* that determines if the resource should show up.
*/
initialize: function(document, options) {
this.doc = document;
this.options = merge({
resourceFormatter: function(resource, elt) {
elt.textContent = resource.toString();
}
}, options);
this.models = new Set();
this.roots = new Set();
this._containers = new Map();
this.elt = document.createElement("vbox");
this.elt.tree = this;
this.elt.className = "side-menu-widget-container sources-tree";
this.elt.setAttribute("with-arrows", "true");
this.elt.setAttribute("theme", "dark");
this.elt.setAttribute("flex", "1");
this.children = document.createElementNS(HTML_NS, "ul");
this.children.setAttribute("flex", "1");
this.elt.appendChild(this.children);
this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this);
this.updateResource = this.updateResource.bind(this);
},
destroy: function() {
this._destroyed = true;
this.elt.remove();
},
/**
* Prompt the user to create a new file in the tree.
*
* @param string initial
* The suggested starting file name
* @param Resource parent
* @param Resource sibling
* Which resource to put this next to. If not set,
* it will be put in front of all other children.
*
* @returns Promise
* Resolves once the prompt has been successful,
* Rejected if it is cancelled
*/
promptNew: function(initial, parent, sibling=null) {
let deferred = promise.defer();
let parentContainer = this._containers.get(parent);
let item = this.doc.createElement("li");
item.className = "child";
let placeholder = this.doc.createElementNS(HTML_NS, "div");
placeholder.className = "child";
item.appendChild(placeholder);
let children = parentContainer.children;
sibling = sibling ? this._containers.get(sibling).elt : null;
parentContainer.children.insertBefore(item, sibling ? sibling.nextSibling : children.firstChild);
new InplaceEditor({
element: placeholder,
initial: initial,
start: editor => {
editor.input.select();
},
done: function(val, commit) {
if (commit) {
deferred.resolve(val);
} else {
deferred.reject(val);
}
parentContainer.line.focus();
},
destroy: () => {
item.parentNode.removeChild(item);
},
});
return deferred.promise;
},
/**
* Add a new Store into the TreeView
*
* @param Store model
*/
addModel: function(model) {
if (this.models.has(model)) {
// Requesting to add a model that already exists
return;
}
this.models.add(model);
let placeholder = this.doc.createElementNS(HTML_NS, "li");
placeholder.style.display = "none";
this.children.appendChild(placeholder);
this.roots.add(model.root);
model.root.refresh().then(root => {
if (this._destroyed || !this.models.has(model)) {
// model may have been removed during the initial refresh.
// In this case, do not import the resource or add to DOM, just leave it be.
return;
}
let container = this.importResource(root);
container.line.classList.add("side-menu-widget-group-title");
container.line.setAttribute("theme", "dark");
this.selectContainer(container);
this.children.insertBefore(container.elt, placeholder);
this.children.removeChild(placeholder);
});
},
/**
* Remove a Store from the TreeView
*
* @param Store model
*/
removeModel: function(model) {
this.models.delete(model);
this.removeResource(model.root);
},
/**
* Get the ResourceContainer. Used for testing the view.
*
* @param Resource resource
* @returns ResourceContainer
*/
getViewContainer: function(resource) {
return this._containers.get(resource);
},
/**
* Select a ResourceContainer in the tree.
*
* @param ResourceContainer container
*/
selectContainer: function(container) {
if (this.selectedContainer === container) {
return;
}
if (this.selectedContainer) {
this.selectedContainer.selected = false;
}
this.selectedContainer = container;
container.selected = true;
emit(this, "selection", container.resource);
},
/**
* Select a Resource in the tree.
*
* @param Resource resource
*/
selectResource: function(resource) {
this.selectContainer(this._containers.get(resource));
},
/**
* Get the currently selected Resource
*
* @param Resource resource
*/
getSelectedResource: function() {
return this.selectedContainer.resource;
},
/**
* Insert a Resource into the view.
* Makes a new ResourceContainer if needed
*
* @param Resource resource
*/
importResource: function(resource) {
if (!resource) {
return null;
}
if (this._containers.has(resource)) {
return this._containers.get(resource);
}
var container = ResourceContainer(this, resource);
this._containers.set(resource, container);
this._updateChildren(container);
on(this, resource, "children-changed", this.resourceChildrenChanged);
on(this, resource, "label-change", this.updateResource);
return container;
},
/**
* Delete a Resource from the FileSystem. XXX: This should
* definitely be moved away from here, maybe to the store?
*
* @param Resource resource
*/
deleteResource: function(resource) {
if (resource.isDir) {
return OS.File.removeDir(resource.path);
} else {
return OS.File.remove(resource.path);
}
},
/**
* Remove a Resource (including children) from the view.
*
* @param Resource resource
*/
removeResource: function(resource) {
let toRemove = resource.allDescendants();
toRemove.add(resource);
for (let remove of toRemove) {
this._removeResource(remove);
}
},
/**
* Remove an individual Resource (but not children) from the view.
*
* @param Resource resource
*/
_removeResource: function(resource) {
resource.off("children-changed", this.resourceChildrenChanged);
resource.off("label-change", this.updateResource);
if (this._containers.get(resource)) {
this._containers.get(resource).destroy();
this._containers.delete(resource);
}
},
/**
* Listener for when a resource has new children.
* This can happen as files are being loaded in from FileSystem, for example.
*
* @param Resource resource
*/
resourceChildrenChanged: function(resource) {
this.updateResource(resource);
this._updateChildren(this._containers.get(resource));
},
/**
* Listener for when a label in the view has been updated.
* For example, the 'dirty' plugin marks changed files with an '*'
* next to the filename, and notifies with this event.
*
* @param Resource resource
*/
updateResource: function(resource) {
let container = this._containers.get(resource);
container.update();
},
/**
* Build necessary ResourceContainers for a Resource and its
* children, then append them into the view.
*
* @param ResourceContainer container
*/
_updateChildren: function(container) {
let resource = container.resource;
let fragment = this.doc.createDocumentFragment();
if (resource.children) {
for (let child of resource.childrenSorted) {
let childContainer = this.importResource(child);
fragment.appendChild(childContainer.elt);
}
}
while (container.children.firstChild) {
container.children.firstChild.remove();
}
container.children.appendChild(fragment);
},
});
/**
* ProjectTreeView is the implementation of TreeView
* that is exported. This is the class that is to be used
* directly.
*/
var ProjectTreeView = Class({
extends: TreeView,
/**
* See TreeView.initialize
*
* @param Document document
* @param Object options
*/
initialize: function(document, options) {
TreeView.prototype.initialize.apply(this, arguments);
},
destroy: function() {
this.forgetProject();
TreeView.prototype.destroy.apply(this, arguments);
},
/**
* Remove current project and empty the tree
*/
forgetProject: function() {
if (this.project) {
forget(this, this.project);
for (let store of this.project.allStores()) {
this.removeModel(store);
}
}
},
/**
* Show a project in the tree
*
* @param Project project
* The project to render into a tree
*/
setProject: function(project) {
this.forgetProject();
this.project = project;
if (this.project) {
on(this, project, "store-added", this.addModel.bind(this));
on(this, project, "store-removed", this.removeModel.bind(this));
on(this, project, "project-saved", this.refresh.bind(this));
this.refresh();
}
},
/**
* Refresh the tree with all of the current project stores
*/
refresh: function() {
for (let store of this.project.allStores()) {
this.addModel(store);
}
}
});
exports.ProjectTreeView = ProjectTreeView;

View File

@ -0,0 +1,6 @@
# 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/.
TEST_DIRS += ['test']

View File

@ -0,0 +1,13 @@
[DEFAULT]
subsuite = devtools
support-files =
head.js
helper_homepage.html
[browser_projecteditor_delete_file.js]
[browser_projecteditor_editing_01.js]
[browser_projecteditor_immediate_destroy.js]
[browser_projecteditor_init.js]
[browser_projecteditor_new_file.js]
[browser_projecteditor_stores.js]
[browser_projecteditor_tree_selection.js]

View File

@ -0,0 +1,80 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test tree selection functionality
let test = asyncTest(function*() {
let projecteditor = yield addProjectEditorTabForTempDirectory();
ok(true, "ProjectEditor has loaded");
let root = [...projecteditor.project.allStores()][0].root;
is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
for (let child of root.children) {
yield deleteWithContextMenu(projecteditor.projectTree.getViewContainer(child));
}
function onPopupShow(contextMenu) {
let defer = promise.defer();
contextMenu.addEventListener("popupshown", function onpopupshown() {
contextMenu.removeEventListener("popupshown", onpopupshown);
defer.resolve();
});
return defer.promise;
}
function onPopupHide(contextMenu) {
let defer = promise.defer();
contextMenu.addEventListener("popuphidden", function popuphidden() {
contextMenu.removeEventListener("popuphidden", popuphidden);
defer.resolve();
});
return defer.promise;
}
function openContextMenuOn(node) {
EventUtils.synthesizeMouseAtCenter(
node,
{button: 2, type: "contextmenu"},
node.ownerDocument.defaultView
);
}
function deleteWithContextMenu(container) {
let defer = promise.defer();
let resource = container.resource;
let popup = projecteditor.document.getElementById("directory-menu-popup");
info ("Going to attempt deletion for: " + resource.path)
onPopupShow(popup).then(function () {
let deleteCommand = popup.querySelector("[command=cmd-delete]");
ok (deleteCommand, "Delete command exists in popup");
is (deleteCommand.getAttribute("hidden"), "", "Delete command is visible");
is (deleteCommand.getAttribute("disabled"), "", "Delete command is enabled");
onPopupHide(popup).then(() => {
ok (true, "Popup has been hidden, waiting for project refresh");
projecteditor.project.refresh().then(() => {
OS.File.stat(resource.path).then(() => {
ok (false, "The file was not deleted");
defer.resolve();
}, (ex) => {
ok (ex instanceof OS.File.Error && ex.becauseNoSuchFile, "OS.File.stat promise was rejected because the file is gone");
defer.resolve();
});
});
});
deleteCommand.click();
popup.hidePopup();
});
openContextMenuOn(container.label);
return defer.promise;
}
});

View File

@ -0,0 +1,94 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test ProjectEditor basic functionality
let test = asyncTest(function*() {
let projecteditor = yield addProjectEditorTabForTempDirectory();
let TEMP_PATH = [...projecteditor.project.allPaths()][0];
is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
ok (projecteditor.currentEditor, "There is an editor for projecteditor");
let resources = projecteditor.project.allResources();
resources.forEach((r, i) => {
console.log("Resource detected", r.path, i);
});
let stylesCss = resources.filter(r=>r.basename === "styles.css")[0];
yield selectFile(projecteditor, stylesCss);
yield testEditFile(projecteditor, getTempFile("css/styles.css").path, "body,html { color: orange; }");
let indexHtml = resources.filter(r=>r.basename === "index.html")[0];
yield selectFile(projecteditor, indexHtml);
yield testEditFile(projecteditor, getTempFile("index.html").path, "<h1>Changed Content Again</h1>");
let license = resources.filter(r=>r.basename === "LICENSE")[0];
yield selectFile(projecteditor, license);
yield testEditFile(projecteditor, getTempFile("LICENSE").path, "My new license");
let readmeMd = resources.filter(r=>r.basename === "README.md")[0];
yield selectFile(projecteditor, readmeMd);
yield testEditFile(projecteditor, getTempFile("README.md").path, "My new license");
let scriptJs = resources.filter(r=>r.basename === "script.js")[0];
yield selectFile(projecteditor, scriptJs);
yield testEditFile(projecteditor, getTempFile("js/script.js").path, "alert('hi')");
let vectorSvg = resources.filter(r=>r.basename === "vector.svg")[0];
yield selectFile(projecteditor, vectorSvg);
yield testEditFile(projecteditor, getTempFile("img/icons/vector.svg").path, "<svg></svg>");
});
function selectFile (projecteditor, resource) {
ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
projecteditor.projectTree.selectResource(resource);
if (resource.isDir) {
return;
}
let [editorActivated] = yield promise.all([
onceEditorActivated(projecteditor)
]);
is (editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
}
function testEditFile(projecteditor, filePath, newData) {
info ("Testing file editing for: " + filePath);
let initialData = yield getFileData(filePath);
let editor = projecteditor.currentEditor;
let resource = projecteditor.resourceFor(editor);
let viewContainer= projecteditor.projectTree.getViewContainer(resource);
let originalTreeLabel = viewContainer.label.textContent;
is (resource.path, filePath, "Resource path is set correctly");
is (editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
info ("Setting text in the editor and doing checks before saving");
editor.editor.setText(newData);
is (editor.editor.getText(), newData, "Editor has been filled with new data");
is (viewContainer.label.textContent, "*" + originalTreeLabel, "Label is marked as changed");
info ("Saving the editor and checking to make sure the file gets saved on disk");
editor.save(resource);
let savedResource = yield onceEditorSave(projecteditor);
is (viewContainer.label.textContent, originalTreeLabel, "Label is unmarked as changed");
is (savedResource.path, filePath, "The saved resouce path matches the original file path");
is (savedResource, resource, "The saved resource is the same as the original resource");
let savedData = yield getFileData(filePath);
is (savedData, newData, "Data has been correctly saved to disk");
info ("Finished checking saving for " + filePath);
}

View File

@ -0,0 +1,62 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that projecteditor can be destroyed in various states of loading
// without causing any leaks or exceptions.
let test = asyncTest(function* () {
info ("Testing tab closure when projecteditor is in various states");
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
let iframe = content.document.getElementById("projecteditor-iframe");
ok (iframe, "Tab has placeholder iframe for projecteditor");
info ("Closing the tab without doing anything");
gBrowser.removeCurrentTab();
});
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
let iframe = content.document.getElementById("projecteditor-iframe");
ok (iframe, "Tab has placeholder iframe for projecteditor");
let projecteditor = ProjectEditor.ProjectEditor();
ok (projecteditor, "ProjectEditor has been initialized");
info ("Closing the tab before attempting to load");
gBrowser.removeCurrentTab();
});
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
let iframe = content.document.getElementById("projecteditor-iframe");
ok (iframe, "Tab has placeholder iframe for projecteditor");
let projecteditor = ProjectEditor.ProjectEditor();
ok (projecteditor, "ProjectEditor has been initialized");
projecteditor.load(iframe);
info ("Closing the tab after a load is requested, but before load is finished");
gBrowser.removeCurrentTab();
});
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
let iframe = content.document.getElementById("projecteditor-iframe");
ok (iframe, "Tab has placeholder iframe for projecteditor");
let projecteditor = ProjectEditor.ProjectEditor();
ok (projecteditor, "ProjectEditor has been initialized");
return projecteditor.load(iframe).then(() => {
info ("Closing the tab after a load has been requested and finished");
gBrowser.removeCurrentTab();
});
});
finish();
});

View File

@ -0,0 +1,18 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that projecteditor can be initialized.
function test() {
info ("Initializing projecteditor");
addProjectEditorTab().then((projecteditor) => {
ok (projecteditor, "Load callback has been called");
ok (projecteditor.shells, "ProjectEditor has shells");
ok (projecteditor.project, "ProjectEditor has a project");
finish();
});
}

View File

@ -0,0 +1,13 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test tree selection functionality
let test = asyncTest(function*() {
let projecteditor = yield addProjectEditorTabForTempDirectory();
ok(projecteditor, "ProjectEditor has loaded");
});

View File

@ -0,0 +1,16 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test ProjectEditor basic functionality
let test = asyncTest(function*() {
let projecteditor = yield addProjectEditorTabForTempDirectory();
let TEMP_PATH = [...projecteditor.project.allPaths()][0];
is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
is ([...projecteditor.project.allPaths()].length, 1, "1 path is set");
projecteditor.project.removeAllStores();
is ([...projecteditor.project.allPaths()].length, 0, "No paths are remaining");
});

View File

@ -0,0 +1,69 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test tree selection functionality
let test = asyncTest(function*() {
let projecteditor = yield addProjectEditorTabForTempDirectory();
let TEMP_PATH = [...projecteditor.project.allPaths()][0];
is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
ok (projecteditor.currentEditor, "There is an editor for projecteditor");
let resources = projecteditor.project.allResources();
is (
resources.map(r=>r.basename).join("|"),
"ProjectEditor|css|styles.css|data|img|icons|128x128.png|16x16.png|32x32.png|vector.svg|fake.png|js|script.js|index.html|LICENSE|README.md",
"Resources came through in proper order"
);
for (let i = 0; i < resources.length; i++){
yield selectFileFirstLoad(projecteditor, resources[i]);
}
for (let i = 0; i < resources.length; i++){
yield selectFileSubsequentLoad(projecteditor, resources[i]);
}
for (let i = 0; i < resources.length; i++){
yield selectFileSubsequentLoad(projecteditor, resources[i]);
}
});
function selectFileFirstLoad(projecteditor, resource) {
ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
projecteditor.projectTree.selectResource(resource);
if (resource.isDir) {
return;
}
let [editorCreated, editorLoaded, editorActivated] = yield promise.all([
onceEditorCreated(projecteditor),
onceEditorLoad(projecteditor),
onceEditorActivated(projecteditor)
]);
is (editorCreated, projecteditor.currentEditor, "Editor has been created for " + resource.path);
is (editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
is (editorLoaded, projecteditor.currentEditor, "Editor has been loaded for " + resource.path);
}
function selectFileSubsequentLoad(projecteditor, resource) {
ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
projecteditor.projectTree.selectResource(resource);
if (resource.isDir) {
return;
}
// Only activated should fire the next time
// (may add load() if we begin checking for changes from disk)
let [editorActivated] = yield promise.all([
onceEditorActivated(projecteditor)
]);
is (editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
}

View File

@ -0,0 +1,255 @@
/* 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 = Components.utils;
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const TargetFactory = devtools.TargetFactory;
const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
const promise = devtools.require("sdk/core/promise");
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const ProjectEditor = devtools.require("projecteditor/projecteditor");
const TEST_URL_ROOT = "http://mochi.test:8888/browser/browser/devtools/projecteditor/test/";
const SAMPLE_WEBAPP_URL = TEST_URL_ROOT + "/helper_homepage.html";
let TEMP_PATH;
// All test are asynchronous
waitForExplicitFinish();
//Services.prefs.setBoolPref("devtools.dump.emit", true);
// Set the testing flag on gDevTools and reset it when the test ends
gDevTools.testing = true;
registerCleanupFunction(() => gDevTools.testing = false);
// Clear preferences that may be set during the course of tests.
registerCleanupFunction(() => {
// Services.prefs.clearUserPref("devtools.dump.emit");
TEMP_PATH = null;
});
// Auto close the toolbox and close the test tabs when the test ends
registerCleanupFunction(() => {
try {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.closeToolbox(target);
} catch (ex) {
dump(ex);
}
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
/**
* Define an async test based on a generator function
*/
function asyncTest(generator) {
return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
}
/**
* Add a new test tab in the browser and load the given url.
* @param {String} url The url to be loaded in the new tab
* @return a promise that resolves to the tab object when the url is loaded
*/
function addTab(url) {
info("Adding a new tab with URL: '" + url + "'");
let def = promise.defer();
let tab = gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
info("URL '" + url + "' loading complete");
waitForFocus(() => {
def.resolve(tab);
}, content);
}, true);
content.location = url;
return def.promise;
}
function addProjectEditorTabForTempDirectory() {
TEMP_PATH = buildTempDirectoryStructure();
let CUSTOM_OPTS = {
name: "Test",
iconUrl: "chrome://browser/skin/devtools/tool-options.svg",
projectOverviewURL: SAMPLE_WEBAPP_URL
};
return addProjectEditorTab().then((projecteditor) => {
return projecteditor.setProjectToAppPath(TEMP_PATH, CUSTOM_OPTS).then(() => {
return projecteditor;
});
});
}
function addProjectEditorTab() {
return addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
let iframe = content.document.getElementById("projecteditor-iframe");
let projecteditor = ProjectEditor.ProjectEditor(iframe);
ok (iframe, "Tab has placeholder iframe for projecteditor");
ok (projecteditor, "ProjectEditor has been initialized");
return projecteditor.loaded.then((projecteditor) => {
return projecteditor;
});
});
}
/**
* Build a temporary directory as a workspace for this loader
* https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
*/
function buildTempDirectoryStructure() {
// First create (and remove) the temp dir to discard any changes
let TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
TEMP_DIR.remove(true);
// Now rebuild our fake project.
TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "css"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "data"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "img", "icons"], true);
FileUtils.getDir("TmpD", ["ProjectEditor", "js"], true);
let htmlFile = FileUtils.getFile("TmpD", ["ProjectEditor", "index.html"]);
htmlFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(htmlFile, [
'<!DOCTYPE html>',
'<html lang="en">',
' <head>',
' <meta charset="utf-8" />',
' <title>ProjectEditor Temp File</title>',
' <link rel="stylesheet" href="style.css" />',
' </head>',
' <body id="home">',
' <p>ProjectEditor Temp File</p>',
' </body>',
'</html>'].join("\n")
);
let readmeFile = FileUtils.getFile("TmpD", ["ProjectEditor", "README.md"]);
readmeFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(readmeFile, [
'## Readme'
].join("\n")
);
let licenseFile = FileUtils.getFile("TmpD", ["ProjectEditor", "LICENSE"]);
licenseFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(licenseFile, [
'/* 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/. */'
].join("\n")
);
let cssFile = FileUtils.getFile("TmpD", ["ProjectEditor", "css", "styles.css"]);
cssFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
writeToFile(cssFile, [
'body {',
' background: red;',
'}'
].join("\n")
);
FileUtils.getFile("TmpD", ["ProjectEditor", "js", "script.js"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "fake.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "16x16.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "32x32.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "128x128.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "vector.svg"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
return TEMP_DIR.path;
}
// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
function writeToFile(file, data) {
console.log("Writing to file: " + file.path, file.exists());
let defer = promise.defer();
var ostream = FileUtils.openSafeFileOutputStream(file);
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var istream = converter.convertToInputStream(data);
// The last argument (the callback) is optional.
NetUtil.asyncCopy(istream, ostream, function(status) {
if (!Components.isSuccessCode(status)) {
// Handle error!
info("ERROR WRITING TEMP FILE", status);
}
});
}
function getTempFile(path) {
let parts = ["ProjectEditor"];
parts = parts.concat(path.split("/"));
return FileUtils.getFile("TmpD", parts);
}
// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
function* getFileData(path) {
let file = new FileUtils.File(path);
let def = promise.defer();
NetUtil.asyncFetch(file, function(inputStream, status) {
if (!Components.isSuccessCode(status)) {
info("ERROR READING TEMP FILE", status);
}
// Detect if an empty file is loaded
try {
inputStream.available();
} catch(e) {
def.resolve("");
return;
}
var data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
def.resolve(data);
});
return def.promise;
}
function onceEditorCreated(projecteditor) {
let def = promise.defer();
projecteditor.once("onEditorCreated", (editor) => {
def.resolve(editor);
});
return def.promise;
}
function onceEditorLoad(projecteditor) {
let def = promise.defer();
projecteditor.once("onEditorLoad", (editor) => {
def.resolve(editor);
});
return def.promise;
}
function onceEditorActivated(projecteditor) {
let def = promise.defer();
projecteditor.once("onEditorActivated", (editor) => {
def.resolve(editor);
});
return def.promise;
}
function onceEditorSave(projecteditor) {
let def = promise.defer();
projecteditor.once("onEditorSave", (editor, resource) => {
def.resolve(resource);
});
return def.promise;
}

View File

@ -0,0 +1 @@
<h1>ProjectEditor tests</h1>

View File

@ -0,0 +1,8 @@
# -*- Mode: python; c-basic-offset: 4; 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/.
BROWSER_CHROME_MANIFESTS += ['browser.ini']

View File

@ -0,0 +1,49 @@
# 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/.
# LOCALIZATION NOTE These strings are used inside the ProjectEditor component
# which is used for editing files in a directory and is used inside the
# App Manager.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (projecteditor.deleteLabel):
# This string is displayed as a context menu item for allowing the selected
# file / folder to be deleted
projecteditor.deleteLabel=Delete
# LOCALIZATION NOTE (projecteditor.newLabel):
# This string is displayed as a context menu item for adding a new file to
# the directory
projecteditor.newLabel=New...
# LOCALIZATION NOTE (projecteditor.selectFileLabel):
# This string is displayed as the title on the file picker when saving a file
projecteditor.selectFileLabel=Select a File
# LOCALIZATION NOTE (projecteditor.openFolderLabel):
# This string is displayed as the title on the file picker when opening a folder
projecteditor.openFolderLabel=Select a Folder
# LOCALIZATION NOTE (projecteditor.openFileLabel):
# This string is displayed as the title on the file picker when opening a file
projecteditor.openFileLabel=Open a File
# LOCALIZATION NOTE (projecteditor.find.commandkey): This is the key to use in
# conjunction with accel (Command on Mac or Ctrl on other platforms) to search
# text in the files
projecteditor.find.commandkey=F
# LOCALIZATION NOTE (projecteditor.save.commandkey): This is the key to use in
# conjunction with accel (Command on Mac or Ctrl on other platforms) to
# save the file. It is used with accel+shift to "save as"
projecteditor.save.commandkey=S
# LOCALIZATION NOTE (projecteditor.new.commandkey): This is the key to use in
# conjunction with accel (Command on Mac or Ctrl on other platforms) to
# create a new file
projecteditor.new.commandkey=N

View File

@ -57,6 +57,7 @@
locale/browser/devtools/toolbox.dtd (%chrome/browser/devtools/toolbox.dtd)
locale/browser/devtools/toolbox.properties (%chrome/browser/devtools/toolbox.properties)
locale/browser/devtools/inspector.dtd (%chrome/browser/devtools/inspector.dtd)
locale/browser/devtools/projecteditor.properties (%chrome/browser/devtools/projecteditor.properties)
locale/browser/devtools/eyedropper.properties (%chrome/browser/devtools/eyedropper.properties)
locale/browser/devtools/connection-screen.dtd (%chrome/browser/devtools/connection-screen.dtd)
locale/browser/devtools/connection-screen.properties (%chrome/browser/devtools/connection-screen.properties)

View File

@ -306,6 +306,8 @@ browser.jar:
skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png)
skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png)
skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
skin/classic/browser/devtools/projecteditor/projecteditor.css (../shared/devtools/projecteditor/projecteditor.css)
skin/classic/browser/devtools/projecteditor/file-icons-sheet@2x.png (../shared/devtools/projecteditor/file-icons-sheet@2x.png)
skin/classic/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css)
skin/classic/browser/devtools/app-manager/index.css (../shared/devtools/app-manager/index.css)
skin/classic/browser/devtools/app-manager/device.css (../shared/devtools/app-manager/device.css)

View File

@ -428,6 +428,8 @@ browser.jar:
skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png)
skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png)
skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
skin/classic/browser/devtools/projecteditor/projecteditor.css (../shared/devtools/projecteditor/projecteditor.css)
skin/classic/browser/devtools/projecteditor/file-icons-sheet@2x.png (../shared/devtools/projecteditor/file-icons-sheet@2x.png)
skin/classic/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css)
skin/classic/browser/devtools/app-manager/index.css (../shared/devtools/app-manager/index.css)
skin/classic/browser/devtools/app-manager/device.css (../shared/devtools/app-manager/device.css)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,172 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
:root {
color: #18191a;
}
.plugin-hidden {
display: none;
}
#projecteditor-menubar {
/* XXX: Hide menu bar until we have option to add menu items
to an existing one. */
display: none;
}
#projecteditor-toolbar,
#projecteditor-toolbar-bottom {
display: none; /* For now don't show the status bars */
min-height: 22px;
height: 22px;
background: rgb(237, 237, 237);
}
.sources-tree {
overflow:auto;
-moz-user-focus: true;
}
.sources-tree input {
margin: 2px;
border: 1px solid gray;
}
#main-deck .sources-tree {
background: rgb(225, 225, 225);
min-width: 50px;
}
#main-deck .sources-tree .side-menu-widget-item {
color: #18191A;
}
#main-deck .sources-tree .side-menu-widget-item .file-label {
vertical-align: middle;
display: inline-block;
}
#main-deck .sources-tree .side-menu-widget-item .file-icon {
display: inline-block;
background: url(file-icons-sheet@2x.png);
background-size: 140px 15px;
background-repeat: no-repeat;
width: 20px;
height: 15px;
vertical-align: middle;
background-position: -40px 0;
}
#main-deck .sources-tree .side-menu-widget-item .file-icon.icon-none {
display: none;
}
#main-deck .sources-tree .side-menu-widget-item .icon-css {
background-position: 0 0;
}
#main-deck .sources-tree .side-menu-widget-item .icon-js {
background-position: -20px 0;
}
#main-deck .sources-tree .side-menu-widget-item .icon-html {
background-position: -40px 0;
}
#main-deck .sources-tree .side-menu-widget-item .icon-file {
background-position: -60px 0;
}
#main-deck .sources-tree .side-menu-widget-item .icon-folder {
background-position: -80px 0;
}
#main-deck .sources-tree .side-menu-widget-item .icon-img {
background-position: -100px 0;
}
#main-deck .sources-tree .side-menu-widget-item .icon-manifest {
background-position: -120px 0;
}
#main-deck .sources-tree .side-menu-widget-item:hover {
background: rgba(0, 0, 0, .05);
cursor: pointer;
}
#main-deck .sources-tree .side-menu-widget-item {
border: none;
box-shadow: none;
line-height: 20px;
vertical-align: middle;
white-space: nowrap;
}
#main-deck .sources-tree .side-menu-widget-item.selected {
background: #3875D7;
color: #F5F7FA;
outline: none;
}
#main-deck .sources-tree .side-menu-widget-group-title,
#main-deck .sources-tree .side-menu-widget-group-title:hover:not(.selected) {
background: #B4D7EB;
color: #222;
font-weight: bold;
font-size: 1.05em;
cursor: default;
line-height: 35px;
}
#main-deck .sources-tree li.child:only-child .side-menu-widget-group-title .expander {
display: none;
}
#main-deck .sources-tree .side-menu-widget-item .expander {
width: 16px;
padding: 0;
}
.tree-collapsed .children {
display: none;
}
/* Plugins */
#projecteditor-toolbar textbox {
margin: 0;
}
.projecteditor-basic-display {
padding: 0 3px;
}
.project-name-label {
font-weight: bold;
padding-left: 10px;
}
.project-version-label {
color: #666;
padding-left: 5px;
font-size: .9em;
}
.project-image {
max-height: 28px;
margin-left: -.5em;
vertical-align: middle;
}
.editor-image {
padding: 10px;
}
.projecteditor-file-label {
font-weight: bold;
padding-left: 29px;
vertical-align: middle;
}

View File

@ -343,6 +343,8 @@ browser.jar:
skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png)
skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png)
skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
skin/classic/browser/devtools/projecteditor/projecteditor.css (../shared/devtools/projecteditor/projecteditor.css)
skin/classic/browser/devtools/projecteditor/file-icons-sheet@2x.png (../shared/devtools/projecteditor/file-icons-sheet@2x.png)
skin/classic/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css)
skin/classic/browser/devtools/app-manager/index.css (../shared/devtools/app-manager/index.css)
skin/classic/browser/devtools/app-manager/device.css (../shared/devtools/app-manager/device.css)
@ -729,6 +731,8 @@ browser.jar:
skin/classic/aero/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png)
skin/classic/aero/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png)
skin/classic/aero/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
skin/classic/aero/browser/devtools/projecteditor/projecteditor.css (../shared/devtools/projecteditor/projecteditor.css)
skin/classic/aero/browser/devtools/projecteditor/file-icons-sheet@2x.png (../shared/devtools/projecteditor/file-icons-sheet@2x.png)
skin/classic/aero/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css)
skin/classic/aero/browser/devtools/app-manager/index.css (../shared/devtools/app-manager/index.css)
skin/classic/aero/browser/devtools/app-manager/device.css (../shared/devtools/app-manager/device.css)

View File

@ -61,6 +61,7 @@ function BuiltinProvider() {}
BuiltinProvider.prototype = {
load: function() {
this.loader = new loader.Loader({
id: "fx-devtools",
modules: {
"Debugger": Debugger,
"Services": Object.create(Services),
@ -87,6 +88,7 @@ BuiltinProvider.prototype = {
"devtools/async-utils": "resource://gre/modules/devtools/async-utils",
"devtools/content-observer": "resource://gre/modules/devtools/content-observer",
"gcli": "resource://gre/modules/devtools/gcli",
"projecteditor": "resource:///modules/devtools/projecteditor",
"acorn": "resource://gre/modules/devtools/acorn",
"acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js",
"tern": "resource://gre/modules/devtools/tern",
@ -138,10 +140,12 @@ SrcdirProvider.prototype = {
let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js");
let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js");
let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli"));
let projecteditorURI = this.fileURI(OS.Path.join(devtoolsDir, "projecteditor"));
let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn"));
let acornWalkURI = OS.Path.join(acornURI, "walk.js");
let ternURI = OS.Path.join(toolkitDir, "tern");
this.loader = new loader.Loader({
id: "fx-devtools",
modules: {
"Debugger": Debugger,
"Services": Object.create(Services),
@ -166,6 +170,7 @@ SrcdirProvider.prototype = {
"devtools/async-utils": asyncUtilsURI,
"devtools/content-observer": contentObserverURI,
"gcli": gcliURI,
"projecteditor": projecteditorURI,
"acorn": acornURI,
"acorn/util/walk": acornWalkURI,
"tern": ternURI