diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index bff03af5..d52e7b80 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -11,7 +11,7 @@ var ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; 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_view2.js').MultiLineEditTextView2; +var MultiLineEditTextView = require('./multi_line_edit_text_view2.js').MultiLineEditTextView; var Config = require('./config.js').config; var ansi = require('./ansi_term.js'); diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js deleted file mode 100644 index cbb0d9af..00000000 --- a/core/multi_line_edit_text_view.js +++ /dev/null @@ -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); -}; diff --git a/core/multi_line_edit_text_view2.js b/core/multi_line_edit_text_view2.js index 557b9de5..f6f8fe82 100644 --- a/core/multi_line_edit_text_view2.js +++ b/core/multi_line_edit_text_view2.js @@ -39,6 +39,15 @@ var _ = require('lodash'); // * 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 // @@ -48,6 +57,8 @@ var _ = require('lodash'); // * 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) // * 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 = { 'line feed' : [ 'return' ], @@ -67,9 +78,9 @@ var SPECIAL_KEY_MAP_DEFAULT = { insert : [ 'insert', 'ctrl + v' ], }; -exports.MultiLineEditTextView2 = MultiLineEditTextView2; +exports.MultiLineEditTextView = MultiLineEditTextView; -function MultiLineEditTextView2(options) { +function MultiLineEditTextView(options) { if(!_.isBoolean(options.acceptsFocus)) { 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 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) { - MultiLineEditTextView2.super_.prototype.setWidth.call(this, width); +MultiLineEditTextView.prototype.setWidth = function(width) { + MultiLineEditTextView.super_.prototype.setWidth.call(this, width); this.calculateTabStops(); }; -MultiLineEditTextView2.prototype.redraw = function() { - MultiLineEditTextView2.super_.prototype.redraw.call(this); +MultiLineEditTextView.prototype.redraw = function() { + MultiLineEditTextView.super_.prototype.redraw.call(this); this.redrawVisibleArea(); }; -MultiLineEditTextView2.prototype.setFocus = function(focused) { +MultiLineEditTextView.prototype.setFocus = function(focused) { this.client.term.write(this.getSGRFor('text')); 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.insertRawText(''); //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); }; -MultiLineEditTextView2.prototype.setPropertyValue = function(propName, value) { +MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { /* switch(propName) { 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 = [ @@ -1030,7 +1041,7 @@ var HANDLED_SPECIAL_KEYS = [ 'delete line', ]; -MultiLineEditTextView2.prototype.onKeyPress = function(ch, key) { +MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { var self = this; var handled; @@ -1048,6 +1059,6 @@ MultiLineEditTextView2.prototype.onKeyPress = function(ch, key) { } if(!handled) { - MultiLineEditTextView2.super_.prototype.onKeyPress.call(this, ch, key); + MultiLineEditTextView.super_.prototype.onKeyPress.call(this, ch, key); } };