* Code cleanup

* MultiLineEditTextView2 -> MultiLineEditTextView
This commit is contained in:
Bryan Ashby 2015-07-01 20:35:03 -06:00
parent c1469a1f9c
commit bb7504a35a
3 changed files with 28 additions and 535 deletions

View File

@ -11,7 +11,7 @@ var ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
var MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; var MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
//var MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; //var MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView;
var MultiLineEditTextView = require('./multi_line_edit_text_view2.js').MultiLineEditTextView2; var MultiLineEditTextView = require('./multi_line_edit_text_view2.js').MultiLineEditTextView;
var Config = require('./config.js').config; var Config = require('./config.js').config;
var ansi = require('./ansi_term.js'); var ansi = require('./ansi_term.js');

View File

@ -1,518 +0,0 @@
/* jslint node: true */
'use strict';
var View = require('./view.js').View;
var miscUtil = require('./misc_util.js');
var strUtil = require('./string_util.js');
var ansi = require('./ansi_term.js');
var assert = require('assert');
var _ = require('lodash');
var GapBuffer = require('gapbuffer').GapBuffer;
//
// Notes
// * options.tabSize can be used to resolve \t
// * See https://github.com/dominictarr/hipster/issues/15 about insert/delete lines
//
// Blessed
// insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height)
// deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height)
// Quick Ansi -- update only what was changed:
// https://github.com/dominictarr/quickansi
//
// This thread is awesome:
// https://github.com/dominictarr/hipster/issues/15
//
// See Atom's implementations
// Newer TextDocument
// https://github.com/atom/text-document
//
// Older TextBuffer
// http://www.oscon.com/oscon2014/public/schedule/detail/37593
//
// Span Skip List could be used for mappings of rows/cols (display) to
// character offsets in a buffer
// https://github.com/atom/span-skip-list
//
// Buffer: Actual text buffer
// Transform: Display of soft wrap & tab expansion (e.g. tab -> ' ' * tabWidth)
//
//
// General Design
//
// * Take any existing input & word wrap into lines[] preserving
// formatting characters.
// * When drawing, formatting characters are processed but not shown
// or processed directly in many cases. E.g., \n is processed but simply
// causes us to go to our "next line" visibly.
// * Empty/blank lines = \n
//
exports.MultiLineEditTextView = MultiLineEditTextView;
//
// Some resources & comparisons
//
// Enthral @ https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp
// * Tabs are ignored
// * Preview/reading mode processes colors, otherwise just text (e.g. editor)
//
// x84 @ https://github.com/jquast/x84/blob/master/x84/bbs/editor.py
//
// Syncronet
//
//
// Projects of use/interest:
//
// https://github.com/atom/text-buffer
// http://danieltao.com/lazy.js/
// http://www.jbox.dk/downloads/edit.c
// https://github.com/slap-editor/slap
// https://github.com/chjj/blessed
//
// need self.skipTabs(dir): if pos='\t', skip ahead (in dir) until reg char. This can be used @ up, left, right, down
function MultiLineEditTextView(options) {
if(!_.isBoolean(options.acceptsFocus)) {
options.acceptsFocus = true;
}
if(!_.isBoolean(this.acceptsInput)) {
options.acceptsInput = true;
}
View.call(this, options);
//
// defualt tabWidth is 8
// See the following:
// * http://www.ansi-bbs.org/ansi-bbs2/control_chars/
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
//
this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 8;
var self = this;
this.renderBuffer = [];
this.textBuffer = new GapBuffer(1024);
this.lines = []; // a given line is text...until EOL
this.topLineIndex = 0;
this.cursorPos = { row : 0, col : 0 }; // relative to view window
this.renderStartIndex = 0;
this.getTabString = function() {
return new Array(self.tabWidth).join(' ');
};
this.getRenderLine = function(line) {
// :TODO: fix tabbing here
line = line.replace(self.getReplaceTabsRegExp(), self.getTabString()).replace(/\n/g, '');
var remain = self.dimens.width - line.length;
if(remain > 0) {
line += new Array(remain).join(' ');
}
return line;
};
this.redrawViewableText = function() {
var row = self.position.row;
var bottom = row + self.dimens.height;
var i = self.topLineIndex;
self.client.term.write(self.getSGR());
while(i < self.renderBuffer.length && row < bottom) {
self.client.term.write(ansi.goto(row, this.position.col));
self.client.term.write(self.getRenderLine(self.renderBuffer[i]));
++row;
++i;
}
};
this.wordWrap = function(line) {
//
// Other implementations:
// * http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
// * http://james.padolsey.com/snippets/wordwrap-for-javascript/
// * http://phpjs.org/functions/wordwrap/
// * https://github.com/jonschlinkert/word-wrap
//
var re = new RegExp(
'.{1,' + self.dimens.width + '}(\\s+|$)|\\S+?(\\s+|$)', 'g');
return line.match(re) || [];
};
this.wordWrap3 = function(line, width) {
var re = new RegExp('.{1,' + width + '}(\\s+|$)|\\S+?(\\s+|$)', 'g');
return line.replace(/\t/g, new Array(self.tabWidth).join('\t')).match(re) || [];
};
this.updateRenderBuffer = function() {
//
// We can estimate what is visible:
// * Starting point = start of buffer or previous LF from where we were previously
// * Ending point = start + width * height (max chars possible)
// If this system is kept, this can be optimized as per above
//
self.renderBuffer = [];
// :TODO: optimize this with asArray() taking the slice information
var lines = self.textBuffer.asArray()//.slice(self.renderStartIndex, self.renderStartIndex + self.dimens.width * self.dimens.height)
.join('')
.split(/\r\n|\n|\r/g);
var maxLines = self.dimens.height - self.position.row;
for(var i = 0; i < lines.length && self.renderBuffer.length < maxLines; ++i) {
if(0 === lines[i].length) {
self.renderBuffer.push('');
} else {
Array.prototype.push.apply(self.renderBuffer, self.wordWrap3(lines[i] + '\n', self.dimens.width));
}
}
};
this.getReplaceTabsRegExp = function() {
return new RegExp('\\t{' + (self.tabWidth - 1) + '}', 'g');
};
this.getTextBufferPosition = function(row, col) {
var replaceTabsRe = self.getReplaceTabsRegExp();
var pos = 0;
for(var r = 0; r < row; ++r) {
if(self.renderBuffer[r].length > 0) {
pos += self.renderBuffer[r].replace(replaceTabsRe, '\t').length;
} else {
pos += 1;
}
}
pos += self.renderBuffer[row]
.slice(0, Math.min(col, self.dimens.width))
.replace(replaceTabsRe, '\t')
.length;
return pos;
};
this.getLineTextLength = function(row) {
return self.renderBuffer[row].replace(self.getReplaceTabsRegExp(), '\t').replace(/\n/g, '').length;
//return self.renderBuffer[row].replace(/\n/g, '').length;
};
this.getRenderTextLength = function(row) {
return self.renderBuffer[row].replace(/\n/g, '').length;
};
// :TODO: this name makes no sense...
this.getLineTextLengthToColumn = function(row, col) {
return self.renderBuffer[row].replace(self.getReplaceTabsRegExp(), '\t').replace(/\n/g, '').slice(0, col).length;
};
this.getEndOfLineColumn = function(row) {
if(!_.isNumber(row)) {
row = self.cursorPos.row;
}
return self.getLineTextLength(row);
};
this.getAbsolutePosition = function(row, col) {
if(!_.isNumber(row)) {
row = self.cursorPos.row;
}
if(!_.isNumber(col)) {
col = self.cursorPos.col;
}
return { row : self.position.row + row, col : self.position.col + col };
};
// :TODO: rename this to show it is the *buffer*
this.getCharAtCursorPosition = function() {
var pos = self.getTextBufferPosition(self.cursorPos.row, self.cursorPos.col);
return self.textBuffer.get(pos);
};
this.getRenderCharAtRowAndColumn = function(row, col) {
return self.renderBuffer[row][col];
};
this.getRenderCharAtCursorPosition = function() {
return self.getRenderCharAtRowAndColumn(self.cursorPos.row, self.cursorPos.col);
};
this.getRemainingRowsFromCurrent = function() {
return Math.min(self.dimens.height, self.renderBuffer.length) - (self.cursorPos.row + 1);
};
this.moveCursorTo = function(row, col) {
var absPos = self.getAbsolutePosition(row, col);
self.client.term.write(ansi.goto(absPos.row, absPos.col));
};
this.scrollUp = function(count) {
};
this.scrollDown = function(count) {
};
this.cursorMoveJumpTab = function(cursorDir) {
if('\t' !== self.getRenderCharAtCursorPosition()) {
return; // nothing to do
}
//
// A few scenarios:
// * Cursor just moved up or down and we got dumped in the middle of a tab sequence.
// :TODO: document: Jump to nearest tab right/left. This needs some more research
// * Cursor moved left or right: We should be on the first \t in either direction & need to jump
// * Tabs may expand to start/end of line -- in this case we should move to the next line
//
// Example tab sequence when up/down (tabSize=8)
// Actual: Hello\tWorld!
// Render: Hello\t\t\t\t\t\t\t\tWorld!
// ^-- cursor up from here
//
switch(cursorDir) {
case 'left' :
self.cursorPos.col -= (self.tabWidth - 1);
if(self.cursorPos.col <= 0) {
self.cursorToEndOfPreviousLine();
} else {
self.client.term.write(ansi.left(self.tabWidth - 1));
}
break;
case 'right' :
self.cursorPos.col += (self.tabWidth - 1);
if(self.cursorPos.col >= self.dimens.width) {
self.cursorToStartOfNextLine();
} else {
self.client.term.write(ansi.right(self.tabWidth - 1));
}
break;
case 'up' :
case 'down' :
//
// We're going to move right, but we need to know where we're at in
// in the render buffer expanded tabs
//
// :TODO: This is not right -- we need to move to *nearest*. Research how
// sublime/etc. treat this
var col = self.cursorPos.col;
var prevTabs = 0;
while('\t' === self.getRenderCharAtRowAndColumn(self.cursorPos.row, col--)) {
prevTabs++;
}
//console.log('prevTabs: ' + prevTabs)
var adjust = self.tabWidth - prevTabs;
self.cursorPos.col += adjust;
self.client.term.write(ansi.right(adjust));
break;
}
};
this.adjustColumnToEndOfLine = function() {
var eolColumn = self.getEndOfLineColumn();
if(self.cursorPos.col > eolColumn) {
self.cursorPos.col = eolColumn;
var absPos = self.getAbsolutePosition(self.cursorPos.row, eolColumn);
self.client.term.write(ansi.goto(absPos.row, absPos.col));
}
};
this.cursorUp = function() {
if(self.cursorPos.row > 0) {
self.cursorPos.row--;
self.client.term.write(ansi.up());
self.cursorMoveJumpTab('up');
} else if(self.topLineIndex > 0) {
// :TODO: scroll up if possible to do so
}
// adjust to EOL position if needed
self.adjustColumnToEndOfLine();
};
this.cursorDown = function() {
if(self.getRemainingRowsFromCurrent() > 0) {
self.cursorPos.row++;
self.client.term.write(ansi.down());
self.cursorMoveJumpTab('down');
} else {
// :TODO: scroll if possible
}
// adjust to EOL position if needed
self.adjustColumnToEndOfLine();
};
this.cursorLeft = function() {
if(self.cursorPos.col > 0) {
self.cursorPos.col--;
self.client.term.write(ansi.left());
self.cursorMoveJumpTab('left');
} else {
self.cursorToEndOfPreviousLine();
}
};
this.cursorRight = function() {
var rowVisibleLen = self.renderBuffer[self.cursorPos.row].replace(/\n/g, '').length;
var max = Math.min(self.dimens.width, rowVisibleLen - 1);//selfself.getLineTextLength(self.cursorPos.row) - 1);
console.log('self.dimens.width: ' + self.dimens.width + ' / lineLength: ' + (self.getLineTextLength(self.cursorPos.row) - 1))
if(self.cursorPos.col < max) {
self.cursorPos.col++;
self.client.term.write(ansi.right());
// make tab adjustment if necessary
self.cursorMoveJumpTab('right');
} else {
self.cursorToStartOfNextLine();
}
};
this.cursorToEndOfPreviousLine = function() {
if(self.cursorPos.row > 0) {
self.cursorPos.row--;
self.cursorPos.col = self.getRenderTextLength(self.cursorPos.row) - 1;
self.moveCursorTo(self.cursorPos.row, self.cursorPos.col);
} else {
// can we scroll??!!!
}
};
this.cursorToStartOfNextLine = function() {
if(self.getRemainingRowsFromCurrent() > 0) {
self.cursorPos.row++;
self.cursorPos.col = 0;
self.moveCursorTo(self.cursorPos.row, self.cursorPos.col);
} else {
// :TODO: can we scroll??
}
};
this.getLineIndex = function() {
return self.topLineIndex + self.cursorPos.row;
};
this.insertCharacterAtCurrentPosition = function(c) {
var pos = self.getTextBufferPosition(self.cursorPos.row, self.cursorPos.col);
self.cursorPos.col++;
if(self.cursorPos.col >= self.dimens.width) {
self.cursorToStartOfNextLine();
}
self.client.term.write(c);
self.textBuffer.insert(pos, c);
};
}
require('util').inherits(MultiLineEditTextView, View);
MultiLineEditTextView.prototype.setPosition = function(pos) {
MultiLineEditTextView.super_.prototype.setPosition.call(this, pos);
};
MultiLineEditTextView.prototype.redraw = function() {
MultiLineEditTextView.super_.prototype.redraw.call(this);
this.redrawViewableText();
//this.client.term.write(this.text);
};
MultiLineEditTextView.prototype.setFocus = function(focused) {
MultiLineEditTextView.super_.prototype.setFocus.call(this, focused);
this.moveCursorTo(this.cursorPos.row, this.cursorPos.col);
this.client.term.write(this.getSGR());
};
MultiLineEditTextView.prototype.setText = function(text) {
//this.cursorPos.row = this.position.row + this.dimens.height;
//this.lines = this.wordWrap(text);
if(this.textBuffer.length > 0) { // :TODO: work around GapBuffer bug: if it's already empty this will cause gapEnd to be undefined
this.textBuffer.clear();
}
//this.textBuffer.insertAll(0, text);
text = text.replace(/\b/g, '');
this.textBuffer.insertAll(0, text);
this.updateRenderBuffer();
console.log(this.renderBuffer)
/*
var idx = this.getTextBufferPosition(4, 0);
for(var i = idx; i < idx + 4; ++i) {
console.log(i + ' = "' + this.textBuffer.asArray()[i] + '"');
}
this.cursorPos.row = 15;
this.cursorPos.col = 0;
*/
this.cursorPos.row = 14;
this.cursorPos.col = 0;
};
MultiLineEditTextView.prototype.onKeyPress = function(key, isSpecial) {
if(isSpecial) {
return;
}
assert(1 === key.length);
this.insertCharacterAtCurrentPosition(key);
this.updateRenderBuffer();
// :TODO: is save/restore supported enough? Should we do it ourselves?
this.client.term.write(ansi.savePos());
// :TODO: Just draw from position onward
this.redraw();
this.client.term.write(ansi.restorePos());
MultiLineEditTextView.super_.prototype.onKeyPress.call(this, key, isSpecial);
};
MultiLineEditTextView.prototype.onSpecialKeyPress = function(keyName) {
if(this.isKeyMapped('up', keyName)) {
this.cursorUp();
} else if(this.isKeyMapped('down', keyName)) {
this.cursorDown();
} else if(this.isKeyMapped('left', keyName)) {
this.cursorLeft();
} else if(this.isKeyMapped('right', keyName)) {
this.cursorRight();
}
console.log(
'row=' + this.cursorPos.row + ' / col=' + this.cursorPos.col +
' / abs=' + JSON.stringify(this.getAbsolutePosition()) +
': ' + this.getCharAtCursorPosition() + '( ' + this.getCharAtCursorPosition().charCodeAt(0) + ')')
MultiLineEditTextView.super_.prototype.onSpecialKeyPress.call(this, keyName);
};

