mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-08 00:38:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
303
devtools/client/projecteditor/lib/editors.js
Normal file
303
devtools/client/projecteditor/lib/editors.js
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const promise = require("promise");
|
||||
const Editor = require("devtools/client/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,
|
||||
|
||||
/**
|
||||
* A boolean specifying whether the editor can be edited / saved.
|
||||
* For instance, a 'save' doesn't make sense on an image.
|
||||
*/
|
||||
isEditable: false,
|
||||
|
||||
toString: function () {
|
||||
return this.label || "";
|
||||
},
|
||||
|
||||
emit: function (name, ...args) {
|
||||
emit(this, name, ...args);
|
||||
},
|
||||
|
||||
/* Does the editor not have any unsaved changes? */
|
||||
isClean: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the editor with a single host. This should be called
|
||||
* by objects extending this object with:
|
||||
* ItchEditor.prototype.initialize.apply(this, arguments)
|
||||
*/
|
||||
initialize: function (host) {
|
||||
this.host = host;
|
||||
this.doc = host.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");
|
||||
this.projectEditorKeyset = host.projectEditorKeyset;
|
||||
this.projectEditorCommandset = host.projectEditorCommandset;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
isEditable: true,
|
||||
|
||||
/**
|
||||
* 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.projectEditorKeyset.querySelectorAll("key")].forEach((key) => {
|
||||
let keyUpper = key.getAttribute("key").toUpperCase();
|
||||
let toolModifiers = key.getAttribute("modifiers");
|
||||
let modifiers = {
|
||||
alt: toolModifiers.includes("alt"),
|
||||
shift: toolModifiers.includes("shift")
|
||||
};
|
||||
|
||||
// On the key press, we will dispatch the event within projecteditor.
|
||||
extraKeys[Editor.accel(keyUpper, modifiers)] = () => {
|
||||
let doc = this.projectEditorCommandset.ownerDocument;
|
||||
let event = doc.createEvent("Event");
|
||||
event.initEvent("command", true, true);
|
||||
let command = this.projectEditorCommandset.querySelector("#" + key.getAttribute("command"));
|
||||
command.dispatchEvent(event);
|
||||
};
|
||||
});
|
||||
|
||||
return extraKeys;
|
||||
},
|
||||
|
||||
isClean: function () {
|
||||
if (!this.editor.isAppended()) {
|
||||
return true;
|
||||
}
|
||||
return this.editor.getText() === this._savedResourceContents;
|
||||
},
|
||||
|
||||
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,
|
||||
autocomplete: true,
|
||||
contextMenu: this.host.textEditorContextMenuPopup
|
||||
});
|
||||
|
||||
// Trigger a few editor specific events on `this`.
|
||||
this.editor.on("change", (...args) => {
|
||||
this.emit("change", ...args);
|
||||
});
|
||||
this.editor.on("cursorActivity", (...args) => {
|
||||
this.emit("cursorActivity", ...args);
|
||||
});
|
||||
this.editor.on("focus", (...args) => {
|
||||
this.emit("focus", ...args);
|
||||
});
|
||||
this.editor.on("saveRequested", (...args) => {
|
||||
this.emit("saveRequested", ...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])=> {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
this._savedResourceContents = resourceContents;
|
||||
this.editor.setText(resourceContents);
|
||||
this.editor.clearHistory();
|
||||
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) {
|
||||
let newText = this.editor.getText();
|
||||
return resource.save(newText).then(() => {
|
||||
this._savedResourceContents = newText;
|
||||
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(() => {
|
||||
if (this.editor) {
|
||||
this.editor.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Wrapper for TextEditor using JavaScript syntax highlighting.
|
||||
*/
|
||||
function JSEditor(host) {
|
||||
return TextEditor(host, Editor.modes.js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for TextEditor using CSS syntax highlighting.
|
||||
*/
|
||||
function CSSEditor(host) {
|
||||
return TextEditor(host, Editor.modes.css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for TextEditor using HTML syntax highlighting.
|
||||
*/
|
||||
function HTMLEditor(host) {
|
||||
return TextEditor(host, 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;
|
||||
86
devtools/client/projecteditor/lib/helpers/event.js
Normal file
86
devtools/client/projecteditor/lib/helpers/event.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
116
devtools/client/projecteditor/lib/helpers/file-picker.js
Normal file
116
devtools/client/projecteditor/lib/helpers/file-picker.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* 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("promise");
|
||||
const { merge } = require("sdk/util/object");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/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;
|
||||
26
devtools/client/projecteditor/lib/helpers/l10n.js
Normal file
26
devtools/client/projecteditor/lib/helpers/l10n.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This file contains helper functions for internationalizing projecteditor strings
|
||||
*/
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const ITCHPAD_STRINGS_URI = "devtools/client/locales/projecteditor.properties";
|
||||
const L10N = new LocalizationHelper(ITCHPAD_STRINGS_URI);
|
||||
|
||||
function getLocalizedString(name) {
|
||||
try {
|
||||
return L10N.getStr(name);
|
||||
} catch (ex) {
|
||||
console.log("Error reading '" + name + "'");
|
||||
throw new Error("l10n error with " + name);
|
||||
}
|
||||
}
|
||||
|
||||
exports.getLocalizedString = getLocalizedString;
|
||||
12
devtools/client/projecteditor/lib/helpers/moz.build
Normal file
12
devtools/client/projecteditor/lib/helpers/moz.build
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'event.js',
|
||||
'file-picker.js',
|
||||
'l10n.js',
|
||||
'prompts.js',
|
||||
)
|
||||
33
devtools/client/projecteditor/lib/helpers/prompts.js
Normal file
33
devtools/client/projecteditor/lib/helpers/prompts.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* -*- 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 user prompts.
|
||||
* See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPromptService
|
||||
*/
|
||||
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
|
||||
const prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Ci.nsIPromptService);
|
||||
|
||||
/**
|
||||
* Show a prompt.
|
||||
*
|
||||
* @param string title
|
||||
* The title to the dialog
|
||||
* @param string message
|
||||
* The message to display
|
||||
*
|
||||
* @return bool
|
||||
* Whether the user has confirmed the action
|
||||
*/
|
||||
function confirm(title, message) {
|
||||
var result = prompts.confirm(null, title || "Title of this Dialog", message || "Are you sure?");
|
||||
return result;
|
||||
}
|
||||
exports.confirm = confirm;
|
||||
|
||||
89
devtools/client/projecteditor/lib/helpers/readdir.js
Normal file
89
devtools/client/projecteditor/lib/helpers/readdir.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
importScripts("resource://gre/modules/osfile.jsm");
|
||||
|
||||
/**
|
||||
* This file is meant to be loaded in a worker using:
|
||||
* new ChromeWorker("chrome://devtools/content/projecteditor/lib/helpers/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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
19
devtools/client/projecteditor/lib/moz.build
Normal file
19
devtools/client/projecteditor/lib/moz.build
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'helpers',
|
||||
'plugins',
|
||||
'stores',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'editors.js',
|
||||
'project.js',
|
||||
'projecteditor.js',
|
||||
'shells.js',
|
||||
'tree.js',
|
||||
)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("promise");
|
||||
const { ItchEditor } = require("devtools/client/projecteditor/lib/editors");
|
||||
|
||||
var AppProjectEditor = Class({
|
||||
extends: ItchEditor,
|
||||
|
||||
hidesToolbar: true,
|
||||
|
||||
initialize: function (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) {
|
||||
let {appManagerOpts} = this.host.project;
|
||||
|
||||
// Only load the frame the first time it is selected
|
||||
if (!this.iframe || this.iframe.getAttribute("src") !== appManagerOpts.projectOverviewURL) {
|
||||
|
||||
this.elt.textContent = "";
|
||||
let iframe = this.iframe = this.elt.ownerDocument.createElement("iframe");
|
||||
let iframeLoaded = this.iframeLoaded = promise.defer();
|
||||
|
||||
iframe.addEventListener("load", function onLoad() {
|
||||
iframe.removeEventListener("load", onLoad);
|
||||
iframeLoaded.resolve();
|
||||
});
|
||||
|
||||
iframe.setAttribute("flex", "1");
|
||||
iframe.setAttribute("src", appManagerOpts.projectOverviewURL);
|
||||
this.elt.appendChild(iframe);
|
||||
|
||||
}
|
||||
|
||||
promise.all([this.iframeLoaded.promise, this.appended]).then(() => {
|
||||
this.emit("load");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.AppProjectEditor = AppProjectEditor;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'app-project-editor.js',
|
||||
'plugin.js',
|
||||
)
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
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("promise");
|
||||
var { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
const { AppProjectEditor } = require("./app-project-editor");
|
||||
const OPTION_URL = "chrome://devtools/skin/images/tool-options.svg";
|
||||
const Services = require("Services");
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
var AppManagerRenderer = Class({
|
||||
extends: Plugin,
|
||||
|
||||
isAppManagerProject: function () {
|
||||
return !!this.host.project.appManagerOpts;
|
||||
},
|
||||
editorForResource: function (resource) {
|
||||
if (!resource.parent && this.isAppManagerProject()) {
|
||||
return AppProjectEditor;
|
||||
}
|
||||
},
|
||||
getUI: function (parent) {
|
||||
let doc = parent.ownerDocument;
|
||||
if (parent.childElementCount == 0) {
|
||||
let image = doc.createElement("image");
|
||||
let optionImage = doc.createElement("image");
|
||||
let flexElement = doc.createElement("div");
|
||||
let nameLabel = doc.createElement("span");
|
||||
let statusElement = doc.createElement("div");
|
||||
|
||||
image.className = "project-image";
|
||||
optionImage.className = "project-options";
|
||||
optionImage.setAttribute("src", OPTION_URL);
|
||||
nameLabel.className = "project-name-label";
|
||||
statusElement.className = "project-status";
|
||||
flexElement.className = "project-flex";
|
||||
|
||||
parent.appendChild(image);
|
||||
parent.appendChild(nameLabel);
|
||||
parent.appendChild(flexElement);
|
||||
parent.appendChild(statusElement);
|
||||
parent.appendChild(optionImage);
|
||||
}
|
||||
|
||||
return {
|
||||
image: parent.querySelector(".project-image"),
|
||||
nameLabel: parent.querySelector(".project-name-label"),
|
||||
statusElement: parent.querySelector(".project-status")
|
||||
};
|
||||
},
|
||||
onAnnotate: function (resource, editor, elt) {
|
||||
if (resource.parent || !this.isAppManagerProject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {appManagerOpts} = this.host.project;
|
||||
let doc = elt.ownerDocument;
|
||||
|
||||
let {image, nameLabel, statusElement} = this.getUI(elt);
|
||||
let name = appManagerOpts.name || resource.basename;
|
||||
let url = appManagerOpts.iconUrl || "icon-sample.png";
|
||||
let status = appManagerOpts.validationStatus || "unknown";
|
||||
let tooltip = Strings.formatStringFromName("status_tooltip",
|
||||
[Strings.GetStringFromName("status_" + status)], 1);
|
||||
|
||||
nameLabel.textContent = name;
|
||||
image.setAttribute("src", url);
|
||||
statusElement.setAttribute("status", status);
|
||||
statusElement.setAttribute("tooltiptext", tooltip);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
exports.AppManagerRenderer = AppManagerRenderer;
|
||||
registerPlugin(AppManagerRenderer);
|
||||
83
devtools/client/projecteditor/lib/plugins/core.js
Normal file
83
devtools/client/projecteditor/lib/plugins/core.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// 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 = [];
|
||||
67
devtools/client/projecteditor/lib/plugins/delete/delete.js
Normal file
67
devtools/client/projecteditor/lib/plugins/delete/delete.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
const { confirm } = require("devtools/client/projecteditor/lib/helpers/prompts");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
|
||||
|
||||
var DeletePlugin = Class({
|
||||
extends: Plugin,
|
||||
shouldConfirm: true,
|
||||
|
||||
init: function (host) {
|
||||
this.host.addCommand(this, {
|
||||
id: "cmd-delete"
|
||||
});
|
||||
this.contextMenuItem = this.host.createMenuItem({
|
||||
parent: this.host.contextMenuPopup,
|
||||
label: getLocalizedString("projecteditor.deleteLabel"),
|
||||
command: "cmd-delete"
|
||||
});
|
||||
},
|
||||
|
||||
confirmDelete: function (resource) {
|
||||
let deletePromptMessage = resource.isDir ?
|
||||
getLocalizedString("projecteditor.deleteFolderPromptMessage") :
|
||||
getLocalizedString("projecteditor.deleteFilePromptMessage");
|
||||
return !this.shouldConfirm || confirm(
|
||||
getLocalizedString("projecteditor.deletePromptTitle"),
|
||||
deletePromptMessage
|
||||
);
|
||||
},
|
||||
|
||||
onContextMenuOpen: function (resource) {
|
||||
// Do not allow deletion of the top level items in the tree. In the
|
||||
// case of the Web IDE in particular this can leave the UI in a weird
|
||||
// state. If we'd like to add ability to delete the project folder from
|
||||
// the tree in the future, then the UI could be cleaned up by listening
|
||||
// to the ProjectTree's "resource-removed" event.
|
||||
if (!resource.parent) {
|
||||
this.contextMenuItem.setAttribute("hidden", "true");
|
||||
} else {
|
||||
this.contextMenuItem.removeAttribute("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
onCommand: function (cmd) {
|
||||
if (cmd === "cmd-delete") {
|
||||
let tree = this.host.projectTree;
|
||||
let resource = tree.getSelectedResource();
|
||||
|
||||
if (!this.confirmDelete(resource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delete().then(() => {
|
||||
this.host.project.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.DeletePlugin = DeletePlugin;
|
||||
registerPlugin(DeletePlugin);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'delete.js',
|
||||
)
|
||||
47
devtools/client/projecteditor/lib/plugins/dirty/dirty.js
Normal file
47
devtools/client/projecteditor/lib/plugins/dirty/dirty.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/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.isClean();
|
||||
if (priv.isClean !== clean) {
|
||||
let resource = editor.shell.resource;
|
||||
emit(resource, "label-change", resource);
|
||||
priv.isClean = clean;
|
||||
}
|
||||
},
|
||||
|
||||
onAnnotate: function (resource, editor, elt) {
|
||||
// Only run on a TextEditor
|
||||
if (!editor || !editor.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editor.isClean()) {
|
||||
elt.textContent = "*" + resource.displayName;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
exports.DirtyPlugin = DirtyPlugin;
|
||||
|
||||
registerPlugin(DirtyPlugin);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'dirty.js',
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("promise");
|
||||
const { ItchEditor } = require("devtools/client/projecteditor/lib/editors");
|
||||
|
||||
var ImageEditor = Class({
|
||||
extends: ItchEditor,
|
||||
|
||||
initialize: function () {
|
||||
ItchEditor.prototype.initialize.apply(this, arguments);
|
||||
this.label = "image";
|
||||
this.appended = promise.resolve();
|
||||
},
|
||||
|
||||
load: function (resource) {
|
||||
this.elt.innerHTML = "";
|
||||
let image = this.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");
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (this.image) {
|
||||
this.image.remove();
|
||||
this.image = null;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
exports.ImageEditor = ImageEditor;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'image-editor.js',
|
||||
'plugin.js',
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("promise");
|
||||
const { ImageEditor } = require("./image-editor");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
|
||||
var ImageEditorPlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
editorForResource: function (node) {
|
||||
if (node.contentCategory === "image") {
|
||||
return ImageEditor;
|
||||
}
|
||||
},
|
||||
|
||||
init: function (host) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
exports.ImageEditorPlugin = ImageEditorPlugin;
|
||||
registerPlugin(ImageEditorPlugin);
|
||||
29
devtools/client/projecteditor/lib/plugins/logging/logging.js
Normal file
29
devtools/client/projecteditor/lib/plugins/logging/logging.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var { Class } = require("sdk/core/heritage");
|
||||
var { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/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);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'logging.js',
|
||||
)
|
||||
21
devtools/client/projecteditor/lib/plugins/moz.build
Normal file
21
devtools/client/projecteditor/lib/plugins/moz.build
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'app-manager',
|
||||
'delete',
|
||||
'dirty',
|
||||
'image-view',
|
||||
'logging',
|
||||
'new',
|
||||
'rename',
|
||||
'save',
|
||||
'status-bar',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'core.js',
|
||||
)
|
||||
9
devtools/client/projecteditor/lib/plugins/new/moz.build
Normal file
9
devtools/client/projecteditor/lib/plugins/new/moz.build
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'new.js',
|
||||
)
|
||||
80
devtools/client/projecteditor/lib/plugins/new/new.js
Normal file
80
devtools/client/projecteditor/lib/plugins/new/new.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
|
||||
|
||||
// Handles the new command.
|
||||
var NewFile = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function () {
|
||||
this.command = this.host.addCommand(this, {
|
||||
id: "cmd-new",
|
||||
key: getLocalizedString("projecteditor.new.commandkey"),
|
||||
modifiers: "accel"
|
||||
});
|
||||
this.host.createMenuItem({
|
||||
parent: this.host.fileMenuPopup,
|
||||
label: getLocalizedString("projecteditor.newLabel"),
|
||||
command: "cmd-new",
|
||||
key: "key_cmd-new"
|
||||
});
|
||||
this.host.createMenuItem({
|
||||
parent: this.host.contextMenuPopup,
|
||||
label: getLocalizedString("projecteditor.newLabel"),
|
||||
command: "cmd-new"
|
||||
});
|
||||
},
|
||||
|
||||
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 (parent.hasChild(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 (parent.hasChild(name));
|
||||
|
||||
return name;
|
||||
}
|
||||
});
|
||||
exports.NewFile = NewFile;
|
||||
registerPlugin(NewFile);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'rename.js',
|
||||
)
|
||||
74
devtools/client/projecteditor/lib/plugins/rename/rename.js
Normal file
74
devtools/client/projecteditor/lib/plugins/rename/rename.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
|
||||
|
||||
var RenamePlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function (host) {
|
||||
this.host.addCommand(this, {
|
||||
id: "cmd-rename"
|
||||
});
|
||||
this.contextMenuItem = this.host.createMenuItem({
|
||||
parent: this.host.contextMenuPopup,
|
||||
label: getLocalizedString("projecteditor.renameLabel"),
|
||||
command: "cmd-rename"
|
||||
});
|
||||
},
|
||||
|
||||
onContextMenuOpen: function (resource) {
|
||||
if (resource.isRoot) {
|
||||
this.contextMenuItem.setAttribute("hidden", "true");
|
||||
} else {
|
||||
this.contextMenuItem.removeAttribute("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
onCommand: function (cmd) {
|
||||
if (cmd === "cmd-rename") {
|
||||
let tree = this.host.projectTree;
|
||||
let resource = tree.getSelectedResource();
|
||||
let parent = resource.parent;
|
||||
let oldName = resource.basename;
|
||||
|
||||
tree.promptEdit(oldName, resource).then(name => {
|
||||
if (name === oldName) {
|
||||
return resource;
|
||||
}
|
||||
if (parent.hasChild(name)) {
|
||||
let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
|
||||
let template = matches[1] + "{1}" + matches[3] + matches[4];
|
||||
name = this.suggestName(resource, template, parseInt(matches[2]) || 2);
|
||||
}
|
||||
return parent.rename(oldName, name);
|
||||
}).then(resource => {
|
||||
this.host.project.refresh();
|
||||
tree.selectResource(resource);
|
||||
if (!resource.isDir) {
|
||||
this.host.currentEditor.focus();
|
||||
}
|
||||
}).then(null, console.error);
|
||||
}
|
||||
},
|
||||
|
||||
suggestName: function (resource, template, start = 1) {
|
||||
let i = start;
|
||||
let name;
|
||||
let parent = resource.parent;
|
||||
do {
|
||||
name = template.replace("\{1\}", i === 1 ? "" : i);
|
||||
i++;
|
||||
} while (parent.hasChild(name));
|
||||
|
||||
return name;
|
||||
}
|
||||
});
|
||||
|
||||
exports.RenamePlugin = RenamePlugin;
|
||||
registerPlugin(RenamePlugin);
|
||||
9
devtools/client/projecteditor/lib/plugins/save/moz.build
Normal file
9
devtools/client/projecteditor/lib/plugins/save/moz.build
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'save.js',
|
||||
)
|
||||
93
devtools/client/projecteditor/lib/plugins/save/save.js
Normal file
93
devtools/client/projecteditor/lib/plugins/save/save.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
const picker = require("devtools/client/projecteditor/lib/helpers/file-picker");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
|
||||
|
||||
// Handles the save command.
|
||||
var SavePlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function (host) {
|
||||
|
||||
this.host.addCommand(this, {
|
||||
id: "cmd-save",
|
||||
key: getLocalizedString("projecteditor.save.commandkey"),
|
||||
modifiers: "accel"
|
||||
});
|
||||
this.host.addCommand(this, {
|
||||
id: "cmd-saveas",
|
||||
key: getLocalizedString("projecteditor.save.commandkey"),
|
||||
modifiers: "accel shift"
|
||||
});
|
||||
this.host.createMenuItem({
|
||||
parent: this.host.fileMenuPopup,
|
||||
label: getLocalizedString("projecteditor.saveLabel"),
|
||||
command: "cmd-save",
|
||||
key: "key_cmd-save"
|
||||
});
|
||||
this.host.createMenuItem({
|
||||
parent: this.host.fileMenuPopup,
|
||||
label: getLocalizedString("projecteditor.saveAsLabel"),
|
||||
command: "cmd-saveas",
|
||||
key: "key_cmd-saveas"
|
||||
});
|
||||
},
|
||||
|
||||
isCommandEnabled: function (cmd) {
|
||||
let currentEditor = this.host.currentEditor;
|
||||
return currentEditor.isEditable;
|
||||
},
|
||||
|
||||
onCommand: function (cmd) {
|
||||
if (cmd === "cmd-save") {
|
||||
this.onEditorSaveRequested();
|
||||
} 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);
|
||||
},
|
||||
|
||||
onEditorSaveRequested: 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);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'plugin.js',
|
||||
)
|
||||
105
devtools/client/projecteditor/lib/plugins/status-bar/plugin.js
Normal file
105
devtools/client/projecteditor/lib/plugins/status-bar/plugin.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("promise");
|
||||
const { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/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);
|
||||
246
devtools/client/projecteditor/lib/project.js
Normal file
246
devtools/client/projecteditor/lib/project.js
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { scope, on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
|
||||
const prefs = require("sdk/preferences/service");
|
||||
const { LocalStore } = require("devtools/client/projecteditor/lib/stores/local");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const promise = require("promise");
|
||||
const { TextEncoder, TextDecoder } = require("sdk/io/buffer");
|
||||
const url = require("sdk/url");
|
||||
|
||||
const gDecoder = new TextDecoder();
|
||||
const gEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* A Project keeps track of the opened folders using LocalStore
|
||||
* objects. Resources are generally requested from the project,
|
||||
* even though the Store is actually keeping track of them.
|
||||
*
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "refresh-complete": After all stores have been refreshed from disk.
|
||||
* - "store-added": When a store has been added to the project.
|
||||
* - "store-removed": When a store has been removed from the project.
|
||||
* - "resource-added": When a resource has been added to one of the stores.
|
||||
* - "resource-removed": When a resource has been removed from one of the stores.
|
||||
*/
|
||||
var Project = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* Intialize the Project.
|
||||
*
|
||||
* @param Object options
|
||||
* Options to be passed into Project.load function
|
||||
*/
|
||||
initialize: function (options) {
|
||||
this.localStores = new Map();
|
||||
|
||||
this.load(options);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
// We are removing the store because the project never gets persisted.
|
||||
// There may need to be separate destroy functionality that doesn't remove
|
||||
// from project if this is saved to DB.
|
||||
this.removeAllStores();
|
||||
},
|
||||
|
||||
toString: function () {
|
||||
return "[Project] " + this.name;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a project given metadata about it.
|
||||
*
|
||||
* @param Object options
|
||||
* Information about the project, containing:
|
||||
* id: An ID (currently unused, but could be used for saving)
|
||||
* name: The display name of the project
|
||||
* directories: An array of path strings to load
|
||||
*/
|
||||
load: function (options) {
|
||||
this.id = options.id;
|
||||
this.name = options.name || "Untitled";
|
||||
|
||||
let paths = new Set(options.directories.map(name => OS.Path.normalize(name)));
|
||||
|
||||
for (let [path, store] of this.localStores) {
|
||||
if (!paths.has(path)) {
|
||||
this.removePath(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (let path of paths) {
|
||||
this.addPath(path);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh all project stores from disk
|
||||
*
|
||||
* @returns Promise
|
||||
* A promise that resolves when everything has been refreshed.
|
||||
*/
|
||||
refresh: function () {
|
||||
return Task.spawn(function* () {
|
||||
for (let [path, store] of this.localStores) {
|
||||
yield store.refresh();
|
||||
}
|
||||
emit(this, "refresh-complete");
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Fetch a resource from the backing storage system for the store.
|
||||
*
|
||||
* @param string path
|
||||
* The path to fetch
|
||||
* @param Object options
|
||||
* "create": bool indicating whether to create a file if it does not exist.
|
||||
* @returns Promise
|
||||
* A promise that resolves with the Resource.
|
||||
*/
|
||||
resourceFor: function (path, options) {
|
||||
let store = this.storeContaining(path);
|
||||
return store.resourceFor(path, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get every resource used inside of the project.
|
||||
*
|
||||
* @returns Array<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 Array<string>
|
||||
* A list of all file paths
|
||||
*/
|
||||
allPaths: function () {
|
||||
return [...this.localStores.keys()];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the store that contains a path.
|
||||
*
|
||||
* @returns Store
|
||||
* The store, if any. Will return null if no store
|
||||
* contains the given path.
|
||||
*/
|
||||
storeContaining: function (path) {
|
||||
let containingStore = null;
|
||||
for (let store of this.allStores()) {
|
||||
if (store.contains(path)) {
|
||||
// With nested projects, the final containing store will be returned.
|
||||
containingStore = store;
|
||||
}
|
||||
}
|
||||
return containingStore;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a store at the current path. If a store already exists
|
||||
* for this path, then return it.
|
||||
*
|
||||
* @param string path
|
||||
* @returns LocalStore
|
||||
*/
|
||||
addPath: function (path) {
|
||||
if (!this.localStores.has(path)) {
|
||||
this.addLocalStore(new LocalStore(path));
|
||||
}
|
||||
return this.localStores.get(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a store for a given path.
|
||||
*
|
||||
* @param string path
|
||||
*/
|
||||
removePath: function (path) {
|
||||
this.removeLocalStore(this.localStores.get(path));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Add the given Store to the project.
|
||||
* Fires a 'store-added' event on the project.
|
||||
*
|
||||
* @param Store store
|
||||
*/
|
||||
addLocalStore: function (store) {
|
||||
store.canPair = true;
|
||||
this.localStores.set(store.path, store);
|
||||
|
||||
// Originally StoreCollection.addStore
|
||||
on(this, store, "resource-added", (resource) => {
|
||||
emit(this, "resource-added", resource);
|
||||
});
|
||||
on(this, store, "resource-removed", (resource) => {
|
||||
emit(this, "resource-removed", resource);
|
||||
});
|
||||
|
||||
emit(this, "store-added", store);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove all of the Stores belonging to the project.
|
||||
*/
|
||||
removeAllStores: function () {
|
||||
for (let store of this.allStores()) {
|
||||
this.removeLocalStore(store);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the given Store from the project.
|
||||
* Fires a 'store-removed' event on the project.
|
||||
*
|
||||
* @param Store store
|
||||
*/
|
||||
removeLocalStore: function (store) {
|
||||
// XXX: tree selection should be reset if active element is affected by
|
||||
// the store being removed
|
||||
if (store) {
|
||||
this.localStores.delete(store.path);
|
||||
forget(this, store);
|
||||
emit(this, "store-removed", store);
|
||||
store.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.Project = Project;
|
||||
816
devtools/client/projecteditor/lib/projecteditor.js
Normal file
816
devtools/client/projecteditor/lib/projecteditor.js
Normal file
|
|
@ -0,0 +1,816 @@
|
|||
/* 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 { Class } = require("sdk/core/heritage");
|
||||
const { Project } = require("devtools/client/projecteditor/lib/project");
|
||||
const { ProjectTreeView } = require("devtools/client/projecteditor/lib/tree");
|
||||
const { ShellDeck } = require("devtools/client/projecteditor/lib/shells");
|
||||
const { Resource } = require("devtools/client/projecteditor/lib/stores/resource");
|
||||
const { registeredPlugins } = require("devtools/client/projecteditor/lib/plugins/core");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { merge } = require("sdk/util/object");
|
||||
const promise = require("promise");
|
||||
const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
|
||||
const { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
|
||||
const Services = require("Services");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const ITCHPAD_URL = "chrome://devtools/content/projecteditor/chrome/content/projecteditor.xul";
|
||||
const { confirm } = require("devtools/client/projecteditor/lib/helpers/prompts");
|
||||
const { getLocalizedString } = require("devtools/client/projecteditor/lib/helpers/l10n");
|
||||
|
||||
// Enabled Plugins
|
||||
require("devtools/client/projecteditor/lib/plugins/dirty/dirty");
|
||||
require("devtools/client/projecteditor/lib/plugins/delete/delete");
|
||||
require("devtools/client/projecteditor/lib/plugins/new/new");
|
||||
require("devtools/client/projecteditor/lib/plugins/rename/rename");
|
||||
require("devtools/client/projecteditor/lib/plugins/save/save");
|
||||
require("devtools/client/projecteditor/lib/plugins/image-view/plugin");
|
||||
require("devtools/client/projecteditor/lib/plugins/app-manager/plugin");
|
||||
require("devtools/client/projecteditor/lib/plugins/status-bar/plugin");
|
||||
|
||||
// Uncomment to enable logging.
|
||||
// require("devtools/client/projecteditor/lib/plugins/logging/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
|
||||
* - "onContextMenuOpen": When the context menu is opened on the project tree
|
||||
*
|
||||
* 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.
|
||||
* @param Object options
|
||||
* - menubar: a <menubar> element to inject menus into
|
||||
* - menuindex: Integer child index to insert menus
|
||||
*/
|
||||
initialize: function (iframe, options = {}) {
|
||||
this._onTreeSelected = this._onTreeSelected.bind(this);
|
||||
this._onTreeResourceRemoved = this._onTreeResourceRemoved.bind(this);
|
||||
this._onEditorCreated = this._onEditorCreated.bind(this);
|
||||
this._onEditorActivated = this._onEditorActivated.bind(this);
|
||||
this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
|
||||
this._updateMenuItems = this._updateMenuItems.bind(this);
|
||||
this._updateContextMenuItems = this._updateContextMenuItems.bind(this);
|
||||
this.destroy = this.destroy.bind(this);
|
||||
this.menubar = options.menubar || null;
|
||||
this.menuindex = options.menuindex || null;
|
||||
this._menuEnabled = true;
|
||||
this._destroyed = false;
|
||||
this._loaded = false;
|
||||
this._pluginCommands = new Map();
|
||||
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 = () => {
|
||||
if (this._destroyed) {
|
||||
deferred.reject("Error: ProjectEditor has been destroyed before loading");
|
||||
return;
|
||||
}
|
||||
this._onLoad();
|
||||
this._loaded = true;
|
||||
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._initCommands();
|
||||
this._buildMenubar();
|
||||
this._buildSidebar();
|
||||
|
||||
this.window.addEventListener("unload", this.destroy, false);
|
||||
|
||||
// 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);
|
||||
|
||||
// We are not allowing preset projects for now - rebuild a fresh one
|
||||
// each time.
|
||||
this.setProject(new Project({
|
||||
id: "",
|
||||
name: "",
|
||||
directories: [],
|
||||
openFiles: []
|
||||
}));
|
||||
|
||||
this._initPlugins();
|
||||
},
|
||||
|
||||
_buildMenubar: function () {
|
||||
|
||||
this.contextMenuPopup = this.document.getElementById("context-menu-popup");
|
||||
this.contextMenuPopup.addEventListener("popupshowing", this._updateContextMenuItems);
|
||||
|
||||
this.textEditorContextMenuPopup = this.document.getElementById("texteditor-context-popup");
|
||||
this.textEditorContextMenuPopup.addEventListener("popupshowing", this._updateMenuItems);
|
||||
|
||||
this.editMenu = this.document.getElementById("edit-menu");
|
||||
this.fileMenu = this.document.getElementById("file-menu");
|
||||
|
||||
this.editMenuPopup = this.document.getElementById("edit-menu-popup");
|
||||
this.fileMenuPopup = this.document.getElementById("file-menu-popup");
|
||||
this.editMenu.addEventListener("popupshowing", this._updateMenuItems);
|
||||
this.fileMenu.addEventListener("popupshowing", this._updateMenuItems);
|
||||
|
||||
if (this.menubar) {
|
||||
let body = this.menubar.ownerDocument.body ||
|
||||
this.menubar.ownerDocument.querySelector("window");
|
||||
body.appendChild(this.projectEditorCommandset);
|
||||
body.appendChild(this.projectEditorKeyset);
|
||||
body.appendChild(this.editorCommandset);
|
||||
body.appendChild(this.editorKeyset);
|
||||
body.appendChild(this.contextMenuPopup);
|
||||
body.appendChild(this.textEditorContextMenuPopup);
|
||||
|
||||
let index = this.menuindex || 0;
|
||||
this.menubar.insertBefore(this.editMenu, this.menubar.children[index]);
|
||||
this.menubar.insertBefore(this.fileMenu, this.menubar.children[index]);
|
||||
} else {
|
||||
this.document.getElementById("projecteditor-menubar").style.display = "block";
|
||||
}
|
||||
|
||||
// Insert a controller to allow enabling and disabling of menu items.
|
||||
this._commandWindow = this.editorCommandset.ownerDocument.defaultView;
|
||||
this._commandController = getCommandController(this);
|
||||
this._commandWindow.controllers.insertControllerAt(0, this._commandController);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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),
|
||||
contextMenuPopup: this.contextMenuPopup
|
||||
});
|
||||
on(this, this.projectTree, "selection", this._onTreeSelected);
|
||||
on(this, this.projectTree, "resource-removed", this._onTreeResourceRemoved);
|
||||
|
||||
let sourcesBox = this.document.querySelector("#sources > vbox");
|
||||
sourcesBox.appendChild(this.projectTree.elt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up listeners for commands to dispatch to all of the plugins
|
||||
*/
|
||||
_initCommands: function () {
|
||||
|
||||
this.projectEditorCommandset = this.document.getElementById("projecteditor-commandset");
|
||||
this.projectEditorKeyset = this.document.getElementById("projecteditor-keyset");
|
||||
|
||||
this.editorCommandset = this.document.getElementById("editMenuCommands");
|
||||
this.editorKeyset = this.document.getElementById("editMenuKeys");
|
||||
|
||||
this.projectEditorCommandset.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.
|
||||
*/
|
||||
_updateMenuItems: function () {
|
||||
let window = this.editMenu.ownerDocument.defaultView;
|
||||
let commands = ["cmd_undo", "cmd_redo", "cmd_delete", "cmd_cut", "cmd_copy", "cmd_paste"];
|
||||
commands.forEach(window.goUpdateCommand);
|
||||
|
||||
for (let c of this._pluginCommands.keys()) {
|
||||
window.goUpdateCommand(c);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable / disable necessary context menu items by passing an event
|
||||
* onto plugins.
|
||||
*/
|
||||
_updateContextMenuItems: function () {
|
||||
let resource = this.projectTree.getSelectedResource();
|
||||
this.pluginDispatch("onContextMenuOpen", resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy all objects on the iframe unload event.
|
||||
*/
|
||||
destroy: function () {
|
||||
this._destroyed = true;
|
||||
|
||||
|
||||
// If been destroyed before the iframe finished loading, then
|
||||
// the properties below will not exist.
|
||||
if (!this._loaded) {
|
||||
this.iframe.setAttribute("src", "about:blank");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the src for the iframe so if it reused for a new ProjectEditor
|
||||
// instance, the load will fire properly.
|
||||
this.window.removeEventListener("unload", this.destroy, false);
|
||||
this.iframe.setAttribute("src", "about:blank");
|
||||
|
||||
this._plugins.forEach(plugin => { plugin.destroy(); });
|
||||
|
||||
forget(this, this.projectTree);
|
||||
this.projectTree.destroy();
|
||||
this.projectTree = null;
|
||||
|
||||
this.shells.destroy();
|
||||
|
||||
this.projectEditorCommandset.remove();
|
||||
this.projectEditorKeyset.remove();
|
||||
this.editorCommandset.remove();
|
||||
this.editorKeyset.remove();
|
||||
this.contextMenuPopup.remove();
|
||||
this.textEditorContextMenuPopup.remove();
|
||||
this.editMenu.remove();
|
||||
this.fileMenu.remove();
|
||||
|
||||
this._commandWindow.controllers.removeController(this._commandController);
|
||||
this._commandController = null;
|
||||
|
||||
forget(this, this.project);
|
||||
this.project.destroy();
|
||||
this.project = 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) => {
|
||||
this.shells.removeResource(resource);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* - name: display name for project
|
||||
* - iconUrl: path to icon for project
|
||||
* - validationStatus: one of 'unknown|error|warning|valid'
|
||||
* - projectOverviewURL: path to load for iframe when project
|
||||
* is selected in the tree.
|
||||
* @param Promise
|
||||
* Promise that is resolved once the project is ready to be used.
|
||||
*/
|
||||
setProjectToAppPath: function (path, opts = {}) {
|
||||
this.project.appManagerOpts = opts;
|
||||
|
||||
let existingPaths = this.project.allPaths();
|
||||
if (existingPaths.length !== 1 || existingPaths[0] !== path) {
|
||||
// Only fully reset if this is a new path.
|
||||
this.project.removeAllStores();
|
||||
this.project.addPath(path);
|
||||
} else {
|
||||
// Otherwise, just ask for the root to be redrawn
|
||||
let rootResource = this.project.localStores.get(path).root;
|
||||
emit(rootResource, "label-change", rootResource);
|
||||
}
|
||||
|
||||
return this.project.refresh();
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a resource in a particular shell.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file to be opened.
|
||||
*/
|
||||
openResource: function (resource) {
|
||||
let shell = this.shells.open(resource);
|
||||
this.projectTree.selectResource(resource);
|
||||
shell.editor.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* When a node is removed, destroy it and its associated editor.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The resource being removed
|
||||
*/
|
||||
_onTreeResourceRemoved: function (resource) {
|
||||
this.shells.removeResource(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 (plugin, definition) {
|
||||
this._pluginCommands.set(definition.id, plugin);
|
||||
let document = this.projectEditorKeyset.ownerDocument;
|
||||
let command = document.createElement("command");
|
||||
command.setAttribute("id", definition.id);
|
||||
if (definition.key) {
|
||||
let key = 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.projectEditorKeyset.appendChild(key);
|
||||
}
|
||||
command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
|
||||
this.projectEditorCommandset.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, "saveRequested", "onEditorSaveRequested");
|
||||
this._editorListenAndDispatch(editor, "save", "onEditorSave");
|
||||
|
||||
editor.on("focus", () => {
|
||||
this.projectTree.selectResource(this.resourceFor(editor));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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) {
|
||||
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.classList.contains("pane-collapsed");
|
||||
},
|
||||
|
||||
get currentShell() {
|
||||
return this.shells.currentShell;
|
||||
},
|
||||
|
||||
get currentEditor() {
|
||||
return this.shells.currentEditor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether or not menu items should be able to be enabled.
|
||||
* Note that even if this is true, certain menu items will not be
|
||||
* enabled until the correct state is achieved (for instance, the
|
||||
* 'copy' menu item is only enabled when there is a selection).
|
||||
* But if this is false, then nothing will be enabled.
|
||||
*/
|
||||
set menuEnabled(val) {
|
||||
this._menuEnabled = val;
|
||||
if (this._loaded) {
|
||||
this._updateMenuItems();
|
||||
}
|
||||
},
|
||||
|
||||
get menuEnabled() {
|
||||
return this._menuEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Are there any unsaved resources in the Project?
|
||||
*/
|
||||
get hasUnsavedResources() {
|
||||
return this.project.allResources().some(resource=> {
|
||||
let editor = this.editorFor(resource);
|
||||
return editor && !editor.isClean();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Check with the user about navigating away with unsaved changes.
|
||||
*
|
||||
* @returns Boolean
|
||||
* True if there are no unsaved changes
|
||||
* Otherwise, ask the user to confirm and return the outcome.
|
||||
*/
|
||||
confirmUnsaved: function () {
|
||||
if (this.hasUnsavedResources) {
|
||||
return confirm(
|
||||
getLocalizedString("projecteditor.confirmUnsavedTitle"),
|
||||
getLocalizedString("projecteditor.confirmUnsavedLabel2")
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save all the changes in source files.
|
||||
*
|
||||
* @returns Boolean
|
||||
* True if there were resources to save.
|
||||
*/
|
||||
saveAllFiles: Task.async(function* () {
|
||||
if (this.hasUnsavedResources) {
|
||||
for (let resource of this.project.allResources()) {
|
||||
let editor = this.editorFor(resource);
|
||||
if (editor && !editor.isClean()) {
|
||||
yield editor.save(resource);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Returns a controller object that can be used for
|
||||
* editor-specific commands such as find, jump to line,
|
||||
* copy/paste, etc.
|
||||
*/
|
||||
function getCommandController(host) {
|
||||
return {
|
||||
supportsCommand: function (cmd) {
|
||||
return host._pluginCommands.get(cmd);
|
||||
},
|
||||
|
||||
isCommandEnabled: function (cmd) {
|
||||
if (!host.menuEnabled) {
|
||||
return false;
|
||||
}
|
||||
let plugin = host._pluginCommands.get(cmd);
|
||||
if (plugin && plugin.isCommandEnabled) {
|
||||
return plugin.isCommandEnabled(cmd);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
doCommand: function (cmd) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.ProjectEditor = ProjectEditor;
|
||||
243
devtools/client/projecteditor/lib/shells.js
Normal file
243
devtools/client/projecteditor/lib/shells.js
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { EditorTypeForResource } = require("devtools/client/projecteditor/lib/editors");
|
||||
const NetworkHelper = require("devtools/shared/webconsole/network-helper");
|
||||
const promise = require("promise");
|
||||
|
||||
/**
|
||||
* 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.classList.add("view-project-detail");
|
||||
this.elt.shell = this;
|
||||
|
||||
let constructor = this._editorTypeForResource();
|
||||
|
||||
this.editor = constructor(this.host);
|
||||
this.editor.shell = this;
|
||||
this.editorAppended = this.editor.appended;
|
||||
|
||||
this.editor.on("load", () => {
|
||||
this.editorDeferred.resolve();
|
||||
});
|
||||
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.editorDeferred = promise.defer();
|
||||
this.editorLoaded = this.editorDeferred.promise;
|
||||
this.editor.load(this.resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the shell and its associated editor
|
||||
*/
|
||||
destroy: function () {
|
||||
this.editor.destroy();
|
||||
this.resource.destroy();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the shell for a given resource.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
removeResource: function (resource) {
|
||||
let shell = this.shellFor(resource);
|
||||
if (shell) {
|
||||
this.shells.delete(resource);
|
||||
shell.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
for (let [resource, shell] of this.shells.entries()) {
|
||||
this.shells.delete(resource);
|
||||
shell.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Only reload the shell if the editor doesn't have local changes.
|
||||
if (shell.editor.isClean()) {
|
||||
shell.load();
|
||||
}
|
||||
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;
|
||||
58
devtools/client/projecteditor/lib/stores/base.js
Normal file
58
devtools/client/projecteditor/lib/stores/base.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const promise = require("promise");
|
||||
|
||||
/**
|
||||
* A Store object maintains a collection of Resource objects stored in a tree.
|
||||
*
|
||||
* The Store class should not be instantiated directly. Instead, you should
|
||||
* use a class extending it - right now this is only a LocalStore.
|
||||
*
|
||||
* Events:
|
||||
* This object emits the 'resource-added' and 'resource-removed' events.
|
||||
*/
|
||||
var Store = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* Should be called during initialize() of a subclass.
|
||||
*/
|
||||
initStore: function () {
|
||||
this.resources = new Map();
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a sorted Array of all Resources in the Store
|
||||
*/
|
||||
allResources: function () {
|
||||
var resources = [];
|
||||
function addResource(resource) {
|
||||
resources.push(resource);
|
||||
resource.childrenSorted.forEach(addResource);
|
||||
}
|
||||
addResource(this.root);
|
||||
return resources;
|
||||
},
|
||||
|
||||
notifyAdd: function (resource) {
|
||||
emit(this, "resource-added", resource);
|
||||
},
|
||||
|
||||
notifyRemove: function (resource) {
|
||||
emit(this, "resource-removed", resource);
|
||||
}
|
||||
});
|
||||
|
||||
exports.Store = Store;
|
||||
215
devtools/client/projecteditor/lib/stores/local.js
Normal file
215
devtools/client/projecteditor/lib/stores/local.js
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { OS } = require("resource://gre/modules/osfile.jsm");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { Store } = require("devtools/client/projecteditor/lib/stores/base");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const promise = require("promise");
|
||||
const Services = require("Services");
|
||||
const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event");
|
||||
const { FileResource } = require("devtools/client/projecteditor/lib/stores/resource");
|
||||
|
||||
const CHECK_LINKED_DIRECTORY_DELAY = 5000;
|
||||
const SHOULD_LIVE_REFRESH = true;
|
||||
// XXX: Ignores should be customizable
|
||||
const IGNORE_REGEX = /(^\.)|(\~$)|(^node_modules$)/;
|
||||
|
||||
/**
|
||||
* A LocalStore object maintains a collection of Resource objects
|
||||
* from the file system.
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "resource-added": When a resource is added
|
||||
* - "resource-removed": When a resource is removed
|
||||
*/
|
||||
var LocalStore = Class({
|
||||
extends: Store,
|
||||
|
||||
defaultCategory: "js",
|
||||
|
||||
initialize: function(path) {
|
||||
this.initStore();
|
||||
this.path = OS.Path.normalize(path);
|
||||
this.rootPath = this.path;
|
||||
this.displayName = this.path;
|
||||
this.root = this._forPath(this.path);
|
||||
this.notifyAdd(this.root);
|
||||
this.refreshLoop = this.refreshLoop.bind(this);
|
||||
this.refreshLoop();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
clearTimeout(this._refreshTimeout);
|
||||
|
||||
if (this._refreshDeferred) {
|
||||
this._refreshDeferred.reject("destroy");
|
||||
}
|
||||
if (this.worker) {
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
this._refreshTimeout = null;
|
||||
this._refreshDeferred = null;
|
||||
this.worker = null;
|
||||
|
||||
if (this.root) {
|
||||
forget(this, this.root);
|
||||
this.root.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
toString: function() { return "[LocalStore:" + this.path + "]" },
|
||||
|
||||
/**
|
||||
* Return a FileResource object for the given path. If a FileInfo
|
||||
* is provided the resource will use it, otherwise the FileResource
|
||||
* might not have full information until the next refresh.
|
||||
*
|
||||
* The following parameters are passed into the FileResource constructor
|
||||
* See resource.js for information about them
|
||||
*
|
||||
* @param String path
|
||||
* @param FileInfo info
|
||||
* @returns Resource
|
||||
*/
|
||||
_forPath: function(path, info=null) {
|
||||
if (this.resources.has(path)) {
|
||||
return this.resources.get(path);
|
||||
}
|
||||
|
||||
let resource = FileResource(this, path, info);
|
||||
this.resources.set(path, resource);
|
||||
return resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a promise that resolves to a fully-functional FileResource
|
||||
* within this project. This will hit the disk for stat info.
|
||||
* options:
|
||||
*
|
||||
* create: If true, a resource will be created even if the underlying
|
||||
* file doesn't exist.
|
||||
*/
|
||||
resourceFor: function(path, options) {
|
||||
path = OS.Path.normalize(path);
|
||||
|
||||
if (this.resources.has(path)) {
|
||||
return promise.resolve(this.resources.get(path));
|
||||
}
|
||||
|
||||
if (!this.contains(path)) {
|
||||
return promise.reject(new Error(path + " does not belong to " + this.path));
|
||||
}
|
||||
|
||||
return Task.spawn(function*() {
|
||||
let parent = yield this.resourceFor(OS.Path.dirname(path));
|
||||
|
||||
let info;
|
||||
try {
|
||||
info = yield OS.File.stat(path);
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
if (!options.create) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
let resource = this._forPath(path, info);
|
||||
parent.addChild(resource);
|
||||
return resource;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
refreshLoop: function() {
|
||||
// XXX: Once Bug 958280 adds a watch function, will not need to forever loop here.
|
||||
this.refresh().then(() => {
|
||||
if (SHOULD_LIVE_REFRESH) {
|
||||
this._refreshTimeout = setTimeout(this.refreshLoop,
|
||||
CHECK_LINKED_DIRECTORY_DELAY);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_refreshTimeout: null,
|
||||
_refreshDeferred: null,
|
||||
|
||||
/**
|
||||
* Refresh the directory structure.
|
||||
*/
|
||||
refresh: function(path=this.rootPath) {
|
||||
if (this._refreshDeferred) {
|
||||
return this._refreshDeferred.promise;
|
||||
}
|
||||
this._refreshDeferred = promise.defer();
|
||||
|
||||
let worker = this.worker = new ChromeWorker("chrome://devtools/content/projecteditor/lib/helpers/readdir.js");
|
||||
let start = Date.now();
|
||||
|
||||
worker.onmessage = evt => {
|
||||
// console.log("Directory read finished in " + ( Date.now() - start ) +"ms", evt);
|
||||
for (path in evt.data) {
|
||||
let info = evt.data[path];
|
||||
info.path = path;
|
||||
|
||||
let resource = this._forPath(path, info);
|
||||
resource.info = info;
|
||||
if (info.isDir) {
|
||||
let newChildren = new Set();
|
||||
for (let childPath of info.children) {
|
||||
childInfo = evt.data[childPath];
|
||||
newChildren.add(this._forPath(childPath, childInfo));
|
||||
}
|
||||
resource.setChildren(newChildren);
|
||||
}
|
||||
resource.info.children = null;
|
||||
}
|
||||
|
||||
worker = null;
|
||||
this._refreshDeferred.resolve();
|
||||
this._refreshDeferred = null;
|
||||
};
|
||||
worker.onerror = ex => {
|
||||
console.error(ex);
|
||||
worker = null;
|
||||
this._refreshDeferred.reject(ex);
|
||||
this._refreshDeferred = null;
|
||||
}
|
||||
worker.postMessage({ path: this.rootPath, ignore: IGNORE_REGEX });
|
||||
return this._refreshDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the given path would be a child of the store's
|
||||
* root directory.
|
||||
*/
|
||||
contains: function(path) {
|
||||
path = OS.Path.normalize(path);
|
||||
let thisPath = OS.Path.split(this.rootPath);
|
||||
let thatPath = OS.Path.split(path)
|
||||
|
||||
if (!(thisPath.absolute && thatPath.absolute)) {
|
||||
throw new Error("Contains only works with absolute paths.");
|
||||
}
|
||||
|
||||
if (thisPath.winDrive && (thisPath.winDrive != thatPath.winDrive)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (thatPath.components.length <= thisPath.components.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < thisPath.components.length; i++) {
|
||||
if (thisPath.components[i] != thatPath.components[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
exports.LocalStore = LocalStore;
|
||||
11
devtools/client/projecteditor/lib/stores/moz.build
Normal file
11
devtools/client/projecteditor/lib/stores/moz.build
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'base.js',
|
||||
'local.js',
|
||||
'resource.js',
|
||||
)
|
||||
398
devtools/client/projecteditor/lib/stores/resource.js
Normal file
398
devtools/client/projecteditor/lib/stores/resource.js
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { TextEncoder, TextDecoder } = require("sdk/io/buffer");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const URL = require("sdk/url");
|
||||
const promise = require("promise");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
|
||||
const { Task } = require("devtools/shared/task");
|
||||
|
||||
const gDecoder = new TextDecoder();
|
||||
const gEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* A Resource is a single file-like object that can be respresented
|
||||
* as a file for ProjectEditor.
|
||||
*
|
||||
* The Resource class is not exported, and should not be instantiated
|
||||
* Instead, you should use the FileResource class that extends it.
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "children-changed": When a child has been added or removed.
|
||||
* See setChildren.
|
||||
* - "deleted": When the resource has been deleted.
|
||||
*/
|
||||
var Resource = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
refresh: function () { return promise.resolve(this); },
|
||||
destroy: function () { },
|
||||
delete: function () { },
|
||||
|
||||
setURI: function (uri) {
|
||||
if (typeof (uri) === "string") {
|
||||
uri = URL.URL(uri);
|
||||
}
|
||||
this.uri = uri;
|
||||
},
|
||||
|
||||
/**
|
||||
* Is there more than 1 child Resource?
|
||||
*/
|
||||
get hasChildren() { return this.children && this.children.size > 0; },
|
||||
|
||||
/**
|
||||
* Is this Resource the root (top level for the store)?
|
||||
*/
|
||||
get isRoot() {
|
||||
return !this.parent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sorted array of children for display
|
||||
*/
|
||||
get childrenSorted() {
|
||||
if (!this.hasChildren) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...this.children].sort((a, b)=> {
|
||||
// Put directories above files.
|
||||
if (a.isDir !== b.isDir) {
|
||||
return b.isDir;
|
||||
}
|
||||
return a.basename.toLowerCase() > b.basename.toLowerCase();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the children set of this Resource, and notify of any
|
||||
* additions / removals that happened in the change.
|
||||
*/
|
||||
setChildren: function (newChildren) {
|
||||
let oldChildren = this.children || new Set();
|
||||
let change = false;
|
||||
|
||||
for (let child of oldChildren) {
|
||||
if (!newChildren.has(child)) {
|
||||
change = true;
|
||||
child.parent = null;
|
||||
this.store.notifyRemove(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (let child of newChildren) {
|
||||
if (!oldChildren.has(child)) {
|
||||
change = true;
|
||||
child.parent = this;
|
||||
this.store.notifyAdd(child);
|
||||
}
|
||||
}
|
||||
|
||||
this.children = newChildren;
|
||||
if (change) {
|
||||
emit(this, "children-changed", this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a resource to children set and notify of the change.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
addChild: function (resource) {
|
||||
this.children = this.children || new Set();
|
||||
|
||||
resource.parent = this;
|
||||
this.children.add(resource);
|
||||
this.store.notifyAdd(resource);
|
||||
emit(this, "children-changed", this);
|
||||
return resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if current object has child with specific name.
|
||||
*
|
||||
* @param string name
|
||||
*/
|
||||
hasChild: function (name) {
|
||||
for (let child of this.children) {
|
||||
if (child.basename === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a resource to children set and notify of the change.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
removeChild: function (resource) {
|
||||
resource.parent = null;
|
||||
this.children.remove(resource);
|
||||
this.store.notifyRemove(resource);
|
||||
emit(this, "children-changed", this);
|
||||
return resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a set with children, children of children, etc -
|
||||
* gathered recursively.
|
||||
*
|
||||
* @returns Set<Resource>
|
||||
*/
|
||||
allDescendants: function () {
|
||||
let set = new Set();
|
||||
|
||||
function addChildren(item) {
|
||||
if (!item.children) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let child of item.children) {
|
||||
set.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
addChildren(this);
|
||||
for (let item of set) {
|
||||
addChildren(item);
|
||||
}
|
||||
|
||||
return set;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A FileResource is an implementation of Resource for a File System
|
||||
* backing. This is exported, and should be used instead of Resource.
|
||||
*/
|
||||
var FileResource = Class({
|
||||
extends: Resource,
|
||||
|
||||
/**
|
||||
* @param Store store
|
||||
* @param String path
|
||||
* @param FileInfo info
|
||||
* https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info
|
||||
*/
|
||||
initialize: function (store, path, info) {
|
||||
this.store = store;
|
||||
this.path = path;
|
||||
|
||||
this.setURI(URL.URL(URL.fromFilename(path)));
|
||||
this._lastReadModification = undefined;
|
||||
|
||||
this.info = info;
|
||||
this.parent = null;
|
||||
},
|
||||
|
||||
toString: function () {
|
||||
return "[FileResource:" + this.path + "]";
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (this._refreshDeferred) {
|
||||
this._refreshDeferred.reject();
|
||||
}
|
||||
this._refreshDeferred = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch and cache information about this particular file.
|
||||
* https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File_for_the_main_thread#OS.File.stat
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves once the File.stat has finished.
|
||||
*/
|
||||
refresh: function () {
|
||||
if (this._refreshDeferred) {
|
||||
return this._refreshDeferred.promise;
|
||||
}
|
||||
this._refreshDeferred = promise.defer();
|
||||
OS.File.stat(this.path).then(info => {
|
||||
this.info = info;
|
||||
if (this._refreshDeferred) {
|
||||
this._refreshDeferred.resolve(this);
|
||||
this._refreshDeferred = null;
|
||||
}
|
||||
});
|
||||
return this._refreshDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the trailing name component of this Resource
|
||||
*/
|
||||
get basename() {
|
||||
return this.path.replace(/\/+$/, "").replace(/\\/g, "/").replace(/.*\//, "");
|
||||
},
|
||||
|
||||
/**
|
||||
* A string to be used when displaying this Resource in views
|
||||
*/
|
||||
get displayName() {
|
||||
return this.basename + (this.isDir ? "/" : "");
|
||||
},
|
||||
|
||||
/**
|
||||
* Is this FileResource a directory? Rather than checking children
|
||||
* here, we use this.info. So this could return a false negative
|
||||
* if there was no info passed in on constructor and the first
|
||||
* refresh hasn't yet finished.
|
||||
*/
|
||||
get isDir() {
|
||||
if (!this.info) { return false; }
|
||||
return this.info.isDir && !this.info.isSymLink;
|
||||
},
|
||||
|
||||
/**
|
||||
* Read the file as a string asynchronously.
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves with the text of the file.
|
||||
*/
|
||||
load: function () {
|
||||
return OS.File.read(this.path).then(bytes => {
|
||||
return gDecoder.decode(bytes);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the file from the filesystem
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves when the file is deleted
|
||||
*/
|
||||
delete: function () {
|
||||
emit(this, "deleted", this);
|
||||
if (this.isDir) {
|
||||
return OS.File.removeDir(this.path);
|
||||
} else {
|
||||
return OS.File.remove(this.path);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a text file as a child of this FileResource.
|
||||
* This instance must be a directory.
|
||||
*
|
||||
* @param string name
|
||||
* The filename (path will be generated based on this.path).
|
||||
* string initial
|
||||
* The content to write to the new file.
|
||||
* @returns Promise
|
||||
* Resolves with the new FileResource once it has
|
||||
* been written to disk.
|
||||
* Rejected if this is not a directory.
|
||||
*/
|
||||
createChild: function (name, initial = "") {
|
||||
if (!this.isDir) {
|
||||
return promise.reject(new Error("Cannot add child to a regular file"));
|
||||
}
|
||||
|
||||
let newPath = OS.Path.join(this.path, name);
|
||||
|
||||
let buffer = initial ? gEncoder.encode(initial) : "";
|
||||
return OS.File.writeAtomic(newPath, buffer, {
|
||||
noOverwrite: true
|
||||
}).then(() => {
|
||||
return this.store.refresh();
|
||||
}).then(() => {
|
||||
let resource = this.store.resources.get(newPath);
|
||||
if (!resource) {
|
||||
throw new Error("Error creating " + newPath);
|
||||
}
|
||||
return resource;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename the file from the filesystem
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves with the renamed FileResource.
|
||||
*/
|
||||
rename: function (oldName, newName) {
|
||||
let oldPath = OS.Path.join(this.path, oldName);
|
||||
let newPath = OS.Path.join(this.path, newName);
|
||||
|
||||
return OS.File.move(oldPath, newPath).then(() => {
|
||||
return this.store.refresh();
|
||||
}).then(() => {
|
||||
let resource = this.store.resources.get(newPath);
|
||||
if (!resource) {
|
||||
throw new Error("Error creating " + newPath);
|
||||
}
|
||||
return resource;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Write a string to this file.
|
||||
*
|
||||
* @param string content
|
||||
* @returns Promise
|
||||
* Resolves once it has been written to disk.
|
||||
* Rejected if there is an error
|
||||
*/
|
||||
save: Task.async(function* (content) {
|
||||
// XXX: writeAtomic was losing permissions after saving on OSX
|
||||
// return OS.File.writeAtomic(this.path, buffer, { tmpPath: this.path + ".tmp" });
|
||||
let buffer = gEncoder.encode(content);
|
||||
let path = this.path;
|
||||
let file = yield OS.File.open(path, {truncate: true});
|
||||
yield file.write(buffer);
|
||||
yield file.close();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Attempts to get the content type from the file.
|
||||
*/
|
||||
get contentType() {
|
||||
if (this._contentType) {
|
||||
return this._contentType;
|
||||
}
|
||||
if (this.isDir) {
|
||||
return "x-directory/normal";
|
||||
}
|
||||
try {
|
||||
this._contentType = mimeService.getTypeFromFile(new FileUtils.File(this.path));
|
||||
} catch (ex) {
|
||||
if (ex.name !== "NS_ERROR_NOT_AVAILABLE" &&
|
||||
ex.name !== "NS_ERROR_FAILURE") {
|
||||
console.error(ex, this.path);
|
||||
}
|
||||
this._contentType = null;
|
||||
}
|
||||
return this._contentType;
|
||||
},
|
||||
|
||||
/**
|
||||
* A string used when determining the type of Editor to open for this.
|
||||
* See editors.js -> EditorTypeForResource.
|
||||
*/
|
||||
get contentCategory() {
|
||||
const NetworkHelper = require("devtools/shared/webconsole/network-helper");
|
||||
let category = NetworkHelper.mimeCategoryMap[this.contentType];
|
||||
// Special treatment for manifest.webapp.
|
||||
if (!category && this.basename === "manifest.webapp") {
|
||||
return "json";
|
||||
}
|
||||
return category || "txt";
|
||||
}
|
||||
});
|
||||
|
||||
exports.FileResource = FileResource;
|
||||
593
devtools/client/projecteditor/lib/tree.js
Normal file
593
devtools/client/projecteditor/lib/tree.js
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { merge } = require("sdk/util/object");
|
||||
const promise = require("promise");
|
||||
const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
|
||||
const { on, forget } = require("devtools/client/projecteditor/lib/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("entry");
|
||||
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.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) => {
|
||||
this.select();
|
||||
this.toggleExpansion();
|
||||
evt.stopPropagation();
|
||||
}, false);
|
||||
this.expander.addEventListener("click", (evt) => {
|
||||
this.toggleExpansion();
|
||||
this.select();
|
||||
evt.stopPropagation();
|
||||
}, true);
|
||||
|
||||
if (!this.resource.isRoot) {
|
||||
this.expanded = false;
|
||||
}
|
||||
this.update();
|
||||
},
|
||||
|
||||
toggleExpansion: function () {
|
||||
if (!this.resource.isRoot) {
|
||||
this.expanded = !this.expanded;
|
||||
} else {
|
||||
this.expanded = true;
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.elt.remove();
|
||||
this.expander.remove();
|
||||
this.highlighter.remove();
|
||||
this.children.remove();
|
||||
this.label.remove();
|
||||
this.elt = this.expander = 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.options.contextMenuPopup;
|
||||
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.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
|
||||
* - contextMenuPopup: a <menupopup> element
|
||||
* - 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 (doc, options) {
|
||||
this.doc = doc;
|
||||
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 = this.doc.createElementNS(HTML_NS, "div");
|
||||
this.elt.tree = this;
|
||||
this.elt.className = "sources-tree";
|
||||
this.elt.setAttribute("with-arrows", "true");
|
||||
this.elt.setAttribute("theme", "dark");
|
||||
this.elt.setAttribute("flex", "1");
|
||||
|
||||
this.children = this.doc.createElementNS(HTML_NS, "ul");
|
||||
this.elt.appendChild(this.children);
|
||||
|
||||
this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this);
|
||||
this.removeResource = this.removeResource.bind(this);
|
||||
this.updateResource = this.updateResource.bind(this);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this._destroyed = true;
|
||||
this.elt.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to create DOM elements for promptNew and promptEdit
|
||||
*/
|
||||
createInputContainer: function () {
|
||||
let inputholder = this.doc.createElementNS(HTML_NS, "div");
|
||||
inputholder.className = "child entry";
|
||||
|
||||
let expander = this.doc.createElementNS(HTML_NS, "span");
|
||||
expander.className = "arrow expander";
|
||||
expander.setAttribute("invisible", "");
|
||||
inputholder.appendChild(expander);
|
||||
|
||||
let placeholder = this.doc.createElementNS(HTML_NS, "div");
|
||||
placeholder.className = "child";
|
||||
inputholder.appendChild(placeholder);
|
||||
|
||||
return {inputholder, placeholder};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 {inputholder, placeholder} = this.createInputContainer();
|
||||
item.appendChild(inputholder);
|
||||
|
||||
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,
|
||||
preserveTextStyles: true,
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompt the user to rename file in the tree.
|
||||
*
|
||||
* @param string initial
|
||||
* The suggested starting file name
|
||||
* @param resource
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves once the prompt has been successful,
|
||||
* Rejected if it is cancelled
|
||||
*/
|
||||
promptEdit: function (initial, resource) {
|
||||
let deferred = promise.defer();
|
||||
let item = this._containers.get(resource).elt;
|
||||
let originalText = item.childNodes[0];
|
||||
|
||||
let {inputholder, placeholder} = this.createInputContainer();
|
||||
item.insertBefore(inputholder, originalText);
|
||||
|
||||
item.removeChild(originalText);
|
||||
|
||||
new InplaceEditor({
|
||||
element: placeholder,
|
||||
initial: initial,
|
||||
preserveTextStyles: true,
|
||||
start: editor => {
|
||||
editor.input.select();
|
||||
},
|
||||
done: function (val, commit) {
|
||||
if (val === initial) {
|
||||
item.insertBefore(originalText, inputholder);
|
||||
}
|
||||
|
||||
item.removeChild(inputholder);
|
||||
|
||||
if (commit) {
|
||||
deferred.resolve(val);
|
||||
} else {
|
||||
deferred.reject(val);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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("entry-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);
|
||||
on(this, resource, "deleted", this.removeResource);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
forget(this, resource);
|
||||
if (this._containers.get(resource)) {
|
||||
this._containers.get(resource).destroy();
|
||||
this._containers.delete(resource);
|
||||
}
|
||||
emit(this, "resource-removed", 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue