Documents

A document object represents the contents of a file. It can hold any type of data (so not just strings). The document object is automatically created when opening a tab and is from then on paired with that tab. Most of the API available on the document is used by editor implementations. This article is geared primarily towards working with documents in custom editors.

🚧

Creating documents

We recommend using the Document class to create documents when you need an abstract representation of file contents. In most cases you won't need to create documents as this is done automatically when a file is opened via the tabManager.open() method. However there is no reason you couldn't create documents and use them for other purposes.

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 - Represent a single pane, housing multiple tabs
    • Editor - The editor responsible for displaying the document in the tab
    • Tab - A single tab (button) in a pane
      • Document - The representation of a file in the tab
        • Session - The session information of the editor
        • 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.

Opening Files

For more information on this topic see the Tabs guide

The easiest way to get a references to a document is by opening a file.

tabManager.openFile("/file.js", function(err, tab){
    if (err) return console.error(err);
    
    var doc = tab.document;
});

File Contents

Retrieving the Contents

The callback in the example above is called when the contents of the file is loaded. You can then easily fetch the contents of the file like this.

var value = doc.value;

It is important to be aware that requesting the value from the document will trigger the current editor of the document to serialize it's contents. This is often an expensive operation and doesn't scale very well. To prevent this from happening you can access the recentValue property which is set when the document is loaded and on each save. A drawback is that it will not have the state in between saves.

In the following example we fetch a reference to the document prior to the contents being loaded. We then hook the ready event to wait for the contents to become available.

var tab = tabManager.openFile("file.js");
var doc = tab.document;

// The ready property will be false at this point
console.log(doc.ready);

doc.on("ready", function(){
    console.log("The loaded value is", doc.recentValue);
});

Setting the Contents

The most straight forward way to set the contents of a document is to set the value property of a document.

doc.value = "example";

This will notify the editor that the value has been set to "example". It is important to note that the undo manager will not be affected by this. It is therefore recommended to only set this property at the very start of the document. After that use the setBookmarkedValue() method to give the document a new value. This will add a new state to the undo manager and will bookmark it which will set the changed property on the document to false.

doc.setBookmarkedValue("example");

Setting the value this way has the benefit of preserving the state of your document. For instance for the ace plugin this means that folds, selection and the scroll state are kept when updating the value.

Listening to Events

When you are implementing a custom editor plugin you get a document passed in the documentLoad event. To process changes to that document listen to the setValue and getValue events.

The editor is responsible for calculating the current value of the document. Whenever the value property is accessed, calculate the serialized state and return it.

var session = doc.getSession();
doc.on("getValue", function(e) {
    return serializeValue(session);
}, session);

The editor is also responsible for displaying the current contents of the document. The setValue event is called every time the value of a document is set. (So also when setBookmarkedValue() is called).

var session = doc.getSession();
doc.on("setValue", function(e) {
    loadValueIntoEditor(e.value);
}, session);

🚧

Using the session object

Pass the session object when setting the event handlers to make sure these event handlers are removed when the session terminates. For more information on sessions see below.

Changed

To determine whether a document has unsaved changes check the changed property. It is set to true when the document has unsaved changed and to false otherwise. There are several ways to influence this property:

  • The undo manager arrived at a bookmark via doc.undoManager.undo() or redo()
  • Set the current state of the undo manager as bookmarked doc.undoManager.bookmark()
  • Set the value of the doc via doc.setBookmarkedValue()
  • Change the contents of the document via doc.undoManager.add(undoItem)

Listen to the changed event to get notified when this state changes. Note that unlike the change event of the undo manager which is called for each change, this changed event is only called when the state changes from true to false or vice versa.

doc.on("changed", function(e){
    doc.tab.title = doc.tab.path + (e.changed ? "*" : "");
});

Progress

Loading and saving content of a document can take time. To keep track of the progress the document emits a progress event that allows an editor or other plugin to display a UI.

doc.on("progress", function(e){
    if (e.complete)
        console.log("Downloaded ", e.total, "bytes");
    else
        console.log(e.loaded, "bytes downloaded of", e.total);
});

In some cases your plugin might be in control of loading or storing the content of a document. For those cases you want to set the progress on the document in your plugin.

doc.progress({
    loaded: loadedBytes,
    total: totalBytes,
    complete: loadedBytes == totalBytes,
    upload: false // Set this to true when storing the contents of the document
});

Serializing and Deserializing State

All objects of the object structure described in the top of this article have the ability to serialize and deserialize their state into a JSON serializable state object. By default the state of a document includes the state of it's properties like value, changed, title and caption as well as some of the objects like meta and undoManager in serializable form. Each editor stores their state on a property that has the same name as the editor's type property.

// Retrieve the state
var state = doc.getState();

// Set the state
doc.setState(state);

// Editor state, for example when this doc is loaded in the ace editor
var scrollTop = state.ace.scrolltop;

State retrieving and setting is done automatically for documents that are connected to a tab. This state is stored in the state settings and automatically retrieved when the IDE loads.

If you want to listen in or add to the state of a document you can set these events.

doc.on("getState", function(e){
    e.state.myExtraProperty = 100;
});

doc.on("setState", function(e){
    var prop = e.state.myExtraProperty;
});

When you get a reference to the document after it's created, for instance via the open event of the tabManager the state is already set. Read from the lastState property to get the last state set.

var state = doc.lastState;

Editor

Fetch a reference to the editor that currently renders the contents of the document like this.

var currentEditor = doc.editor;

Note that this value will be null if the editor has not yet loaded this document. To fetch the type of the editor that will be created when the tab that this document belongs to is activated, retrieve doc.tab.editorType.

