kopia lustrzana https://github.com/c9/core
565 wiersze
19 KiB
JavaScript
565 wiersze
19 KiB
JavaScript
|
|
define(function(require, exports, module) {
|
|
var InputHandler = require("./input");
|
|
var MouseHandler = require("./mouse");
|
|
var Terminal = require("./libterm");
|
|
var ScrollBuffer = require("./scroll_buffer");
|
|
|
|
|
|
// Ace
|
|
var dom = require("ace/lib/dom");
|
|
var Range = require("ace/range").Range;
|
|
var AceEditor = require("ace/editor").Editor;
|
|
var EditSession = require("ace/edit_session").EditSession;
|
|
var VirtualRenderer = require("ace/virtual_renderer").VirtualRenderer;
|
|
|
|
|
|
// TODO
|
|
var Aceterm = function(w, h, writeCb) {
|
|
var terminal = new Terminal(w, h, writeCb);
|
|
terminal.aceSession = Aceterm.createSession(terminal);
|
|
terminal.monitor = Aceterm.createMonitor(terminal.aceSession);
|
|
return terminal;
|
|
};
|
|
|
|
Aceterm.createEditor = function(container, theme) {
|
|
// Create Ace editor instance
|
|
var ace = new AceEditor(new VirtualRenderer(container, theme));
|
|
|
|
ace.setStyle("terminal");
|
|
|
|
// todo move ace colors to container
|
|
var scroller = ace.renderer.scroller;
|
|
scroller.style.color = "inherit";
|
|
scroller.style.background = "inherit";
|
|
|
|
ace.setOptions({
|
|
showPrintMargin: false,
|
|
showGutter: false,
|
|
// readOnly: true,
|
|
scrollPastEnd: 0
|
|
});
|
|
|
|
ace.renderer.setScrollMargin(1, 0);
|
|
|
|
ace.renderer.on("resize", function(oldSize, renderer) {
|
|
var session = renderer.session;
|
|
if (!session || !session.term)
|
|
return;
|
|
var size = renderer.$size;
|
|
var dh = size.scrollerHeight - oldSize.scrollerHeight;
|
|
if (dh < 0) {
|
|
var oldMaxScrollTop = renderer.layerConfig.maxHeight
|
|
- oldSize.scrollerHeight + renderer.scrollMargin.v;
|
|
var scrollTop = session.getScrollTop();
|
|
if (scrollTop > oldMaxScrollTop - renderer.layerConfig.lineHeight) {
|
|
session.setScrollTop(scrollTop - dh);
|
|
}
|
|
}
|
|
var h = ace.renderer.layerConfig.lineHeight;
|
|
if (!h)
|
|
return;
|
|
if (session.resize)
|
|
session.resize();
|
|
|
|
updateScrollMargin(session.term, ace.renderer, dh);
|
|
|
|
if (session.getScrollTop() === 0) {
|
|
session.setScrollTop(-1);
|
|
}
|
|
|
|
});
|
|
|
|
ace.renderer.on("beforeRender", function() {
|
|
if (ace.session && ace.session.term && ace.session.term.refreshTimer)
|
|
ace.session.term.refreshSync();
|
|
});
|
|
|
|
// disable spurious copy event from ace
|
|
ace.onCopy = function() {};
|
|
|
|
// permanently hide hor scrollbar
|
|
ace.renderer.scrollBarH.setVisible(false);
|
|
ace.renderer.scrollBarH.setVisible=function(){};
|
|
|
|
ace.renderer.setOption("vScrollBarAlwaysVisible", true);
|
|
ace.renderer.scrollBarV.element.style.overflowY = "auto";
|
|
|
|
initView(ace);
|
|
ace.termInputHandler = new InputHandler(ace);
|
|
ace.termMouseHandler = new MouseHandler(ace);
|
|
|
|
return ace;
|
|
};
|
|
|
|
Aceterm.createSession = function(term) {
|
|
var session = new EditSession("");
|
|
session.setScrollTop(-1);
|
|
session.term = term;
|
|
|
|
term.copyMode = true;
|
|
|
|
session.doc.getLength = function() {
|
|
if (term.noScrollBack())
|
|
return term.rows;
|
|
return term.rows + term.ybase;
|
|
};
|
|
session.doc.getLine = function(row, newLineChar) {
|
|
if (term.noScrollBack()) {
|
|
row += term.ybase;
|
|
}
|
|
var line = term.lines[row] || [];
|
|
var str = "";
|
|
for (var l = Math.min(line.length, term.cols); l--;)
|
|
if (line[l][1])
|
|
break;
|
|
for (var i = 0; i <= l; i++)
|
|
str += line[i][1] || " ";
|
|
|
|
if (newLineChar && !line.wrapped)
|
|
str += newLineChar;
|
|
|
|
return str;
|
|
};
|
|
|
|
session.getLineData = function(row) {
|
|
if (term.noScrollBack()) {
|
|
row += term.ybase;
|
|
}
|
|
return term.lines[row] || [];
|
|
};
|
|
|
|
session.doc.getLines = function(start, end) {
|
|
var lines = [];
|
|
for (var i = start; i < end; i++) {
|
|
lines.push(this.getLine(i));
|
|
}
|
|
return lines;
|
|
};
|
|
session.doc.$lines = null;
|
|
|
|
session.doc.getTextRange = function(range) {
|
|
var start = range.start.row;
|
|
var end = range.end.row;
|
|
if (start === end) {
|
|
return this.getLine(start).substring(range.start.column, range.end.column);
|
|
}
|
|
var newLineChar = this.getNewLineCharacter();
|
|
var str = this.getLine(start++, newLineChar).substring(range.start.column);
|
|
while (start < end) {
|
|
str += this.getLine(start++, newLineChar);
|
|
}
|
|
str += this.getLine(end, newLineChar).substring(0, range.end.column);
|
|
return str;
|
|
};
|
|
session.$computeWidth =
|
|
session.getScreenWidth = function () {
|
|
return this.term.cols;
|
|
};
|
|
|
|
session.doc.insert = function(pos, str) {
|
|
session.send(str);
|
|
return pos;
|
|
};
|
|
session.doc.remove = function(pos) {
|
|
// can be called from ace, ignore
|
|
return pos;
|
|
};
|
|
|
|
var range = session.selection.getRange();
|
|
range.cursor = range.start = range.end;
|
|
|
|
var dummyDelta = {action:"insertText", start: range.clone().start, end: range.clone().end, lines:[""]};
|
|
|
|
session.on("changeEditor", function(e, session) {
|
|
if (e.oldEditor) {
|
|
if (session.ace == e.oldEditor)
|
|
session.ace = null;
|
|
}
|
|
if (e.editor) {
|
|
if (session.ace) {
|
|
console.warn("aceterm session isn't detached");
|
|
}
|
|
session.ace = e.editor;
|
|
}
|
|
if (!session.selectionRange) {
|
|
session.selectionRange = range;
|
|
session.ace.addSelectionMarker(range);
|
|
} else if (session.term) {
|
|
// update scroll position if there was more output while session was detached
|
|
session.term.refreshView(0, 0);
|
|
}
|
|
});
|
|
|
|
session.term.refreshView = function(start, end) {
|
|
var ace = session.ace;
|
|
if (!ace)
|
|
return;
|
|
|
|
var term = session.term;
|
|
var renderer = ace.renderer;
|
|
|
|
var cursorRow = Math.max(term.y, 0);
|
|
range.cursor.row = cursorRow;
|
|
var scrollTop = 0;
|
|
var h = ace.renderer.layerConfig.lineHeight;
|
|
if (!term.noScrollBack()) {
|
|
range.cursor.row += term.ybase;
|
|
start += term.ybase;
|
|
end += term.ybase;
|
|
|
|
scrollTop = term.ybase * h + renderer.scrollMargin.bottom;
|
|
var maxScrollTop = Math.min(session.maxScrollTop, scrollTop);
|
|
if (session.getScrollTop() < (maxScrollTop || 0) - 2 * h)
|
|
scrollTop = null;
|
|
}
|
|
|
|
if (ace.$mouseHandler.isMousePressed)
|
|
scrollTop = null;
|
|
|
|
if (scrollTop !== null) {
|
|
updateScrollMargin(term, renderer, 0);
|
|
|
|
session.setScrollTop(scrollTop || -1);
|
|
session.maxScrollTop = scrollTop;
|
|
} else
|
|
renderer.$loop.schedule(renderer.CHANGE_SCROLL);
|
|
|
|
renderer.updateLines(start, end);
|
|
range.cursor.column = term.x;
|
|
if (scrollTop !== null)
|
|
session.selection.setSelectionRange(range);
|
|
|
|
// needed for things like search to work correctly
|
|
session.doc._signal("change", dummyDelta);
|
|
};
|
|
session.term.isRenderPending = function() {
|
|
var ace = session.ace;
|
|
if (!ace)
|
|
return;
|
|
return ace.renderer.$loop.pending;
|
|
};
|
|
session.bgTokenizer.$worker=function(){};
|
|
session.term.on("input", function() {
|
|
if (session.maxScrollTop) {
|
|
session.maxScrollTop = 0;
|
|
session.setScrollTop(Number.MAX_VALUE);
|
|
}
|
|
});
|
|
session.setScrollLeft = function (scrollLeft) {
|
|
if (scrollLeft) {
|
|
this.$scrollLeft = 0;
|
|
this._signal("changeScrollLeft", 0);
|
|
}
|
|
};
|
|
|
|
session.send = function(str) {
|
|
this.term.send(str);
|
|
};
|
|
|
|
return session;
|
|
};
|
|
|
|
Aceterm.createMonitor = function(session) {
|
|
session.term.on("input", function(str) {
|
|
monitor.$lastInput = str;
|
|
var term = session.term;
|
|
if (str === "\r") {
|
|
var row = term.y + term.ybase;
|
|
var command = monitor.getCommand(row);
|
|
if (command) {
|
|
// wait for \n to know what command was typed
|
|
term.on("afterWrite", waitForNewLine);
|
|
term.lines[row].isUserInput = true;
|
|
monitor.lastCommandRow = row;
|
|
monitor.$command = monitor.getCommand(monitor.lastCommandRow).trim();
|
|
}
|
|
}
|
|
|
|
if (str === "\r" && monitor.$command === "clear" || str === "\x0c") //"ctrl-l"
|
|
if (!term.noScrollBack()) {
|
|
term.once("beforeWrite", function fixClear(data) {
|
|
if (data == "\r\n") {
|
|
term.once("beforeWrite", fixClear);
|
|
return "";
|
|
}
|
|
var m = /^\u001b\[H\u001b\[K\r\n((?:\u001b\[K\u001b\[1B)+)\u001b\[K\u001b\[H(.*)$/.exec(data);
|
|
if (!m) return;
|
|
data = Array(term.rows + 1).join("\r\n") + "\u001b[H" + m[2];
|
|
return data;
|
|
});
|
|
}
|
|
});
|
|
function waitForNewLine(data) {
|
|
if (data.indexOf("\n") >= 0) {
|
|
session.term.off("afterWrite", waitForNewLine);
|
|
monitor.$command = monitor.getCommand(monitor.lastCommandRow).trim();
|
|
}
|
|
}
|
|
|
|
var monitor = {
|
|
lastCommandRow: -1,
|
|
getLastCommand: function() {
|
|
return this.$command;
|
|
},
|
|
getCommand: function(row) {
|
|
|
|
var line = session.getLine(row);
|
|
var i = line.indexOf("$");
|
|
var command = i == -1 ? "" : line.substr(i + 1);
|
|
return command;
|
|
},
|
|
getLastKey: function() {
|
|
return monitor.$lastInput;
|
|
},
|
|
wasQuitSent: function() {
|
|
return this.$command === "exit"
|
|
|| this.$command === "logout"
|
|
|| this.$lastInput === "\u0004"; // Ctrl-D
|
|
}
|
|
};
|
|
return monitor;
|
|
};
|
|
|
|
module.exports = Aceterm;
|
|
|
|
function updateScrollMargin(terminal, renderer, heightDiff) {
|
|
// if (window.updateScrollMargin)
|
|
// return window.updateScrollMargin.apply(this, arguments);
|
|
var sm = renderer.scrollMargin;
|
|
var cursorRow = Math.max(terminal.y, 0);
|
|
var l = terminal.ybase + terminal.rows - 1;
|
|
var lastEmpty = l;
|
|
while (lastEmpty > cursorRow && terminal.isLineEmpty(terminal.lines[lastEmpty])) {
|
|
lastEmpty--;
|
|
}
|
|
var noScrollbar = terminal.noScrollBack();
|
|
|
|
var margin = 0;
|
|
var marginTop = 0;
|
|
var marginBottom = 0;
|
|
var oldMargin = sm.bottom || 0;
|
|
var lineHeight = renderer.layerConfig.lineHeight;
|
|
var height = renderer.$size.height - 1;
|
|
margin = height % lineHeight;
|
|
sm.preffered = margin;
|
|
|
|
var preferTop = lastEmpty < l;
|
|
var prevState = sm.snapToTop || [0, 0, 0, 0];
|
|
var snapToTop = [lastEmpty, terminal.ybase, l, sm.preffered, terminal.y, preferTop];
|
|
sm.snapToTop = snapToTop;
|
|
|
|
var allowSnap = snapToTop[1] > prevState[1] && (
|
|
snapToTop[4] === 0 || !preferTop && !prevState[5]
|
|
);
|
|
// var scrollTop = terminal.aceSession.maxScrollTop;
|
|
|
|
if (noScrollbar) {
|
|
marginTop = Math.ceil(sm.preffered / 2);
|
|
marginBottom = sm.preffered - marginTop;
|
|
}
|
|
else if (heightDiff) {
|
|
// TODO this causes cursor to be half visible sometimes
|
|
// if (heightDiff > 0 || !preferTop) {
|
|
// if (oldMargin < 0) {
|
|
// oldMargin += heightDiff;
|
|
// oldMargin = oldMargin % lineHeight;
|
|
// }
|
|
// marginBottom = Math.min(margin, oldMargin);
|
|
// } else {
|
|
// if (oldMargin >= margin - lineHeight) {
|
|
// oldMargin += heightDiff;
|
|
// oldMargin = oldMargin % lineHeight;
|
|
// marginBottom = Math.max(margin - lineHeight, oldMargin);
|
|
// } else {
|
|
// marginBottom = Math.min(margin - lineHeight, oldMargin);
|
|
// }
|
|
// }
|
|
marginBottom = Math.min(margin, oldMargin);
|
|
}
|
|
else if (allowSnap) {
|
|
if (preferTop) {
|
|
marginBottom = sm.preffered;
|
|
marginTop = 0;
|
|
} else {
|
|
marginBottom = marginTop = 0;
|
|
}
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
renderer.setScrollMargin(1 + marginTop, marginBottom);
|
|
}
|
|
|
|
function initCursor() {
|
|
this.cursorClass = "reverse-video";
|
|
this.getCursorNode = function() {
|
|
if (!this.textLayer.$cur) {
|
|
this.textLayer.$cur =
|
|
this.textLayer.element.querySelector(".reverse-video");
|
|
}
|
|
|
|
return this.textLayer.$cur || {};
|
|
};
|
|
this.hideCursor = function() {
|
|
this.isVisible = false;
|
|
dom.addCssClass(this.element, "ace_hidden-cursors");
|
|
this.restartTimer();
|
|
};
|
|
|
|
this.showCursor = function() {
|
|
this.isVisible = true;
|
|
dom.removeCssClass(this.element, "ace_hidden-cursors");
|
|
this.restartTimer();
|
|
};
|
|
|
|
this.restartTimer = function() {
|
|
clearInterval(this.intervalId);
|
|
clearTimeout(this.timeoutId);
|
|
|
|
this.getCursorNode().className = this.cursorClass;
|
|
|
|
if (!this.blinkInterval || !this.isVisible || !Terminal.cursorBlink)
|
|
return;
|
|
|
|
this.intervalId = setInterval(function() {
|
|
if (this.renderer.$loop.changes) return;
|
|
var node = this.getCursorNode();
|
|
node.className = node.className ? "" : this.cursorClass;
|
|
}.bind(this), 500);
|
|
};
|
|
|
|
this.update = function(config) {
|
|
this.config = config;
|
|
|
|
var pixelPos = this.getPixelPosition(null, true);
|
|
|
|
this.restartTimer();
|
|
|
|
// cache for textarea and gutter highlight
|
|
this.$pixelPos = pixelPos;
|
|
this.restartTimer();
|
|
};
|
|
}
|
|
|
|
function initView(ace) {
|
|
initCursor.call(ace.renderer.$cursorLayer);
|
|
ace.renderer.$cursorLayer.renderer = ace.renderer;
|
|
ace.renderer.$cursorLayer.textLayer = ace.renderer.$textLayer;
|
|
|
|
// allow for 1px of cursor outline
|
|
ace.renderer.content.style.overflow = "visible";
|
|
ace.renderer.$textLayer.element.style.overflow = "visible";
|
|
|
|
ace.renderer.$textLayer.$renderLine = Aceterm.renderLine;
|
|
|
|
ace.renderer.$textLayer.$renderLineInner = Aceterm.renderLineInner;
|
|
|
|
ace.setOption("showPrintMargin", false);
|
|
ace.setOption("highlightActiveLine", false);
|
|
}
|
|
|
|
Aceterm.renderLine = function(stringBuilder, row, onlyContents, foldLine) {
|
|
if (!onlyContents) {
|
|
stringBuilder.push(
|
|
"<div class='ace_line' style='height:", this.config.lineHeight, "px'>"
|
|
);
|
|
}
|
|
this.$renderLineInner(stringBuilder, row);
|
|
|
|
if (!onlyContents)
|
|
stringBuilder.push("</div>");
|
|
};
|
|
|
|
Aceterm.renderLineInner = function(stringBuilder, row) {
|
|
var term = this.session.term;
|
|
if (!term)
|
|
return;
|
|
var fgColor, bgColor, flags;
|
|
var width = term.cols;
|
|
var cursorY = term.y;
|
|
|
|
if (term.noScrollBack()) {
|
|
row += term.ybase;
|
|
}
|
|
|
|
cursorY += term.ybase;
|
|
|
|
|
|
var line = term.lines[row] || [];
|
|
var out = '';
|
|
|
|
var x = row === cursorY && term.cursorState && !term.cursorHidden
|
|
? term.x
|
|
: -1;
|
|
|
|
var defAttr = term.defAttr;
|
|
var attr = defAttr;
|
|
for (var i = 0; i < width; i++) {
|
|
var token = line[i] || term.ch;
|
|
var data = token[0];
|
|
var ch = token[1];
|
|
|
|
if (i === x) data = -1;
|
|
|
|
if (data !== attr) {
|
|
if (attr !== defAttr)
|
|
out += '</span>';
|
|
if (data === defAttr) {
|
|
// do nothing
|
|
} else if (data === -1) {
|
|
out += '<span class="reverse-video">';
|
|
this.$cur = null;
|
|
} else {
|
|
out += '<span style="';
|
|
|
|
bgColor = data & 0x1ff;
|
|
fgColor = (data >> 9) & 0x1ff;
|
|
flags = data >> 18;
|
|
|
|
if (flags & 1) {
|
|
if (this.$fontMetrics.allowBoldFonts)
|
|
out += 'font-weight:bold;';
|
|
// see: XTerm*boldColors
|
|
if (fgColor < 8)
|
|
fgColor += 8;
|
|
}
|
|
|
|
if (flags & 2)
|
|
out += 'text-decoration:underline;';
|
|
|
|
if (bgColor === 256) {
|
|
if (fgColor !== 257)
|
|
out += 'color:' + (
|
|
Terminal.overridenColors[fgColor] ||
|
|
Terminal.colors[fgColor]
|
|
) + ';';
|
|
} else {
|
|
out += 'background-color:' + Terminal.colors[bgColor] + ';';
|
|
if (fgColor !== 257)
|
|
out += 'color:' + Terminal.colors[fgColor] + ';';
|
|
out += 'display:inline-block" class="aceterm-line-bg" l="' + i;
|
|
}
|
|
out += '">';
|
|
}
|
|
}
|
|
|
|
|
|
if (ch <= ' ')
|
|
out += '\xa0';
|
|
else if (ch == '&')
|
|
out += '&';
|
|
else if (ch == '<')
|
|
out += '<';
|
|
else
|
|
out += ch;
|
|
|
|
attr = data;
|
|
}
|
|
|
|
if (attr !== defAttr)
|
|
out += '</span>';
|
|
stringBuilder.push(out);
|
|
};
|
|
|
|
}); |