{"__v":176,"_id":"54d5635632d98b0d00384b05","category":{"__v":0,"_id":"54e68e62a43fe13500db3879","pages":[],"project":"54d53c7b23010a0d001aca0c","version":"54d5635532d98b0d00384afb","sync":{"url":"","isSync":false},"reference":false,"createdAt":"2015-02-20T01:31:13.999Z","from_sync":false,"order":5,"slug":"editor-plugins","title":"Editor Plugins"},"parentDoc":null,"project":"54d53c7b23010a0d001aca0c","user":"54cfa8e1a8a4fd0d00b7fd1d","version":{"__v":10,"_id":"54d5635532d98b0d00384afb","forked_from":"54d53c7c23010a0d001aca0f","project":"54d53c7b23010a0d001aca0c","createdAt":"2015-02-07T00:59:01.934Z","releaseDate":"2015-02-07T00:59:01.934Z","categories":["54d5635632d98b0d00384afc","54d5635632d98b0d00384afd","54d5635632d98b0d00384afe","54d5635632d98b0d00384aff","54d5635632d98b0d00384b00","54d5635632d98b0d00384b01","54d5635632d98b0d00384b02","54d652097e05890d006f153e","54dd1315ca1e5219007e9daa","54e21e2b22de1c230094b147","54e68e62a43fe13500db3879","54fa1d3fe7a0ba2f00306309","551c453a23a1ee190034d19a","551df586e52a0b23000c62b6","551f39be6886f8230055f02a","55a6720751457325000e4d97"],"is_deprecated":false,"is_hidden":false,"is_beta":true,"is_stable":true,"codename":"","version_clean":"0.1.0","version":"0.1"},"updates":["590c203d4f650b1900d27daa"],"next":{"pages":[],"description":""},"createdAt":"2015-02-06T23:57:46.704Z","link_external":false,"link_url":"","githubsync":"","sync_unique":"","hidden":false,"api":{"results":{"codes":[]},"auth":"required","params":[],"url":""},"isReference":false,"order":4,"body":"Some editors display the contents of documents and allow the user to manipulate that content with a set of tools. Other editors, such as the terminal, don't interact with contents but allow the user to interact with other resources. This article is geared towards creating a custom editor.\n[block:callout]\n{\n  \"type\": \"warning\",\n  \"title\": \"Creating Editors\",\n  \"body\": \"In most cases editors are created automatically for the panes they belong too. However it is possible to create editors and use them outside of panes. For instance an experimental and yet to be released plugin that creates a dashboard uses editors displayed in dashboard widgets instead of tabs.\"\n}\n[/block]\n# Object structure\n\nBesides tabs and panes there are several other objects involved in displaying content and making it editable. The following hierarchy describes the relationships between those objects.\n\n- [Pane](https://docs.c9.io/api/#!/api/Pane) - Represent a single pane, housing multiple tabs\n  - [Editor](https://docs.c9.io/api/#!/api/Editor) - The editor responsible for displaying the document in the tab\n  - [Tab](https://docs.c9.io/api/#!/api/Tab) - A single tab (button) in a pane\n    - [Document](https://docs.c9.io/api/#!/api/Document) - The representation of a file in the tab\n      - [Session](https://docs.c9.io/api/#!/api/Session) - The session information of the editor\n      - [UndoManager](https://docs.c9.io/api/#!/api/UndoManager) - The object that manages the undo stack for this document\n\nIt is important to note that the `Editor` plugin(s) are related to the pane, not the tab. There's only one instance of a certain editor for each pane. The editor is responsible for displaying the content of a document based on the tab that is active. There can be a maximum of one tab active per pane.\n\n# Creating a Basic Editor\n\n## Register your Editor\n\nStart by consuming the `editors` plugin and `Editor` [base class](doc:base-classes) which helps creating and registering your editor.\n\n```\nvar Editor = imports.Editor;\nvar editors = imports.editors;\n```\n\nThen define the extensions that your editor can open. If there are none, leave the array empty.\n```\nvar extensions = [\"foo\", \"bar\"];\n```\n\nRegister your editor by giving it an id, name, factory reference and list of extensions. The `handle` which is returned should be passed to the register function.\n```\nvar handle = editors.register(\"foo\", \"Foo\", FooEditor, extensions);\n\nregister(null, {\n    \"my-editor\": handle\n});\n```\n\n## Create a Factory for your Editor\n\nThe factory function mentioned above creates a new `Editor` plugin and this is where the meat of your implementation will be. The following code should all be [straightforward](doc:plugin).\n```\nfunction FooEditor(){\n    var plugin = new Editor(\"Your Name\", main.consumes, extensions);\n\n    // Register the API\n    plugin.freezePublicAPI({});\n    \n    // Initialize the Editor\n    plugin.load(null, \"my-plugin\");\n    \n    return plugin;\n}\n```\n\nThe next sections will tell you how to extend this basic editor and make it function.\n\n# Draw the UI\n\nBefore anything else your plugin *must* draw it's user interface. The `draw` event is the designated place and time for this. This event is called the first time a tab for this editor becomes active in a pane. Usually this is immediately after the editor is created.\n\nIn this example the UI for the editor is loaded from an html file.\n```\nvar container, contents;\nplugin.on(\"draw\", function(e) {\n    container = e.htmlNode;\n    \n    // Insert the UI\n    ui.insertHTML(container, require(\"text!./editor.html\"), plugin);\n    \n    contents = container.querySelector(\".contents\");\n});\n```\n[block:callout]\n{\n  \"type\": \"warning\",\n  \"title\": \"Don't load CSS in the draw event\",\n  \"body\": \"Since the draw event is called for each instance of the editor it is a *bad* idea to load css here. Instead load css in function that can only be called once and is defined outside the factory function of the editor.\"\n}\n[/block]\n# Documents\n\nDealing with documents is the main responsibility of the editor. Documents are loaded, activated and unloaded as tabs in a pane are created, become active and are closed. Your editor is responsible for:\n\n- Rendering the contents of the document\n- Serializing the state of the document when requested\n- Setting the title of the document\n- Rendering the progress events of the document\n- Cleaning up after a document is unloaded\n\n*See also the [guide on document sessions](http://cloud9-sdk.readme.io/v0.1/docs/documents#sessions) for more information on how sessions work*\n\n## Loading\n\nWhen a user opens a tab, for instance by double clicking on the file tree, a document is created and loaded into the appropriate editor. The `documentLoad` event is then called. Use this event to prepare the document's session to be displayed in the editor. It is only when the tab becomes active that the document will actually be displayed.\n\nThe following examples use a canvas element to render the contents of a document. Lets start by creating a canvas element in the `documentLoad` handler.\n```javascript\nplugin.on(\"documentLoad\", function(e){\n    var doc = e.doc;\n    var session = doc.getSession();\n\n    session.canvas = document.createElement(\"canvas\");    \n});\n```\n\nSessions are loaded and unloaded when the document moves between editors. In some cases you want to apply only some of the code in the `documentLoad` event handler to the session. See the [session guide](http://cloud9-sdk.readme.io/v0.1/docs/documents#sessions) for more information. \n[block:callout]\n{\n  \"type\": \"success\",\n  \"title\": \"Add the next examples to the documentLoad handler\",\n  \"body\": \"The following examples are to be added to the `documentLoad` event handler\"\n}\n[/block]\n### Document Value\n\nA custom plugin *should* process the contents of a document via the `setValue` event. Use this event to render the contents of the file (a string) in which ever way you want. In the example below we pass it to a function that will render the contents to the canvas element.\n```javascript\ndoc.on(\"setValue\", function get(e) {\n    renderOnCanvas(e.value, canvas);\n}, session);\n```\n\nWhen the user uses your editor, it will most likely change the contents of the document. At some point the contents of the document will need to be serialized back into a string. For instance the save plugin will request state serialization when it wants to store the contents on disk. A custom plugin *should* implement a `getValue` handler to serialize the document's contents.\n```javascript\ndoc.on(\"getValue\", function get(e) {\n    return doc.changed\n        ? serializeValue(session)\n        : e.value;\n}, session);\n```\nAs an optimization, in the example above, the state is only serialized if the document actually changed. Otherwise the cached value is used.\n\n### Loading Progress\n\nYour editor plugin *could* implement a progress indicator based on the `progress` event of the document. For more information see [this article](http://cloud9-sdk.readme.io/v0.1/docs/documents#progress).\n\n```javascript\ndoc.on(\"progress\", function(e){\n    if (e.complete)\n        hideProgress();\n    else\n        showProgress(e.loaded, e.total);\n}, session);\n```\n\n### Tab Title\n\nManaging the title of the document is task your editor plugin *must* do. For instance the terminal plugin sets the title to the title from the tty. The ace plugin sets it to the filename of the file that's opened. \n\nThis code example shows you how to do that.\n```javascript\ndoc.tab.on(\"setPath\", setTitle, session);\n\nfunction setTitle(e) {\n    var path = doc.tab.path;\n    \n    // Caption is the filename\n    doc.title = basename(path);\n    \n    // Tooltip is the full path\n    doc.tooltip = path;\n}\nsetTitle();\n```\nNote that the `basename()` function comes from `require(\"path\").basename`.\n\n### Cloud9 Theme\n\nSetting the background color of a tab is the last thing an editor plugin *must* do. Often editor plugins want to change the color based on the theme. The following example shows how to accomplish this.\n\n```javascript\nfunction setTheme(e) {\n    var tab = doc.tab;\n    var isDark = e.theme == \"dark\";\n    \n    tab.backgroundColor = BGCOLOR[e.theme];\n    \n    if (isDark) tab.classList.add(\"dark\");\n    else tab.classList.remove(\"dark\");\n}\n\nlayout.on(\"themeChange\", setTheme, session);\nsetTheme({ theme: settings.get(\"user/general/:::at:::skin\") });\n```\nIn this example `BGCOLOR` is a hash of the default Cloud9 themes as keys and colors as values. *We're working on a way to move styling for this to the themes. Stay tuned.*\n\n### Document Unload\n\nFor some editors, such as the terminal, the unload of the session is best to occur in the `unload` event of the document. (For more information on unloading see [this article](http://cloud9-sdk.readme.io/v0.1/docs/documents#unloading)). The following code example come straight from the terminal source code.\n\n```javascript\ndoc.on(\"unload\", function(){\n    // Stop the shell process at the remote machine\n    session.kill();\n    \n    // Destroy the terminal\n    if (session.terminal)\n        session.terminal.destroy();\n});\n```\n\n## Activating\n\nWhenever a tab becomes active, the editor for that document becomes visible and the `documentActivate` event is fired on the editor. An editor plugin *must* implement this event to hide any other document that is displayed and show the contents of the document that is now active.\n\nMost plugins keep a reference to the active document in a local variable such as `currentDocument` or to the session as `currentSession`. This makes it easy to work with the currently active document. \n\nThis example swaps out the canvas element and displays the one of the active document.\n```javascript\nplugin.on(\"documentActivate\", function(e){\n    currentDocument = e.doc;\n    currentSession = e.doc.getSession();\n\n    // Remove a previous canvas element, if any\n    if (contents.firstChild)\n        contents.removeChild(contents.firstChild); \n\n    // Set the canvas element to be displayed\n    contents.appendChild(currentSession.canvas);\n});\n```\n\nThe active document of an editor can be retrieved like this.\n```javascript\nvar doc = editor.activeDocument;\n```\n\n## Unloading\n\nThe unloading topic is extensively covered in the [document guide](http://cloud9-sdk.readme.io/v0.1/docs/documents#unloading). We refer to that section to learn about using the `documentUnload` event.\n\n# Clipboard\n\nYour editor plugin can interact with clipboard actions using three events for `copy`, `cut` and `paste`.\n\n## Copy\nThe copy event is triggered when a user hits `Cmd-C` or `Ctrl-C`, uses the `Edit/Copy` menu item or the `copy` command is triggered programmatically.\n\nThe `clipboardData` object provides an API to set and retrieve data from the clipboard.\n```javascript\nplugin.on(\"copy\", function(e) {\n    var data = getSerializedSelection();\n    e.clipboardData.setData(\"text/plain\", data);\n});\n```\n\n## Cut\nThe cut event is triggered when a user hits `Cmd-X` or `Ctrl-X`, uses the `Edit/Cut` menu item or the `cut` command is triggered programmatically.\n\n```javascript\nplugin.on(\"cut\", function(e) {\n    var data = getSerializedSelection();\n    removeSelection();\n    e.clipboardData.setData(\"text/plain\", data);\n});\n```\n\n## Paste\nThe paste event is triggered when a user hits `Cmd-V` or `Ctrl-V`, uses the `Edit/Paste` menu item or the `paste` command is triggered programmatically.\n\n```javascript\nplugin.on(\"paste\", function(e) {\n    var data = e.clipboardData.getData(\"text/plain\");\n    if (data !== false)\n        replaceSelection(data);\n});\n```\n\n# Serializing and Deserializing\n\nWhen the [document is serialized and deserialized](http://cloud9-sdk.readme.io/v0.1/docs/documents#serializing-and-deserializing-state) the `getState` and `setState` events are called. This serialization step preserves the state of the editor between browser reloads.\n\nIn the following example, we'll store the scroll position of our editor in the state.\n```javascript\nplugin.on(\"getState\", function(e) {\n    var session = e.doc.getSession();\n    \n    e.state.scrollTop = session.scrollTop;\n    e.state.scrollLeft = session.scrollLeft;\n});\n```\n\nWhen the state gets restored, we can restore the scroll position.\n```javascript\nplugin.on(\"setState\", function(e) {\n    var session = e.doc.getSession();\n    \n    session.scrollTop = e.state.scrollTop;\n    session.scrollLeft = e.state.scrollLeft;\n    \n    if (session == currentSession)\n        session.update();\n});\n```\n\nNote that we are reading and setting the state on the session object. Check out the following code that serves as an example on how you could manage the state of the session.\n\n```javascript\nplugin.on(\"draw\", function(e){\n    // Set the scroll state on the session when it changes.\n    contents.addEventListener(\"scroll\", function(){\n        currentSession.scrollLeft = this.scrollLeft;\n        currentSession.scrollTop = this.scrollTop;\n    });\n});\n\nplugin.on(\"documentLoad\", function(e){\n    var session = e.doc.getSession();\n    \n    // Define a way to update the scroll state for that session\n    session.update = function(){\n        contents.scrollTop = session.scrollTop;\n        contents.scrollLeft = session.scrollLeft;\n    }\n});\n\nplugin.on(\"documentActivate\", function(e){\n    currentSession = e.doc.getSession();\n    \n    // Call the update function when this session becomes active\n    session.update();\n});\n```\nNote that the update function could manage all the state changes, including appending the canvas to the `contents` DIV.\n\n# Focus & Blur\n\nA plugin editor *should* indicate to the user when it is focussed and when it is blurred. The `focus` and `blur` events will be called at the appropriate time.\n\n```javascript\nplugin.on(\"focus\", function(){\n    contents.className = \"contents focus\";\n});\nplugin.on(\"blur\", function(){\n    contents.className = \"contents\";\n});\n```\n\n# Resize\n\nWhen the container that is assigned to the editor *potentially* changes in dimension the resize event is triggered. Hook this event to recalculate any positioning that is not done through css.\n\n```javascript\nplugin.on(\"resize\", function(){\n    contents.style.width = (contents.parentNode.offsetWidth - 2) + \"px\";\n});\n```\n# Switching Editors\n\nWhen `tab.switchEditor()` is called to switch the editor that has loaded the document (for instance from `ace` to `hex`, or from an `imgeditor` to `hex`) some editors want to validate the contents of the document before switching. For instance if your editor is a GUI drag&drop editor which stores the contents in JSON. When switching from ace back to your editor, you'll want to make sure the JSON is valid before allowing the switch.\n\nThe `validate` event allows your editor to implement validation logic.\n```javascript\nplugin.on(\"validate\", function(e){\n    var doc = e.document;\n    var info = e.info;\n    \n    try {\n        var json = JSON.parse(doc.value);\n    } catch(e) {\n        info.title = \"Invalid JSON\";\n        info.head = \"Could not switch to GUI editor\";\n        info.message = \"Invalid JSON was detected: \" + e.message;\n        return false;\n    }\n    \n    return true;\n});\n```\nThe values set on the info object will be used to alert the user of the issue.\n\n# Full Example\n\n```javascript\ndefine(function(require, exports, module) {\n    main.consumes = [\"Editor\", \"editors\", \"ui\", \"settings\", \"layout\"];\n    main.provides = [\"my-editor\"];\n    return main;\n\n    function main(options, imports, register) {\n        var Editor = imports.Editor;\n        var editors = imports.editors;\n        var settings = imports.settings;\n        var layout = imports.layout;\n        var ui = imports.ui;\n        \n        var basename = require(\"path\").basename;\n        \n        // Set extensions if any for your editor\n        var extensions = [\"foo\", \"bar\"];\n        \n        // Register the editor\n        var handle = editors.register(\"foo\", \"Foo\", FooEditor, extensions);\n        \n        var BGCOLOR = { \n            \"flat-light\": \"#F1F1F1\", \n            \"light\": \"#D3D3D3\", \n            \"light-gray\": \"#D3D3D3\",\n            \"dark\": \"#3D3D3D\",\n            \"dark-gray\": \"#3D3D3D\" \n        };\n                          \n        function FooEditor(){\n            var plugin = new Editor(\"Your Name\", main.consumes, extensions);\n            \n            var container, contents;\n            var currentSession, currentDocument;\n            \n            plugin.on(\"draw\", function(e) {\n                container = e.htmlNode;\n                \n                // Insert the UI\n                ui.insertHTML(container, require(\"text!./editor.html\"), plugin);\n                \n                contents = container.querySelector(\".contents\");\n                \n                // Set the scroll state on the session when it changes.\n                contents.addEventListener(\"scroll\", function(){\n                    currentSession.scrollLeft = this.scrollLeft;\n                    currentSession.scrollTop = this.scrollTop;\n                });\n            });\n            \n            function renderOnCanvas(){}\n            function hideProgress(){}\n            function showProgress(){}\n            function serializeValue(){}\n            function getSerializedSelection(){}\n            function removeSelection(){}\n            function replaceSelection(){}\n            \n            plugin.on(\"documentLoad\", function(e){\n                var doc = e.doc;\n                var session = doc.getSession();\n                \n                doc.on(\"setValue\", function get(e) {\n                    renderOnCanvas(e.value, session.canvas);\n                }, session);\n                \n                doc.on(\"getValue\", function get(e) {\n                    return doc.changed\n                        ? serializeValue(session)\n                        : e.value;\n                }, session);\n                \n                doc.on(\"progress\", function(e){\n                    if (e.complete)\n                        hideProgress();\n                    else\n                        showProgress(e.loaded, e.total);\n                }, session);\n                \n                doc.tab.on(\"setPath\", setTitle, session);\n                \n                function setTitle(e) {\n                    var path = doc.tab.path;\n                    \n                    // Caption is the filename\n                    doc.title = basename(path);\n                    \n                    // Tooltip is the full path\n                    doc.tooltip = path;\n                }\n                setTitle();\n                \n                function setTheme(e) {\n                    var tab = doc.tab;\n                    var isDark = e.theme == \"dark\";\n                    \n                    tab.backgroundColor = BGCOLOR[e.theme];\n                    \n                    if (isDark) tab.classList.add(\"dark\");\n                    else tab.classList.remove(\"dark\");\n                }\n                \n                layout.on(\"themeChange\", setTheme, session);\n                setTheme({ theme: settings.get(\"user/general/@skin\") });\n                \n                // Define a way to update the state for the session\n                session.update = function(){\n                    contents.scrollTop = session.scrollTop;\n                    contents.scrollLeft = session.scrollLeft;\n                    \n                    // Set the canvas element to be displayed\n                    contents.appendChild(currentSession.canvas);\n                };\n            \n                session.canvas = document.createElement(\"canvas\");    \n            });\n            plugin.on(\"documentActivate\", function(e){\n                currentDocument = e.doc;\n                currentSession = e.doc.getSession();\n            \n                // Remove a previous canvas element, if any\n                if (contents.firstChild)\n                    contents.removeChild(contents.firstChild); \n            \n                // Call the update function when this session becomes active\n                currentSession.update();\n            });\n            plugin.on(\"documentUnload\", function(e){\n                var doc = e.doc;\n                var session = doc.getSession();\n                \n                if (session.canvas.parentNode)\n                    session.canvas.parentNode.removeChild(session.canvas);\n                delete session.canvas;\n            });\n            plugin.on(\"cut\", function(e) {\n                var data = getSerializedSelection();\n                removeSelection();\n                e.clipboardData.setData(\"text/plain\", data);\n            });\n            plugin.on(\"paste\", function(e) {\n                var data = e.clipboardData.getData(\"text/plain\");\n                if (data !== false)\n                    replaceSelection(data);\n            });\n            plugin.on(\"getState\", function(e) {\n                var session = e.doc.getSession();\n                \n                e.state.scrollTop = session.scrollTop;\n                e.state.scrollLeft = session.scrollLeft;\n            });\n            plugin.on(\"setState\", function(e) {\n                var session = e.doc.getSession();\n                \n                session.scrollTop = e.state.scrollTop;\n                session.scrollLeft = e.state.scrollLeft;\n                \n                if (session == currentSession)\n                    session.update();\n            });\n            plugin.on(\"focus\", function(){\n                contents.className = \"contents focus\";\n            });\n            plugin.on(\"blur\", function(){\n                contents.className = \"contents\";\n            });\n            plugin.on(\"resize\", function(){\n                contents.style.width = (contents.parentNode.offsetWidth - 2) + \"px\";\n            });\n\n            plugin.freezePublicAPI({\n                \n            });\n            \n            plugin.load(null, \"my-editor\");\n            \n            return plugin;\n        }\n        \n        register(null, {\n            \"my-editor\": handle\n        });\n    }\n});\n```","excerpt":"","slug":"creating-an-editor-plugin","type":"basic","title":"Editor Plugin"}
Some editors display the contents of documents and allow the user to manipulate that content with a set of tools. Other editors, such as the terminal, don't interact with contents but allow the user to interact with other resources. This article is geared towards creating a custom editor. [block:callout] { "type": "warning", "title": "Creating Editors", "body": "In most cases editors are created automatically for the panes they belong too. However it is possible to create editors and use them outside of panes. For instance an experimental and yet to be released plugin that creates a dashboard uses editors displayed in dashboard widgets instead of tabs." } [/block] # Object structure Besides tabs and panes there are several other objects involved in displaying content and making it editable. The following hierarchy describes the relationships between those objects. - [Pane](https://docs.c9.io/api/#!/api/Pane) - Represent a single pane, housing multiple tabs - [Editor](https://docs.c9.io/api/#!/api/Editor) - The editor responsible for displaying the document in the tab - [Tab](https://docs.c9.io/api/#!/api/Tab) - A single tab (button) in a pane - [Document](https://docs.c9.io/api/#!/api/Document) - The representation of a file in the tab - [Session](https://docs.c9.io/api/#!/api/Session) - The session information of the editor - [UndoManager](https://docs.c9.io/api/#!/api/UndoManager) - The object that manages the undo stack for this document It is important to note that the `Editor` plugin(s) are related to the pane, not the tab. There's only one instance of a certain editor for each pane. The editor is responsible for displaying the content of a document based on the tab that is active. There can be a maximum of one tab active per pane. # Creating a Basic Editor ## Register your Editor Start by consuming the `editors` plugin and `Editor` [base class](doc:base-classes) which helps creating and registering your editor. ``` var Editor = imports.Editor; var editors = imports.editors; ``` Then define the extensions that your editor can open. If there are none, leave the array empty. ``` var extensions = ["foo", "bar"]; ``` Register your editor by giving it an id, name, factory reference and list of extensions. The `handle` which is returned should be passed to the register function. ``` var handle = editors.register("foo", "Foo", FooEditor, extensions); register(null, { "my-editor": handle }); ``` ## Create a Factory for your Editor The factory function mentioned above creates a new `Editor` plugin and this is where the meat of your implementation will be. The following code should all be [straightforward](doc:plugin). ``` function FooEditor(){ var plugin = new Editor("Your Name", main.consumes, extensions); // Register the API plugin.freezePublicAPI({}); // Initialize the Editor plugin.load(null, "my-plugin"); return plugin; } ``` The next sections will tell you how to extend this basic editor and make it function. # Draw the UI Before anything else your plugin *must* draw it's user interface. The `draw` event is the designated place and time for this. This event is called the first time a tab for this editor becomes active in a pane. Usually this is immediately after the editor is created. In this example the UI for the editor is loaded from an html file. ``` var container, contents; plugin.on("draw", function(e) { container = e.htmlNode; // Insert the UI ui.insertHTML(container, require("text!./editor.html"), plugin); contents = container.querySelector(".contents"); }); ``` [block:callout] { "type": "warning", "title": "Don't load CSS in the draw event", "body": "Since the draw event is called for each instance of the editor it is a *bad* idea to load css here. Instead load css in function that can only be called once and is defined outside the factory function of the editor." } [/block] # Documents Dealing with documents is the main responsibility of the editor. Documents are loaded, activated and unloaded as tabs in a pane are created, become active and are closed. Your editor is responsible for: - Rendering the contents of the document - Serializing the state of the document when requested - Setting the title of the document - Rendering the progress events of the document - Cleaning up after a document is unloaded *See also the [guide on document sessions](http://cloud9-sdk.readme.io/v0.1/docs/documents#sessions) for more information on how sessions work* ## Loading When a user opens a tab, for instance by double clicking on the file tree, a document is created and loaded into the appropriate editor. The `documentLoad` event is then called. Use this event to prepare the document's session to be displayed in the editor. It is only when the tab becomes active that the document will actually be displayed. The following examples use a canvas element to render the contents of a document. Lets start by creating a canvas element in the `documentLoad` handler. ```javascript plugin.on("documentLoad", function(e){ var doc = e.doc; var session = doc.getSession(); session.canvas = document.createElement("canvas"); }); ``` Sessions are loaded and unloaded when the document moves between editors. In some cases you want to apply only some of the code in the `documentLoad` event handler to the session. See the [session guide](http://cloud9-sdk.readme.io/v0.1/docs/documents#sessions) for more information. [block:callout] { "type": "success", "title": "Add the next examples to the documentLoad handler", "body": "The following examples are to be added to the `documentLoad` event handler" } [/block] ### Document Value A custom plugin *should* process the contents of a document via the `setValue` event. Use this event to render the contents of the file (a string) in which ever way you want. In the example below we pass it to a function that will render the contents to the canvas element. ```javascript doc.on("setValue", function get(e) { renderOnCanvas(e.value, canvas); }, session); ``` When the user uses your editor, it will most likely change the contents of the document. At some point the contents of the document will need to be serialized back into a string. For instance the save plugin will request state serialization when it wants to store the contents on disk. A custom plugin *should* implement a `getValue` handler to serialize the document's contents. ```javascript doc.on("getValue", function get(e) { return doc.changed ? serializeValue(session) : e.value; }, session); ``` As an optimization, in the example above, the state is only serialized if the document actually changed. Otherwise the cached value is used. ### Loading Progress Your editor plugin *could* implement a progress indicator based on the `progress` event of the document. For more information see [this article](http://cloud9-sdk.readme.io/v0.1/docs/documents#progress). ```javascript doc.on("progress", function(e){ if (e.complete) hideProgress(); else showProgress(e.loaded, e.total); }, session); ``` ### Tab Title Managing the title of the document is task your editor plugin *must* do. For instance the terminal plugin sets the title to the title from the tty. The ace plugin sets it to the filename of the file that's opened. This code example shows you how to do that. ```javascript doc.tab.on("setPath", setTitle, session); function setTitle(e) { var path = doc.tab.path; // Caption is the filename doc.title = basename(path); // Tooltip is the full path doc.tooltip = path; } setTitle(); ``` Note that the `basename()` function comes from `require("path").basename`. ### Cloud9 Theme Setting the background color of a tab is the last thing an editor plugin *must* do. Often editor plugins want to change the color based on the theme. The following example shows how to accomplish this. ```javascript function setTheme(e) { var tab = doc.tab; var isDark = e.theme == "dark"; tab.backgroundColor = BGCOLOR[e.theme]; if (isDark) tab.classList.add("dark"); else tab.classList.remove("dark"); } layout.on("themeChange", setTheme, session); setTheme({ theme: settings.get("user/general/@skin") }); ``` In this example `BGCOLOR` is a hash of the default Cloud9 themes as keys and colors as values. *We're working on a way to move styling for this to the themes. Stay tuned.* ### Document Unload For some editors, such as the terminal, the unload of the session is best to occur in the `unload` event of the document. (For more information on unloading see [this article](http://cloud9-sdk.readme.io/v0.1/docs/documents#unloading)). The following code example come straight from the terminal source code. ```javascript doc.on("unload", function(){ // Stop the shell process at the remote machine session.kill(); // Destroy the terminal if (session.terminal) session.terminal.destroy(); }); ``` ## Activating Whenever a tab becomes active, the editor for that document becomes visible and the `documentActivate` event is fired on the editor. An editor plugin *must* implement this event to hide any other document that is displayed and show the contents of the document that is now active. Most plugins keep a reference to the active document in a local variable such as `currentDocument` or to the session as `currentSession`. This makes it easy to work with the currently active document. This example swaps out the canvas element and displays the one of the active document. ```javascript plugin.on("documentActivate", function(e){ currentDocument = e.doc; currentSession = e.doc.getSession(); // Remove a previous canvas element, if any if (contents.firstChild) contents.removeChild(contents.firstChild); // Set the canvas element to be displayed contents.appendChild(currentSession.canvas); }); ``` The active document of an editor can be retrieved like this. ```javascript var doc = editor.activeDocument; ``` ## Unloading The unloading topic is extensively covered in the [document guide](http://cloud9-sdk.readme.io/v0.1/docs/documents#unloading). We refer to that section to learn about using the `documentUnload` event. # Clipboard Your editor plugin can interact with clipboard actions using three events for `copy`, `cut` and `paste`. ## Copy The copy event is triggered when a user hits `Cmd-C` or `Ctrl-C`, uses the `Edit/Copy` menu item or the `copy` command is triggered programmatically. The `clipboardData` object provides an API to set and retrieve data from the clipboard. ```javascript plugin.on("copy", function(e) { var data = getSerializedSelection(); e.clipboardData.setData("text/plain", data); }); ``` ## Cut The cut event is triggered when a user hits `Cmd-X` or `Ctrl-X`, uses the `Edit/Cut` menu item or the `cut` command is triggered programmatically. ```javascript plugin.on("cut", function(e) { var data = getSerializedSelection(); removeSelection(); e.clipboardData.setData("text/plain", data); }); ``` ## Paste The paste event is triggered when a user hits `Cmd-V` or `Ctrl-V`, uses the `Edit/Paste` menu item or the `paste` command is triggered programmatically. ```javascript plugin.on("paste", function(e) { var data = e.clipboardData.getData("text/plain"); if (data !== false) replaceSelection(data); }); ``` # Serializing and Deserializing When the [document is serialized and deserialized](http://cloud9-sdk.readme.io/v0.1/docs/documents#serializing-and-deserializing-state) the `getState` and `setState` events are called. This serialization step preserves the state of the editor between browser reloads. In the following example, we'll store the scroll position of our editor in the state. ```javascript plugin.on("getState", function(e) { var session = e.doc.getSession(); e.state.scrollTop = session.scrollTop; e.state.scrollLeft = session.scrollLeft; }); ``` When the state gets restored, we can restore the scroll position. ```javascript plugin.on("setState", function(e) { var session = e.doc.getSession(); session.scrollTop = e.state.scrollTop; session.scrollLeft = e.state.scrollLeft; if (session == currentSession) session.update(); }); ``` Note that we are reading and setting the state on the session object. Check out the following code that serves as an example on how you could manage the state of the session. ```javascript plugin.on("draw", function(e){ // Set the scroll state on the session when it changes. contents.addEventListener("scroll", function(){ currentSession.scrollLeft = this.scrollLeft; currentSession.scrollTop = this.scrollTop; }); }); plugin.on("documentLoad", function(e){ var session = e.doc.getSession(); // Define a way to update the scroll state for that session session.update = function(){ contents.scrollTop = session.scrollTop; contents.scrollLeft = session.scrollLeft; } }); plugin.on("documentActivate", function(e){ currentSession = e.doc.getSession(); // Call the update function when this session becomes active session.update(); }); ``` Note that the update function could manage all the state changes, including appending the canvas to the `contents` DIV. # Focus & Blur A plugin editor *should* indicate to the user when it is focussed and when it is blurred. The `focus` and `blur` events will be called at the appropriate time. ```javascript plugin.on("focus", function(){ contents.className = "contents focus"; }); plugin.on("blur", function(){ contents.className = "contents"; }); ``` # Resize When the container that is assigned to the editor *potentially* changes in dimension the resize event is triggered. Hook this event to recalculate any positioning that is not done through css. ```javascript plugin.on("resize", function(){ contents.style.width = (contents.parentNode.offsetWidth - 2) + "px"; }); ``` # Switching Editors When `tab.switchEditor()` is called to switch the editor that has loaded the document (for instance from `ace` to `hex`, or from an `imgeditor` to `hex`) some editors want to validate the contents of the document before switching. For instance if your editor is a GUI drag&drop editor which stores the contents in JSON. When switching from ace back to your editor, you'll want to make sure the JSON is valid before allowing the switch. The `validate` event allows your editor to implement validation logic. ```javascript plugin.on("validate", function(e){ var doc = e.document; var info = e.info; try { var json = JSON.parse(doc.value); } catch(e) { info.title = "Invalid JSON"; info.head = "Could not switch to GUI editor"; info.message = "Invalid JSON was detected: " + e.message; return false; } return true; }); ``` The values set on the info object will be used to alert the user of the issue. # Full Example ```javascript define(function(require, exports, module) { main.consumes = ["Editor", "editors", "ui", "settings", "layout"]; main.provides = ["my-editor"]; return main; function main(options, imports, register) { var Editor = imports.Editor; var editors = imports.editors; var settings = imports.settings; var layout = imports.layout; var ui = imports.ui; var basename = require("path").basename; // Set extensions if any for your editor var extensions = ["foo", "bar"]; // Register the editor var handle = editors.register("foo", "Foo", FooEditor, extensions); var BGCOLOR = { "flat-light": "#F1F1F1", "light": "#D3D3D3", "light-gray": "#D3D3D3", "dark": "#3D3D3D", "dark-gray": "#3D3D3D" }; function FooEditor(){ var plugin = new Editor("Your Name", main.consumes, extensions); var container, contents; var currentSession, currentDocument; plugin.on("draw", function(e) { container = e.htmlNode; // Insert the UI ui.insertHTML(container, require("text!./editor.html"), plugin); contents = container.querySelector(".contents"); // Set the scroll state on the session when it changes. contents.addEventListener("scroll", function(){ currentSession.scrollLeft = this.scrollLeft; currentSession.scrollTop = this.scrollTop; }); }); function renderOnCanvas(){} function hideProgress(){} function showProgress(){} function serializeValue(){} function getSerializedSelection(){} function removeSelection(){} function replaceSelection(){} plugin.on("documentLoad", function(e){ var doc = e.doc; var session = doc.getSession(); doc.on("setValue", function get(e) { renderOnCanvas(e.value, session.canvas); }, session); doc.on("getValue", function get(e) { return doc.changed ? serializeValue(session) : e.value; }, session); doc.on("progress", function(e){ if (e.complete) hideProgress(); else showProgress(e.loaded, e.total); }, session); doc.tab.on("setPath", setTitle, session); function setTitle(e) { var path = doc.tab.path; // Caption is the filename doc.title = basename(path); // Tooltip is the full path doc.tooltip = path; } setTitle(); function setTheme(e) { var tab = doc.tab; var isDark = e.theme == "dark"; tab.backgroundColor = BGCOLOR[e.theme]; if (isDark) tab.classList.add("dark"); else tab.classList.remove("dark"); } layout.on("themeChange", setTheme, session); setTheme({ theme: settings.get("user/general/@skin") }); // Define a way to update the state for the session session.update = function(){ contents.scrollTop = session.scrollTop; contents.scrollLeft = session.scrollLeft; // Set the canvas element to be displayed contents.appendChild(currentSession.canvas); }; session.canvas = document.createElement("canvas"); }); plugin.on("documentActivate", function(e){ currentDocument = e.doc; currentSession = e.doc.getSession(); // Remove a previous canvas element, if any if (contents.firstChild) contents.removeChild(contents.firstChild); // Call the update function when this session becomes active currentSession.update(); }); plugin.on("documentUnload", function(e){ var doc = e.doc; var session = doc.getSession(); if (session.canvas.parentNode) session.canvas.parentNode.removeChild(session.canvas); delete session.canvas; }); plugin.on("cut", function(e) { var data = getSerializedSelection(); removeSelection(); e.clipboardData.setData("text/plain", data); }); plugin.on("paste", function(e) { var data = e.clipboardData.getData("text/plain"); if (data !== false) replaceSelection(data); }); plugin.on("getState", function(e) { var session = e.doc.getSession(); e.state.scrollTop = session.scrollTop; e.state.scrollLeft = session.scrollLeft; }); plugin.on("setState", function(e) { var session = e.doc.getSession(); session.scrollTop = e.state.scrollTop; session.scrollLeft = e.state.scrollLeft; if (session == currentSession) session.update(); }); plugin.on("focus", function(){ contents.className = "contents focus"; }); plugin.on("blur", function(){ contents.className = "contents"; }); plugin.on("resize", function(){ contents.style.width = (contents.parentNode.offsetWidth - 2) + "px"; }); plugin.freezePublicAPI({ }); plugin.load(null, "my-editor"); return plugin; } register(null, { "my-editor": handle }); } }); ```