c9-core/plugins/c9.ide.editors/document.js

612 wiersze
26 KiB
JavaScript

define(function(require, module, exports) {
main.consumes = ["Plugin", "UndoManager", "util", "error_handler"];
main.provides = ["Document"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var util = imports.util;
var UndoManager = imports.UndoManager;
var reportError = imports.error_handler.reportError;
function Document(options) {
var plugin = new Plugin("Ajax.org", main.consumes);
var undoManager;
var emit = plugin.getEmitter();
if (options && options.undoManager instanceof Plugin) {
undoManager = options.undoManager;
delete options.undoManager;
}
else
undoManager = new UndoManager();
var sessions = {};
var value = options && options.value || "";
var changed = false;
var meta = {};
var hasValue = options && (typeof options.value === "string");
var tab, lastState, title, tooltip, editor, recentValue, ready;
plugin.on("newListener", function(type, listener) {
if (type == "state.set" && lastState) {
listener({
doc: plugin,
state: lastState
});
}
});
// Listen to changes and detect when the value of the editor
// is different from what is on disk
function initUndo() {
undoManager.on("change", function(e) {
var c = !undoManager.isAtBookmark();
if (changed !== c) {
changed = c;
emit("changed", { changed: c });
}
});
}
initUndo();
/***** Methods *****/
function setBookmarkedValue(value, cleansed) {
if (plugin.value == value) {
recentValue = value;
undoManager.bookmark();
return;
}
var state = getState();
// Record value (which should add an undo stack item)
plugin.value = value;
// Bookmark the undo manager
undoManager.bookmark();
// Update state
delete state.changed;
delete state.value;
delete state.meta;
state.undoManager = undoManager.getState();
if (cleansed && editor && state[editor.type])
state[editor.type].cleansed = true;
// Set new state (preserving original state)
if (emit("mergeState") !== false)
setState(state);
}
function getState(filter) {
// Editor is not inited yet, so we keep the state set until
// editor is loaded
var state = !editor && lastState
? state = util.extend({}, lastState)
: {};
state.changed = changed;
state.meta = {};
state.filter = filter || false;
for (var prop in meta) {
if (prop.charAt(0) != "$")
state.meta[prop] = meta[prop];
}
if (title)
state.title = title;
if (tooltip)
state.tooltip = tooltip;
if (!filter) {
state.value = plugin.value;
state.undoManager = undoManager.getState();
}
else {
delete state.value;
delete state.undoManager;
}
if (editor)
state[editor.type] = editor.getState(plugin, filter);
var event = { doc: plugin, state: state };
emit("getState", event);
lastState = event.state;
return event.state;
}
function setState(state) {
if (state.meta) {
for (var prop in meta) {
if (prop.charAt(0) == "$")
state.meta[prop] = meta[prop];
}
meta = state.meta;
}
if (state.title) plugin.title = state.title;
if (state.tooltip) plugin.tooltip = state.tooltip;
if (typeof state.value == "string")
plugin.value = state.value;
if (state.undoManager && !(state.undoManager instanceof Plugin))
undoManager.setState(state.undoManager);
if (state.changed && state.changed !== changed) {
changed = state.changed;
emit("changed", { changed: changed });
}
if (editor && state[editor.type])
editor.setState(plugin, state[editor.type]);
var event = { doc: plugin, state: state };
emit("setState", event);
lastState = event.state;
}
function getSession(type) {
var ed = editor || tab && tab.editor;
if (ed && !type) type = ed.type;
if (!type) return;
if (sessions[type])
return sessions[type];
var session = sessions[type] = new Plugin();
session.load(null, "session");
return session;
}
function progress(options) {
emit("progress", options);
}
function clone() {
var state = this.getState();
state.undoManager = undoManager;
var newdoc = new Document(state);
emit("clone", { doc: newdoc });
newdoc.meta.cloned = true;
return newdoc;
}
function canUnload() {
return emit("canUnload");
}
/***** Lifecycle *****/
plugin.on("unload", function() {
changed = false;
// Unload the undo manager
undoManager.unload();
// Unload the sessions
for (var type in sessions)
sessions[type].unload();
});
/**
* Session Class for Cloud9 Editors. Each document that is loaded into an
* editor has one session object per editor. A session object is retrieved by
* calling {@link Document#getSession} and is specifically tied to an editor
* type. The session is used to store state and functions on by the editor.
*
* The session relates to other objects as such:
*
* * {@link Pane} - Represent a single pane, housing multiple tabs
* * {@link Tab} - A single tab (button) in a pane
* * {@link Editor} - The editor responsible for displaying the file in the tab
* * {@link Document} - The representation of a file in the tab
* * **Session - The session information of the editor**
* * {@link UndoManager} - The object that manages the undo stack for this document
*
* Panes can live in certain areas of Cloud9. By default these areas are:
*
* * {@link panes} - The main area where editor panes are displayed
* * {@link console} - The console in the bottom of the screen
*
* Tabs are managed by the {@link tabManager}. The default way to
* open a new file in an editor uses the tabManager:
*
* tabManager.openFile("/file.js", true, function(err, tab) {
* var session = tab.document.getSession();
* var aceSession = session.session;
* });
*
* Generally the state and functions stored on the session object are private
* to the editor and do not concern other plugins. However one can imagine that
* plugins that work on editors make use of these properties. For instance the
* {@link ace.status.Statusbar} plugin uses the information on the state object from the
* ace editor to display state information like the line number and language
* mode.
*
* When a tab is moved from one pane to another, the document is unloaded from
* one editor and then loaded into another one. It is up to the editor to
* decide how to handle the stored state on the session object. Often the
* editor will remove only some of that state and reuse other state in the
* new editor. For instance, the {@link ace.Ace Ace} editor stores the ace session
* object on the Cloud9 session object.
* It will only do this once and will use the same ace session object in
* different ace editors, throughout the lifetime of the session object.
*
* This example shows how an editor could deal with a session. For more
* information see the source code of the {@link texteditor.TextEditor TextEditor} plugin.
*
* plugin.on("documentLoad", function(e) {
* var doc = e.doc;
* var session = doc.getSession();
*
* if (!session.id) {
* session.id = Math.random();
* session.iframe = document.createElement("iframe");
* }
*
* // Container would be a div created in the {@link Editor#draw} event.
* container.appendChild(session.iframe);
* });
*
* @class Session
* @extends Plugin
*/
/**
* Document Class for Cloud9 Editors. Each file that is opened
* in an editor has a document object that contains it's value and
* state.
*
* The document relates to other objects as such:
*
* * {@link Pane} - Represent a single pane, housing multiple tabs
* * {@link Tab} - A single tab (button) in a pane
* * {@link Editor} - The editor responsible for displaying the file in the tab
* * **Document - The representation of a file in the tab**
* * {@link Session} - The session information of the editor
* * {@link UndoManager} - The object that manages the undo stack for this document
*
* Panes can live in certain areas of Cloud9. By default these areas are:
*
* * {@link panes} - The main area where editor panes are displayed
* * {@link console} - The console in the bottom of the screen
*
* Tabs are managed by the {@link tabManager}. The default way to
* open a new file in an editor uses the tabManager:
*
* tabManager.openFile("/file.js", true, function(err, tab) {
* var doc = tab.document;
* console.log("The value is: ", doc.value);
* });
*
* For editors that do not allow a user to edit files
* (e.g. terminal), a document object will still be created.
*/
plugin.freezePublicAPI({
/**
* the tab this document is attached to
* @property {Tab} tab
*/
get tab() { return tab; },
set tab(v) { tab = v; },
/**
* the meta information for this document. Use this object to
* store any additional information you'd like to store. If you
* are storing state information that should not be kept between
* reloads of the IDE, prefix the items with a "$" character.
*
* Example:
*
* // This document has a black bird
* var doc = tabs.focussedTab.document;
* doc.meta.blackBird = true;
*
* Example (state is not preserved):
*
* // This document is being processed
* var doc = tabs.focussedTab.document;
* doc.meta.$processing = true;
*
* @property meta
*/
get meta() { return meta; },
/**
* The last state that was set into the document.
* @property lastState
* @private
*/
get lastState() { return lastState; },
/**
* the editor this document is attached to
* @property {Editor} editor
*/
get editor() { return editor; },
set editor(v) {
editor = v;
emit("setEditor", { editor: v });
},
/**
* Whether the document is fully loaded
* @property {Boolean} ready
*/
get ready() { return ready; },
set ready(v) {
// try to find out why is this called twice
var e = new Error("Setting ready on ready document");
if (ready)
reportError(e, { ready: ready });
ready = e.stack || true;
emit.sticky("ready", { doc: plugin });
},
/**
* The tooltip displayed when hovering over the tab button
* @property {String} tooltip
*/
get tooltip() { return tooltip; },
set tooltip(v) {
tooltip = v;
emit("setTooltip", { tooltip: v });
},
/**
* The title of the document (displayed as caption of the tab button)
* @property {String} title
*/
get title() { return title; },
set title(v) {
title = v;
emit("setTitle", { title: v });
},
/**
* Sets or retrieves the serialized value of this document.
* Setting this property will not change the undo stack. Set
* this property only to initialize the document or to reset
* the value of this document. Requesting the value of this
* document will cause it to serialize it's full state.
* @property {String} value
*/
get value() {
var calculated = emit("getValue", { value: recentValue || value });
if (typeof calculated != "string")
calculated = value;
if (undoManager.isAtBookmark())
recentValue = calculated;
return calculated;
},
set value(v) {
value = recentValue = v;
emit("setValue", { value: v });
hasValue = true;
},
/**
* The last serialized value that was either set or retrieved.
* The `recentValue` is only updated during a get when the
* document is saved (undoManager.isAtBookmark() == true).
* @property {String} recentValue
* @readonly
*/
get recentValue() { return recentValue; },
/**
* Specifies whether the document on this tab is changed
* @property {Boolean} changed
*/
get changed() { return changed; },
/**
* The object managing all the changes to the document
* @property {UndoManager} undoManager
*/
get undoManager() { return undoManager; },
set undoManager(newUndo) {
undoManager.unload();
undoManager = newUndo;
initUndo();
},
_events: [
/**
* Fires when the state is retrieved
* @event getState
* @param {Object} e
* @param {Document} e.doc the document for which the state is retrieved
* @param {Object} e.state the state object to extend with information
*/
"getState",
/**
* Fires when the state is set
* @event setState
* @param {Object} e
* @param {Document} e.doc the document for which the state is set
* @param {Object} e.state the state object
*/
"setState",
/**
* Fires when the value of this document is retrieved.
* When implementing an editor, use this event to return
* the current value of this document in the event handler.
*
* Example:
*
* doc.on("getValue", function(e) {
* return "The current value of this editor"
* }, doc.getSession());
*
* @event getValue
* @param {Object} e
* @param {String} e.value the last cached value
*/
"getValue",
/**
* Fires when the value of this document is set
* When implementing an editor, use this event to set
* the new value of this document in the event handler.
*
* Example:
*
* doc.on("setValue", function(e) {
* myDiv.textContent = e.value
* }, doc.getSession());
*
* @event setValue
* @param {Object} e
* @param {String} e.value the value that is being set
*/
"setValue",
/**
* @ignore
*/
"mergeState",
/**
* Fires when the title of this document is set
* @event setTitle
* @param {Object} e
* @param {String} e.value the title that is being set
*/
"setTitle",
/**
* Fires when the tooltip of this document is set
* @event setTooltip
* @param {Object} e
* @param {String} e.value the tooltip that is being set
*/
"setTooltip",
/**
* Fires when the editor of this document is set
* @event setEditor
* @param {Object} e
* @param {String} e.value the editor that is being set
*/
"setEditor",
/**
* Fires when the change state of the document has changed
* @event changed
* @param {Object} e
* @param {String} e.changed whether the value is changed
*/
"changed",
/**
* Fires when there is progress information related to this
* file. This can be either downloading or uploading.
* @event progress
* @param {Object} e
* @param {Number} e.loaded the number of bytes that have been downloaded/uploaded.
* @param {Number} e.total the total number of bytes for this file.
* @param {Boolean} e.complete whether the download has completed.
* @param {Boolean} e.upload whether this is an upload (instead of a download).
**/
"progress",
/**
* Fires when this document is cloned
* @event cone
* @param {Object} e
* @param {Document} e.doc the cloned document
**/
"clone",
/**
* @event canUnload
*/
"canUnload"
],
/**
* Retrieves the session object where editors can store state
*/
getSession: getSession,
/**
* Sets the value of the document and bookmarks it in the
* undoManager. This method also preserves the complete state
* of the editor while transitioning to the new value.
* @param {String} value The new value of the document
*/
setBookmarkedValue: setBookmarkedValue,
/**
* Retrieves the state of the document. The state object will
* have a section for each editor that it is loaded in. These
* sections have the name of the editor. For instance if the
* editor is ace, the returned object will look something like
* this:
*
* {
* ace : {
* scrollTop : 100,
* ...
* }
* }
*
* @return {Object}
* @return {Boolean} return.changed Specifying whether the document state has been saved.
* @return {Object} return.meta Any metadata set on this document (except items starting with a "$").
* @return {String} return.title The title displayed in the tab, displaying this document.
* @return {String} return.tooltip The tooltip displayed in the tab, displaying this document
* @return {String} return.value The value of the document.
* @return {Object} return.undoManager The result of {@link UndoManager#getState}.
*/
getState: getState,
/**
* Sets the state of the document
* @param {Object} state the state of the document. This can also include sections for editors. See {@link Document#method-getState}.
* @param {Boolean} state.changed Specifying whether the document state has been saved.
* @param {Object} state.meta Any metadata set on this document (except items starting with a "$").
* @param {String} state.title The title displayed in the tab, displaying this document.
* @param {String} state.tooltip The tooltip displayed in the tab, displaying this document
* @param {String} state.value The value of the document.
* @param {Object} state.undoManager The result of {@link UndoManager#getState}.
*/
setState: setState,
/**
* Set the progress indicator for this document
* @param {Object} options
* @param {Number} options.loaded the number of bytes that have been downloaded/uploaded.
* @param {Number} options.total the total number of bytes for this file.
* @param {Boolean} options.complete whether the download has completed.
* @param {Boolean} options.upload whether this is an upload (instead of a download).
*/
progress: progress,
/**
* Clones this document
*/
clone: clone,
/**
* Whether this document is ready to be unloaded
*/
canUnload: canUnload,
/**
* Returns whether the document has it's value set.
* @return {Boolean}
*/
hasValue: function() { return !!hasValue; },
});
if (options)
setState(options);
plugin.load(null, "document");
return plugin;
}
/***** Register and define API *****/
register(null, {
Document: Document
});
}
});