diff options
Diffstat (limited to 'devtools/client/projecteditor/lib/editors.js')
-rw-r--r-- | devtools/client/projecteditor/lib/editors.js | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/devtools/client/projecteditor/lib/editors.js b/devtools/client/projecteditor/lib/editors.js new file mode 100644 index 000000000..7d0150cf7 --- /dev/null +++ b/devtools/client/projecteditor/lib/editors.js @@ -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; |