Title and Tooltip

Documents can have a title and a tooltip text that is displayed as the title and tooltip of the tab they are loaded in. For documents that load data from files the title is set to the filename and the tooltip is set to the full path. It is the editor plugin that decides what the values of these properties are.

Sessions

A session object allows editors to store information specific for that editor-document relationship. Because a plugin API is immutable it is not possible to store this information on the document itself. In addition, the session object facilitates easy clean up of any events and other state recorded (more in this below).

Session objects are created for each editor type that a document is loaded on. A document can be loaded in different editors through it's life time by one of these (user) actions:

  • A user moves a tab from one pane to another (each pane has max one instance of a certain editor type)
  • A user switches the editor (type) via View/Editors menu
  • Some plugin switches the editor via the switchEditor() method on a tab.

Whenever a document switches editor, the session object is unloaded in order to allow the session to be loaded into the new editor. This means it will clean up any event that was set in the context of the session and it will call the unload event.

For instance, the terminal editor plugin stores the connection id on the session object. It also sets numerous events that are unset when a session is unloaded.

The getSession() Method

The doc.getSession() method returns a session object that is unique for the editor type that has the document currently loaded. If the session object doesn't exist yet it will create it. A consequence of this is that instances of the same editor have access to the same session object and can use that to transfer information between them.

Loading a Document in an Editor

❗️

Editor Plugin

In the following examples, the editor variable is an implementation of a custom editor.

The following example uses a session object in a custom editor plugin that uses an iframe to render the content of a document.

editor.on("documentLoad", function(e){
    var doc = e.doc;
    var session = doc.getSession();
    
    session.iframe = document.createElement("iframe");

    doc.on("setValue", function get(e) {
        var url = e.value;
        session.iframe.src = url;
    }, session);
});

By passing the session object as the 3rd argument of the on() method it will be cleaned up when the session unloads.

Unloading a Document from an Editor

You have to be aware of the unload flows of both the document and the session to make a decision on where to clean up references.

A document is unloaded when a user closes a tab. Period. Use the unload flow of a document to clean up anything that is specific to the document. In most cases you won't need to hook the unload of the document.

The session is unloaded when a document is unloaded from an editor (Remember that the session represents the relationship between the document and the editor). This happens when a user closes a tab. This also happens when a user moves a tab between panes.

In your custom editor plugin you want to make sure that not everything is unloaded when the user moves a document between panes. In many cases only part of the init flow of a session needs to be repeated to set the right references. Here's an example:

var id = 100; // Specific to this editor

editor.on("documentLoad", function(e){
    var doc = e.doc;
    var session = doc.getSession();
    
    // This event handler is specific to the editor instance
    doc.on("setValue", function get(e) {
        session.iframe.src = e.value + "?id=" + id;
    }, session);
    
    // Any code after this is only executed the first time or 
    // when the session comes from a different editor.
    if (session.inited) return;
    session.inited = true;

    // The iframe can be carried over to other editors of the same type
    session.iframe = document.createElement("iframe");
});

Now when the document moves between panes the event handler is cleared and set again, while the iframe remains. You can append the iframe to the container of your editor in the documentActivate event which is called when the tab becomes active.

editor.on("documentActivate", function(e){
    var doc = e.doc;
    var session = doc.getSession();
    
    // Remove a previous iframe, if any
    if (container.firstChild)
        container.removeChild(container.firstChild); 
    
    // Set the iframe to be displayed
    container.appendChild(session.iframe);
});

A consequence of this is that we need to clear the state for when the document is loaded in a different editor or when the tab is closed. This is where the documentUnload event comes in.

editor.on("documentUnload", function(e){
    var doc = e.doc;
    var session = doc.getSession();
    
    // Do nothing when switching to an editor of the same type
    if (e.toEditor.type == e.fromEditor.type)
        return; 
    
    session.iframe.parentNode.removeChild(session.iframe);
    delete session.iframe;
    delete session.inited;
});

Note that if you do want to keep the iframe around, in case the user switches back to your editor, then clean those items up in the doc.on("unload" handler.

The meta object

The document plugin offers a meta object that is used by plugins to set extra state on the document.

🚧

The meta object

The meta object, available on most plugins (e.g. document, tab, menu) allows you to set properties that are not directly part of the API for that plugin, but which properties can be used by other plugins. For instance the save plugin will check for meta.ignoreSave on a document and will not warn when a document with changes is closed when that property is set.

Any plugin can extend the meta object of a plugin with custom properties. Note that properties starting with $ are not saved in the object state, while all other properties are and are thus preserved between reloads of the IDE.

The following table is a subset of the flags that Cloud9 plugins set on the document.meta object.

PropertyPluginDescription
newfile tabManager Indicates that tabManager shouldn't load the contents. Will be removed by the save plugin after the first save.
cloned tabManager Indicates that this tab is a cloned tab (As in the 'Duplicate View' behavior)
timestamp metadata The last datetime the document state was synced with the filesystem
$ignoreSave save The save plugin won't complain when closing a tab with unsaved changes when this is set.
ignoreSave save The save plugin won't complain when closing a tab with unsaved changes when this is set. Preserved across reloads.
$saving save Indicates the document is being saved
$savedValue watcher The last known saved value.
nofs * Indicates this file does not exist on the filesystem. The save plugin won't try to save this document. The tabManager won't try to load it.
error autosave Indicates this document is in some form of error state. Autosave won't try to save this document.
$slowSave autosave Indicates this document took a long time to save. Autosave will save this document less often.