jam/js/web/readline.js

730 lines
18 KiB
JavaScript

/* ------------------------------------------------------------------------*
* Copyright 2013-2014 Arne F. Claassen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*-------------------------------------------------------------------------*/
var Josh = Josh || {};
Josh.Version = "0.2.10";
(function(root) {
Josh.Keys = {
Special: {
Backspace: 8,
Tab: 9,
Enter: 13,
Pause: 19,
CapsLock: 20,
Escape: 27,
Space: 32,
PageUp: 33,
PageDown: 34,
End: 35,
Home: 36,
Left: 37,
Up: 38,
Right: 39,
Down: 40,
Insert: 45,
Delete: 46
}
};
Josh.ReadLine = function(config) {
config = config || {};
// instance fields
var _console = config.console || (Josh.Debug && root.console ? root.console : {
log: function() {
}
});
var _history = config.history || new Josh.History();
var _killring = config.killring || new Josh.KillRing();
var _boundToElement = config.element ? true : false;
var _element = config.element || root;
var _active = false;
var _onActivate;
var _onDeactivate;
var _onCompletion;
var _onEnter;
var _onChange;
var _onCancel;
var _onEOT;
var _onClear;
var _onSearchStart;
var _onSearchEnd;
var _onSearchChange;
var _inSearch = false;
var _searchMatch;
var _lastSearchText = '';
var _text = '';
var _cursor = 0;
var _lastCmd;
var _completionActive;
var _cmdQueue = [];
var _suspended = false;
var _cmdMap = {
complete: cmdComplete,
done: cmdDone,
noop: cmdNoOp,
history_top: cmdHistoryTop,
history_end: cmdHistoryEnd,
history_next: cmdHistoryNext,
history_previous: cmdHistoryPrev,
end: cmdEnd,
home: cmdHome,
left: cmdLeft,
right: cmdRight,
cancel: cmdCancel,
'delete': cmdDeleteChar,
backspace: cmdBackspace,
kill_eof: cmdKillToEOF,
kill_wordback: cmdKillWordBackward,
kill_wordforward: cmdKillWordForward,
yank: cmdYank,
clear: cmdClear,
search: cmdReverseSearch,
wordback: cmdBackwardWord,
wordforward: cmdForwardWord,
yank_rotate: cmdRotate
};
var _keyMap = {
'default': {
8: cmdBackspace, // Backspace
9: cmdComplete, // Tab
13: cmdDone, // Enter
27: cmdEsc, // Esc
33: cmdHistoryTop, // Page Up
34: cmdHistoryEnd, // Page Down
35: cmdEnd, // End
36: cmdHome, // Home
37: cmdLeft, // Left
38: cmdHistoryPrev, // Up
39: cmdRight, // Right
40: cmdHistoryNext, // Down
46: cmdDeleteChar, // Delete
10: cmdNoOp, // Pause
19: cmdNoOp, // Caps Lock
45: cmdNoOp // Insert
},
control: {
65: cmdHome, // A
66: cmdLeft, // B
67: cmdCancel, // C
68: cmdDeleteChar, // D
69: cmdEnd, // E
70: cmdRight, // F
80: cmdHistoryPrev, // P
78: cmdHistoryNext, // N
75: cmdKillToEOF, // K
89: cmdYank, // Y
76: cmdClear, // L
82: cmdReverseSearch // R
},
meta: {
8: cmdKillWordBackward, // Backspace
66: cmdBackwardWord, // B
68: cmdKillWordForward, // D
70: cmdForwardWord, // F
89: cmdRotate // Y
}
};
// public methods
var self = {
isActive: function() {
return _active;
},
activate: function() {
_active = true;
if(_onActivate) {
_onActivate();
}
},
deactivate: function() {
_active = false;
if(_onDeactivate) {
_onDeactivate();
}
},
bind: function(key, action) {
var k = getKey(key);
var cmd = _cmdMap[action];
if(!cmd) {
return;
}
_keyMap[k.modifier][k.code];
},
unbind: function(key) {
var k = getKey(key);
delete _keyMap[k.modifier][k.code];
},
attach: function(el) {
if(_element) {
self.detach();
}
_console.log("attaching");
_console.log(el);
_element = el;
_boundToElement = true;
addEvent(_element, "focus", self.activate);
addEvent(_element, "blur", self.deactivate);
subscribeToKeys();
},
detach: function() {
removeEvent(_element, "focus", self.activate);
removeEvent(_element, "blur", self.deactivate);
_element = null;
_boundToElement = false;
},
onActivate: function(completionHandler) {
_onActivate = completionHandler;
},
onDeactivate: function(completionHandler) {
_onDeactivate = completionHandler;
},
onChange: function(changeHandler) {
_onChange = changeHandler;
},
onClear: function(completionHandler) {
_onClear = completionHandler;
},
onEnter: function(enterHandler) {
_onEnter = enterHandler;
},
onCompletion: function(completionHandler) {
_onCompletion = completionHandler;
},
onCancel: function(completionHandler) {
_onCancel = completionHandler;
},
onEOT: function(completionHandler) {
_onEOT = completionHandler;
},
onSearchStart: function(completionHandler) {
_onSearchStart = completionHandler;
},
onSearchEnd: function(completionHandler) {
_onSearchEnd = completionHandler;
},
onSearchChange: function(completionHandler) {
_onSearchChange = completionHandler;
},
getLine: function() {
return {
text: _text,
cursor: _cursor
};
},
setLine: function(line) {
_text = line.text;
_cursor = line.cursor;
refresh();
}
};
// private methods
function addEvent(element, name, callback) {
if(element.addEventListener) {
element.addEventListener(name, callback, false);
} else if(element.attachEvent) {
element.attachEvent('on' + name, callback);
}
}
function removeEvent(element, name, callback) {
if(element.removeEventListener) {
element.removeEventListener(name, callback, false);
} else if(element.detachEvent) {
element.detachEvent('on' + name, callback);
}
}
function getKeyInfo(e) {
var code = e.keyCode || e.charCode;
var c = String.fromCharCode(code);
return {
code: code,
character: c,
shift: e.shiftKey,
control: e.controlKey,
alt: e.altKey,
isChar: true
};
}
function getKey(key) {
var k = {
modifier: 'default',
code: key.keyCode
};
if(key.metaKey || key.altKey) {
k.modifier = 'meta';
} else if(key.ctrlKey) {
k.modifier = 'control';
}
if(key['char']) {
k.code = key['char'].charCodeAt(0);
}
return k;
}
function queue(cmd) {
if(_suspended) {
_cmdQueue.push(cmd);
return;
}
call(cmd);
}
function call(cmd) {
_console.log('calling: ' + cmd.name + ', previous: ' + _lastCmd);
if(_inSearch && cmd.name != "cmdKeyPress" && cmd.name != "cmdReverseSearch") {
_inSearch = false;
if(cmd.name == 'cmdEsc') {
_searchMatch = null;
}
if(_searchMatch) {
if(_searchMatch.text) {
_cursor = _searchMatch.cursoridx;
_text = _searchMatch.text;
_history.applySearch();
}
_searchMatch = null;
}
if(_onSearchEnd) {
_onSearchEnd();
}
}
if(!_inSearch && _killring.isinkill() && cmd.name.substr(0, 7) != 'cmdKill') {
_killring.commit();
}
_lastCmd = cmd.name;
cmd();
}
function suspend(asyncCall) {
_suspended = true;
asyncCall(resume);
}
function resume() {
var cmd = _cmdQueue.shift();
if(!cmd) {
_suspended = false;
return;
}
call(cmd);
resume();
}
function cmdNoOp() {
// no-op, used for keys we capture and ignore
}
function cmdEsc() {
// no-op, only has an effect on reverse search and that action was taken in call()
}
function cmdBackspace() {
if(_cursor == 0) {
return;
}
--_cursor;
_text = remove(_text, _cursor, _cursor + 1);
refresh();
}
function cmdComplete() {
if(!_onCompletion) {
return;
}
suspend(function(resumeCallback) {
_onCompletion(self.getLine(), function(completion) {
if(completion) {
_text = insert(_text, _cursor, completion);
updateCursor(_cursor + completion.length);
}
_completionActive = true;
resumeCallback();
});
});
}
function cmdDone() {
if(!_text) {
return;
}
var text = _text;
_history.accept(text);
_text = '';
_cursor = 0;
if(!_onEnter) {
return;
}
suspend(function(resumeCallback) {
_onEnter(text, function(text) {
if(text) {
_text = text;
_cursor = _text.length;
}
if(_onChange) {
_onChange(self.getLine());
}
resumeCallback();
});
});
}
function cmdEnd() {
updateCursor(_text.length);
}
function cmdHome() {
updateCursor(0);
}
function cmdLeft() {
if(_cursor == 0) {
return;
}
updateCursor(_cursor - 1);
}
function cmdRight() {
if(_cursor == _text.length) {
return;
}
updateCursor(_cursor + 1);
}
function cmdBackwardWord() {
if(_cursor == 0) {
return;
}
updateCursor(findBeginningOfPreviousWord());
}
function cmdForwardWord() {
if(_cursor == _text.length) {
return;
}
updateCursor(findEndOfCurrentWord());
}
function cmdHistoryPrev() {
if(!_history.hasPrev()) {
return;
}
getHistory(_history.prev);
}
function cmdHistoryNext() {
if(!_history.hasNext()) {
return;
}
getHistory(_history.next);
}
function cmdHistoryTop() {
getHistory(_history.top);
}
function cmdHistoryEnd() {
getHistory(_history.end);
}
function cmdDeleteChar() {
if(_text.length == 0) {
if(_onEOT) {
_onEOT();
return;
}
}
if(_cursor == _text.length) {
return;
}
_text = remove(_text, _cursor, _cursor + 1);
refresh();
}
function cmdCancel() {
if(_onCancel) {
_onCancel();
}
}
function cmdKillToEOF() {
_killring.append(_text.substr(_cursor));
_text = _text.substr(0, _cursor);
refresh();
}
function cmdKillWordForward() {
if(_text.length == 0) {
return;
}
if(_cursor == _text.length) {
return;
}
var end = findEndOfCurrentWord();
if(end == _text.length - 1) {
return cmdKillToEOF();
}
_killring.append(_text.substring(_cursor, end))
_text = remove(_text, _cursor, end);
refresh();
}
function cmdKillWordBackward() {
if(_cursor == 0) {
return;
}
var oldCursor = _cursor;
_cursor = findBeginningOfPreviousWord();
_killring.prepend(_text.substring(_cursor, oldCursor));
_text = remove(_text, _cursor, oldCursor);
refresh();
}
function cmdYank() {
var yank = _killring.yank();
if(!yank) {
return;
}
_text = insert(_text, _cursor, yank);
updateCursor(_cursor + yank.length);
}
function cmdRotate() {
var lastyanklength = _killring.lastyanklength();
if(!lastyanklength) {
return;
}
var yank = _killring.rotate();
if(!yank) {
return;
}
var oldCursor = _cursor;
_cursor = _cursor - lastyanklength;
_text = remove(_text, _cursor, oldCursor);
_text = insert(_text, _cursor, yank);
updateCursor(_cursor + yank.length);
}
function cmdClear() {
if(_onClear) {
_onClear();
} else {
refresh();
}
}
function cmdReverseSearch() {
if(!_inSearch) {
_inSearch = true;
if(_onSearchStart) {
_onSearchStart();
}
if(_onSearchChange) {
_onSearchChange({});
}
} else {
if(!_searchMatch) {
_searchMatch = {term: ''};
}
search();
}
}
function updateCursor(position) {
_cursor = position;
refresh();
}
function addText(c) {
_text = insert(_text, _cursor, c);
++_cursor;
refresh();
}
function addSearchText(c) {
if(!_searchMatch) {
_searchMatch = {term: ''};
}
_searchMatch.term += c;
search();
}
function search() {
_console.log("searchtext: " + _searchMatch.term);
var match = _history.search(_searchMatch.term);
if(match != null) {
_searchMatch = match;
_console.log("match: " + match);
if(_onSearchChange) {
_onSearchChange(match);
}
}
}
function refresh() {
if(_onChange) {
_onChange(self.getLine());
}
}
function getHistory(historyCall) {
_history.update(_text);
_text = historyCall();
updateCursor(_text.length);
}
function findBeginningOfPreviousWord() {
var position = _cursor - 1;
if(position < 0) {
return 0;
}
var word = false;
for(var i = position; i > 0; i--) {
var word2 = isWordChar(_text[i]);
if(word && !word2) {
return i + 1;
}
word = word2;
}
return 0;
}
function findEndOfCurrentWord() {
if(_text.length == 0) {
return 0;
}
var position = _cursor + 1;
if(position >= _text.length) {
return _text.length - 1;
}
var word = false;
for(var i = position; i < _text.length; i++) {
var word2 = isWordChar(_text[i]);
if(word && !word2) {
return i;
}
word = word2;
}
return _text.length - 1;
}
function isWordChar(c) {
if(c == undefined) {
return false;
}
var code = c.charCodeAt(0);
return (code >= 48 && code <= 57)
|| (code >= 65 && code <= 90)
|| (code >= 97 && code <= 122);
}
function remove(text, from, to) {
if(text.length <= 1 || text.length <= to - from) {
return '';
}
if(from == 0) {
// delete leading characters
return text.substr(to);
}
var left = text.substr(0, from);
var right = text.substr(to);
return left + right;
}
function insert(text, idx, ins) {
if(idx == 0) {
return ins + text;
}
if(idx >= text.length) {
return text + ins;
}
var left = text.substr(0, idx);
var right = text.substr(idx);
return left + ins + right;
}
function subscribeToKeys() {
// set up key capture
_element.onkeydown = function(e) {
e = e || window.event;
// return as unhandled if we're not active or the key is just a modifier key
if(!_active || e.keyCode == 16 || e.keyCode == 17 || e.keyCode == 18 || e.keyCode == 91) {
return true;
}
// check for some special first keys, regardless of modifiers
_console.log("key: " + e.keyCode);
var cmd = _keyMap['default'][e.keyCode];
// intercept ctrl- and meta- sequences (may override the non-modifier cmd captured above
var mod;
if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
mod = _keyMap.control[e.keyCode];
if(mod) {
cmd = mod;
}
} else if((e.altKey || e.metaKey) && !e.ctrlKey && !e.shiftKey) {
mod = _keyMap.meta[e.keyCode];
if(mod) {
cmd = mod;
}
}
if(!cmd) {
return true;
}
queue(cmd);
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
return false;
};
_element.onkeypress = function(e) {
if(!_active) {
return true;
}
var key = getKeyInfo(e);
if(key.code == 0 || e.defaultPrevented || e.metaKey || e.altKey || e.ctrlKey) {
return false;
}
queue(function cmdKeyPress() {
if(_inSearch) {
addSearchText(key.character);
} else {
addText(key.character);
}
});
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
return false;
};
}
if(_boundToElement) {
self.attach(_element);
} else {
subscribeToKeys();
}
return self;
};
})(this);