View File

@ -39,6 +39,15 @@ var _ = require('lodash');
// * http://www.jbox.dk/downloads/edit.c // * http://www.jbox.dk/downloads/edit.c
// //
// Misc notes
// * See https://github.com/dominictarr/hipster/issues/15 about insert/delete lines
//
// Blessed
// insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height)
// deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height)
// Quick Ansi -- update only what was changed:
// https://github.com/dominictarr/quickansi
// //
// To-Do // To-Do
// //
@ -48,6 +57,8 @@ var _ = require('lodash');
// * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap) // * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap)
// * Word wrapping is a bit broke: cannot fill entire self.dimens.width (off by 1-2) // * Word wrapping is a bit broke: cannot fill entire self.dimens.width (off by 1-2)
// * Contigous words will break word wrapping... is not breaking mid word properly // * Contigous words will break word wrapping... is not breaking mid word properly
// * Fix backspace when col=0 (e.g. bs to prev line)
var SPECIAL_KEY_MAP_DEFAULT = { var SPECIAL_KEY_MAP_DEFAULT = {
'line feed' : [ 'return' ], 'line feed' : [ 'return' ],
@ -67,9 +78,9 @@ var SPECIAL_KEY_MAP_DEFAULT = {
insert : [ 'insert', 'ctrl + v' ], insert : [ 'insert', 'ctrl + v' ],
}; };
exports.MultiLineEditTextView2 = MultiLineEditTextView2; exports.MultiLineEditTextView = MultiLineEditTextView;
function MultiLineEditTextView2(options) { function MultiLineEditTextView(options) {
if(!_.isBoolean(options.acceptsFocus)) { if(!_.isBoolean(options.acceptsFocus)) {
options.acceptsFocus = true; options.acceptsFocus = true;
} }
@ -802,7 +813,7 @@ function MultiLineEditTextView2(options) {
// //
// :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev // :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev
self.keyPressLeft(); // same as hitting left - jump to previous line self.keyPressLeft(); // same as hitting left - jump to previous line
self.keyPressBackspace(); //self.keyPressBackspace();
} }
}; };
@ -973,28 +984,28 @@ function MultiLineEditTextView2(options) {
} }
require('util').inherits(MultiLineEditTextView2, View); require('util').inherits(MultiLineEditTextView, View);
MultiLineEditTextView2.prototype.setWidth = function(width) { MultiLineEditTextView.prototype.setWidth = function(width) {
MultiLineEditTextView2.super_.prototype.setWidth.call(this, width); MultiLineEditTextView.super_.prototype.setWidth.call(this, width);
this.calculateTabStops(); this.calculateTabStops();
}; };
MultiLineEditTextView2.prototype.redraw = function() { MultiLineEditTextView.prototype.redraw = function() {
MultiLineEditTextView2.super_.prototype.redraw.call(this); MultiLineEditTextView.super_.prototype.redraw.call(this);
this.redrawVisibleArea(); this.redrawVisibleArea();
}; };
MultiLineEditTextView2.prototype.setFocus = function(focused) { MultiLineEditTextView.prototype.setFocus = function(focused) {
this.client.term.write(this.getSGRFor('text')); this.client.term.write(this.getSGRFor('text'));
this.moveClientCusorToCursorPos(); this.moveClientCusorToCursorPos();
MultiLineEditTextView2.super_.prototype.setFocus.call(this, focused); MultiLineEditTextView.super_.prototype.setFocus.call(this, focused);
}; };
MultiLineEditTextView2.prototype.setText = function(text) { MultiLineEditTextView.prototype.setText = function(text) {
//this.textLines = [ { text : '' } ]; //this.textLines = [ { text : '' } ];
//this.insertRawText(''); //this.insertRawText('');
//text = "Tab:\r\n\tA\tB\tC\tD\tE\tF\tG\r\n reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally long word!!!"; //text = "Tab:\r\n\tA\tB\tC\tD\tE\tF\tG\r\n reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally long word!!!";
@ -1007,16 +1018,16 @@ MultiLineEditTextView2.prototype.setText = function(text) {
}; };
MultiLineEditTextView2.prototype.getData = function() { MultiLineEditTextView.prototype.getData = function() {
return this.getOutputText(0, this.textLines.length, true); return this.getOutputText(0, this.textLines.length, true);
}; };
MultiLineEditTextView2.prototype.setPropertyValue = function(propName, value) { MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
/* switch(propName) { /* switch(propName) {
case 'text' : this.setText(value); break; case 'text' : this.setText(value); break;
} }
*/ */
MultiLineEditTextView2.super_.prototype.setPropertyValue.call(this, propName, value); MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
}; };
var HANDLED_SPECIAL_KEYS = [ var HANDLED_SPECIAL_KEYS = [
@ -1030,7 +1041,7 @@ var HANDLED_SPECIAL_KEYS = [
'delete line', 'delete line',
]; ];
MultiLineEditTextView2.prototype.onKeyPress = function(ch, key) { MultiLineEditTextView.prototype.onKeyPress = function(ch, key) {
var self = this; var self = this;
var handled; var handled;
@ -1048,6 +1059,6 @@ MultiLineEditTextView2.prototype.onKeyPress = function(ch, key) {
} }
if(!handled) { if(!handled) {
MultiLineEditTextView2.super_.prototype.onKeyPress.call(this, ch, key); MultiLineEditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} }
}; };