atheos-ide
Version:
Web-based IDE framework
808 lines (701 loc) • 21.7 kB
JavaScript
/*
* Copyright (c) atheos & Kent Safranski (atheos.com), distributed
* as-is and without warranty under the MIT License. See
* [root]/license.txt for more. This information must remain intact.
*/
(function(global) {
var ace = global.ace,
atheos = global.atheos,
storage = atheos.storage;
// Classes from Ace
var VirtualRenderer = ace.require('ace/virtual_renderer').VirtualRenderer;
var Editor = ace.require('ace/editor').Editor;
var EditSession = ace.require('ace/edit_session').EditSession;
var UndoManager = ace.require('ace/undomanager').UndoManager;
var self = null;
carbon.subscribe('system.loadMajor', () => atheos.editor.init());
//////////////////////////////////////////////////////////////////
//
// Editor Component for atheos
// ---------------------------
// Manage the lifecycle of Editor instances
//
//////////////////////////////////////////////////////////////////
atheos.editor = {
/// Editor instances - One instance corresponds to an editor
/// pane in the user interface. Different EditSessions
/// (ace/edit_session)
instances: [],
/// Currently focussed editor
activeInstance: null,
// Settings for Editor instances
settings: {
theme: 'atheos',
fontSize: '13px',
printMargin: false,
printMarginColumn: 80,
highlightLine: true,
indentGuides: true,
wrapMode: false,
softTabs: false,
tabSize: 4
},
rootContainer: null,
fileExtensionTextMode: {},
init: function() {
self = this;
this.cursorTracking();
var editor = oX('#editor-region');
//////////////////////////////////////////////////////////////////80
// h-resize & v-resize events are custom events triggered solely for
// use in allowing you to resize split containers. The -root events
// are triggered throughout Atheos as needed, and cascade down into
// any split containers. - Liam Siira
//////////////////////////////////////////////////////////////////80
editor.on('h-resize-root, v-resize-root', function(e) {
var wrapper = oX('#editor-region .editor-wrapper');
if (wrapper) {
if (e.type === 'h-resize-root') {
wrapper.css('width', editor.width());
wrapper.trigger('h-resize');
} else {
wrapper.css('height', editor.height());
wrapper.trigger('v-resize');
}
}
});
window.addEventListener('resize', function() {
editor.trigger('h-resize-root, v-resize-root');
});
},
//////////////////////////////////////////////////////////////////
//
// Retrieve editor settings from localStorage
//
//////////////////////////////////////////////////////////////////
getSettings: function() {
var settings = ['theme', 'fontSize', 'tabSize', 'printMargin', 'printMarginColumn', 'highlightLine', 'indentGuides', 'wrapMode', 'softTabs'];
settings.forEach(function(key) {
var local = storage('editor.' + key);
if (local !== null) {
self.settings[key] = local;
}
});
},
/////////////////////////////////////////////////////////////////
//
// Apply configuration settings
//
// Parameters:
// i - {Editor}
//
/////////////////////////////////////////////////////////////////
applySettings: function(instance) {
// Check user-specified settings
self.getSettings();
// Apply the current configuration settings:
instance.setTheme('ace/theme/' + self.settings.theme);
instance.setOptions({
// fontFamily: 'VictorMono-Bold',
fontFamily: 'Ubuntu-Fira',
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
});
instance.setAnimatedScroll(true);
instance.setFontSize(self.settings.fontSize);
instance.setPrintMarginColumn(self.settings.printMarginColumn);
instance.setShowPrintMargin(self.settings.printMargin);
instance.setHighlightActiveLine(self.settings.highlightLine);
instance.setDisplayIndentGuides(self.settings.indentGuides);
instance.getSession().setUseWrapMode(self.settings.wrapMode);
self.setTabSize(self.settings.tabSize, instance);
self.setSoftTabs(self.settings.softTabs, instance);
},
beautify: function() {
var beautify = ace.require('ace/ext/beautify');
var editor = self.activeInstance;
beautify.beautify(editor.session);
},
//////////////////////////////////////////////////////////////////
//
// Create a new editor instance attached to given session
//
// Parameters:
// session - {EditSession} Session to be used for new Editor instance
//
//////////////////////////////////////////////////////////////////
addInstance: function(session, where) {
// This can be a little confusing to follow, took me a while. First,
// keep in mind that Ace objects has their own .el property, which
// holds an onyx object, that holds it's own raw element.
// SplitContainer takes raw html elements and has it's own mini-dom
// helper that has zero error checking in order to be as fast as
// possible.
var editor = oX('<div class="editor"></div>');
var childID = null,
splitContainer = null;
if (this.instances.length === 0) {
oX('#root-editor-wrapper').append(editor);
} else {
var firstChild = this.activeInstance.el;
childID = (where === 'top' || where === 'left') ? 0 : 1;
var type = (where === 'top' || where === 'bottom') ? 'vertical' : 'horizontal';
var children = [];
children[childID] = editor.el;
children[1 - childID] = firstChild.el;
var parent = oX('<div class="editor-wrapper">');
parent.css('height', firstChild.height());
parent.css('width', firstChild.width());
parent.addClass('editor-wrapper-' + type);
firstChild.parent().append(parent);
splitContainer = new SplitContainer(parent.el, children, type);
if (this.instances.length > 1) {
var pContainer = this.activeInstance.splitContainer;
var idx = this.activeInstance.splitID;
pContainer.setChild(idx, splitContainer);
}
}
// var i = ace.edit(editor[0]);
var instance = ace.edit(editor.el);
var resizeEditor = function() {
instance.resize();
};
if (splitContainer) {
instance.splitContainer = splitContainer;
instance.splitID = childID;
this.activeInstance.splitContainer = splitContainer;
this.activeInstance.splitID = 1 - childID;
oX(splitContainer.parent).on('h-resize', resizeEditor);
oX(splitContainer.parent).on('v-resize', resizeEditor);
if (this.instances.length === 1) {
var re = function() {
self.instances[0].resize();
};
oX(splitContainer.parent).on('h-resize', re);
oX(splitContainer.parent).on('v-resize', re);
}
}
instance.el = editor;
this.setSession(session, instance);
this.changeListener(instance);
// this.cursorTracking(instance);
this.bindKeys(instance);
this.instances.push(instance);
instance.on('focus', function() {
self.focus(instance);
});
return instance;
},
//////////////////////////////////////////////////////////////////
//
// Remove all Editor instances and clean up the DOM
//
//////////////////////////////////////////////////////////////////
exterminate: function() {
var editors = oX('#editor-region').findAll('.editor, .editor-wrapper');
editors.forEach((editor) => {
editor.remove();
});
oX('#current_file').html('');
oX('#current_mode>span').html('');
self.instances = [];
self.activeInstance = null;
},
//////////////////////////////////////////////////////////////////
//
// Detach EditSession session from all Editor instances replacing
// them with replacementSession
//
//////////////////////////////////////////////////////////////////
removeSession: function(session, replacementSession) {
for (var k = 0; k < self.instances.length; k++) {
if (self.instances[k].getSession().path === session.path) {
self.instances[k].setSession(replacementSession);
}
}
if (oX('#current_file').text() === session.path) {
oX('#current_file').text(replacementSession.path);
}
atheos.textmode.setModeDisplay(replacementSession);
},
isOpen: function(session) {
for (var k = 0; k < self.instances.length; k++) {
if (self.instances[k].getSession().path === session.path) {
return true;
}
}
return false;
},
/////////////////////////////////////////////////////////////////
//
// Convenience function to iterate over Editor instances
//
// Parameters:
// fn - {Function} callback called with each member as an argument
//
/////////////////////////////////////////////////////////////////
forEach: function(fn) {
for (var k = 0; k < self.instances.length; k++) {
fn.call(self, self.instances[k]);
}
},
/////////////////////////////////////////////////////////////////
//
// Get the currently active Editor instance
//
// In a multi-pane setup this would correspond to the
// editor pane user is currently working on.
//
/////////////////////////////////////////////////////////////////
getActive: function() {
return self.activeInstance;
},
/////////////////////////////////////////////////////////////////
//
// Set an editor instance as active
//
// Parameters:
// i - {Editor}
//
/////////////////////////////////////////////////////////////////
setActive: function(instance) {
if (instance) {
self.activeInstance = instance;
var path = instance.getSession().path;
path = (path.length < 30) ? path : '...' + path.substr(path.length - 30);
oX('#current_file').text(path);
atheos.textmode.setModeDisplay(instance.getSession());
}
},
/////////////////////////////////////////////////////////////////
//
// Change the EditSession of Editor instance
//
// Parameters:
// session - {EditSession}
// instance - {Editor}
//
/////////////////////////////////////////////////////////////////
setSession: function(session, instance) {
instance = instance || this.activeInstance;
if (!this.isOpen(session)) {
if (!instance) {
instance = this.addInstance(session);
} else {
instance.setSession(session);
}
} else {
// Proxy session is required because scroll-position and
// cursor position etc. are shared among sessions.
var proxySession = new EditSession(session.getDocument(),
session.getMode());
proxySession.setUndoManager(new UndoManager());
proxySession.path = session.path;
proxySession.listThumb = session.listThumb;
proxySession.tabThumb = session.tabThumb;
if (!instance) {
instance = this.addInstance(proxySession);
} else {
instance.setSession(proxySession);
}
}
this.applySettings(instance);
this.setActive(instance);
},
/////////////////////////////////////////////////////////////////
//
// Set the editor theme
//
// Parameters:
// t - {String} theme eg. twilight, cobalt etc.
// i - {Editor} Editor instance (If omitted, Defaults to all editors)
//
// For a list of themes supported by Ace - refer :
// https://github.com/ajaxorg/ace/tree/master/lib/ace/theme
//
// TODO: Provide support for custom themes
//
/////////////////////////////////////////////////////////////////
setTheme: function(t, i) {
if (i) {
// If a specific instance is specified, change the theme for
// this instance
i.setTheme('ace/theme/' + t);
} else {
// Change the theme for the existing editor instances
// and make it the default for new instances
this.settings.theme = t;
for (var k = 0; k < this.instances.length; k++) {
this.instances[k].setTheme('ace/theme/' + t);
}
}
// LocalStorage
localStorage.setItem('atheos.editor.theme', t);
},
/////////////////////////////////////////////////////////////////
//
// Set contents of the editor
//
// Parameters:
// c - {String} content
// i - {Editor} (Defaults to active editor)
//
/////////////////////////////////////////////////////////////////
setContent: function(c, i) {
i = i || this.getActive();
i.getSession().setValue(c);
},
/////////////////////////////////////////////////////////////////
//
// Set Font Size
//
// Set the font for all Editor instances and remember
// the value for Editor instances to be created in
// future
//
// Parameters:
// s - {Number} font size
// i - {Editor} Editor instance (If omitted, Defaults to all editors)
//
/////////////////////////////////////////////////////////////////
setFontSize: function(s, i) {
if (i) {
i.setFontSize(s);
} else {
this.settings.fontSize = s;
this.forEach(function(i) {
i.setFontSize(s);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.fontSize', s);
},
/////////////////////////////////////////////////////////////////
//
// Enable/disable Highlighting of active line
//
// Parameters:
// h - {Boolean}
// i - {Editor} Editor instance ( If left out, setting is
// applied to all editors )
//
/////////////////////////////////////////////////////////////////
setHighlightLine: function(h, i) {
if (i) {
i.setHighlightActiveLine(h);
} else {
this.settings.highlightLine = h;
this.forEach(function(i) {
i.setHighlightActiveLine(h);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.highlightLine', h);
},
//////////////////////////////////////////////////////////////////
//
// Show/Hide print margin indicator
//
// Parameters:
// p - {Number} print margin column
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setPrintMargin: function(p, i) {
if (i) {
i.setShowPrintMargin(p);
} else {
this.settings.printMargin = p;
this.forEach(function(i) {
i.setShowPrintMargin(p);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.printMargin', p);
},
//////////////////////////////////////////////////////////////////
//
// Set print margin column
//
// Parameters:
// p - {Number} print margin column
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setPrintMarginColumn: function(p, i) {
if (i) {
i.setPrintMarginColumn(p);
} else {
this.settings.printMarginColumn = p;
this.forEach(function(i) {
i.setPrintMarginColumn(p);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.printMarginColumn', p);
},
//////////////////////////////////////////////////////////////////
//
// Show/Hide indent guides
//
// Parameters:
// g - {Boolean}
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setIndentGuides: function(g, i) {
if (i) {
i.setDisplayIndentGuides(g);
} else {
this.settings.indentGuides = g;
this.forEach(function(i) {
i.setDisplayIndentGuides(g);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.indentGuides', g);
},
//////////////////////////////////////////////////////////////////
//
// Enable/Disable Code Folding
//
// Parameters:
// f - {Boolean}
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setCodeFolding: function(f, i) {
if (i) {
i.setFoldStyle(f);
} else {
this.forEach(function(i) {
i.setFoldStyle(f);
});
}
},
//////////////////////////////////////////////////////////////////
//
// Enable/Disable Line Wrapping
//
// Parameters:
// w - {Boolean}
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setWrapMode: function(w, i) {
if (i) {
i.getSession().setUseWrapMode(w);
} else {
this.forEach(function(i) {
i.getSession().setUseWrapMode(w);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.wrapMode', w);
},
//////////////////////////////////////////////////////////////////
//
// set Tab Size
//
// Parameters:
// s - size
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setTabSize: function(s, i) {
if (i) {
i.getSession().setTabSize(parseInt(s, 10));
} else {
this.forEach(function(i) {
i.getSession().setTabSize(parseInt(s, 10));
});
}
// LocalStorage
localStorage.setItem('atheos.editor.tabSize', s);
},
//////////////////////////////////////////////////////////////////
//
// Enable or disable Soft Tabs
//
// Parameters:
// t - true / false
// i - {Editor} (If omitted, Defaults to all editors)
//
//////////////////////////////////////////////////////////////////
setSoftTabs: function(t, i) {
if (i) {
i.getSession().setUseSoftTabs(t);
} else {
this.forEach(function(i) {
i.getSession().setUseSoftTabs(t);
});
}
// LocalStorage
localStorage.setItem('atheos.editor.softTabs', t);
},
//////////////////////////////////////////////////////////////////
//
// Get content from editor
//
// Parameters:
// i - {Editor} (Defaults to active editor)
//
//////////////////////////////////////////////////////////////////
getContent: function(i) {
i = i || this.getActive();
if (!i) return;
var content = i.getSession().getValue();
if (!content) {
content = ' ';
} // Pass something through
return content;
},
//////////////////////////////////////////////////////////////////
//
// Resize the editor - Trigger the editor to readjust its layout
// esp if the container has been resized manually.
//
// Parameters:
// i - {Editor} (Defaults to active editor)
//
//////////////////////////////////////////////////////////////////
resize: function(i) {
i = i || this.getActive();
if (!i) return;
i.resize();
},
//////////////////////////////////////////////////////////////////
//
// Mark the instance as changed (in the user interface)
// upon change in the document content.
//
// Parameters:
// i - {Editor}
//
//////////////////////////////////////////////////////////////////
changeListener: function(i) {
var self = this;
i.on('change', function() {
atheos.active.markChanged(self.getActive().getSession().path);
});
},
//////////////////////////////////////////////////////////////////
//
// Get Selected Text
//
// Parameters:
// i - {Editor} (Defaults to active editor)
//
//////////////////////////////////////////////////////////////////
getSelectedText: function(i) {
i = i || this.getActive();
if (!i) return;
return i.getCopyText();
},
//////////////////////////////////////////////////////////////////
//
// Insert text
//
// Parameters:
// val - {String} Text to be inserted
// i - {Editor} (Defaults to active editor)
//
//////////////////////////////////////////////////////////////////
insertText: function(val, i) {
i = i || this.getActive();
if (!i) return;
i.insert(val);
},
//////////////////////////////////////////////////////////////////
//
// Move the cursor to a particular line
//
// Parameters:
// line - {Number} Line number
// i - {Editor} Editor instance
//
//////////////////////////////////////////////////////////////////
gotoLine: function(line, i) {
i = i || this.getActive();
if (!i) return;
i.scrollToLine(line, true, true);
i.gotoLine(line, 0, true);
self.focus();
},
//////////////////////////////////////////////////////////////////
//
// Focus an editor
//
// Parameters:
// i - {Editor} Editor instance (Defaults to current editor)
//
//////////////////////////////////////////////////////////////////
focus: function(i) {
i = i || this.getActive();
this.setActive(i);
if (!i) return;
i.focus();
atheos.active.focus(i.getSession().path);
// this.cursorTracking(i);
},
//////////////////////////////////////////////////////////////////
//
// Setup Cursor Tracking
//
// Parameters:
// i - {Editor} (Defaults to active editor)
//
//////////////////////////////////////////////////////////////////
cursorTracking: function(i) {
var col = global.i18n('Col');
carbon.subscribe('chrono.kilo', function() {
var i = atheos.editor.getActive();
if (i) {
var pos = i.getCursorPosition();
oX('#cursor-position').html(`${i18n('Ln')}: ${pos.row + 1}·${col}: ${pos.column}`);
}
});
},
//////////////////////////////////////////////////////////////////
//
// Setup Key bindings
//
// Parameters:
// i - {Editor}
//
//////////////////////////////////////////////////////////////////
bindKeys: function(instance) {
// instance.commands.addCommand({
// name: "Beautify",
// bindKey: {
// win: "Ctrl-Alt-B",
// mac: "Command-Alt-B"
// },
// exec: function() {
// self.beautify();
// }
// });
instance.commands.addCommand({
name: 'Move Up',
bindKey: {
win: 'Ctrl-up',
mac: 'Command-up'
},
exec: function(e) {
atheos.active.move('up');
}
});
instance.commands.addCommand({
name: 'Move Down',
bindKey: {
win: 'Ctrl-down',
mac: 'Command-up'
},
exec: function(e) {
atheos.active.move('down');
}
});
}
};
})(this);