From 397239705cdbb4e95b680b43b6fe3a477fc6dda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Thu, 7 Apr 2016 06:10:17 +0800 Subject: [PATCH 1/7] Indent based on last indent --- botanjs/src/Components/Vim/Actions/INSERT.js | 99 +++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/botanjs/src/Components/Vim/Actions/INSERT.js b/botanjs/src/Components/Vim/Actions/INSERT.js index ed9ebd8..5a4b30a 100644 --- a/botanjs/src/Components/Vim/Actions/INSERT.js +++ b/botanjs/src/Components/Vim/Actions/INSERT.js @@ -10,6 +10,11 @@ var Mesg = __import( "Components.Vim.Message" ); + // Phantom indent + var IN_START = 0; + var IN_END = 1; + var IN_DEL = 2; + var Translate = function( c ) { switch( c ) @@ -47,6 +52,10 @@ INSERT.prototype.dispose = function() { + if( this.__cancelIndent() ) + { + this.__cursor.feeder.pan(); + } this.__msg = ""; this.__rec( "", true ); this.__cursor.moveX( -1 ); @@ -105,8 +114,9 @@ var feeder = cur.feeder; // Backspace - if( e.kMap( "BS" ) ) + if( e.kMap( "BS" ) || e.kMap( "S-BS" ) ) { + this.__realizeIndent(); var oY = feeder.panY + cur.Y; if( cur.X == 0 && feeder.panY == 0 && cur.Y == 0 ) return; @@ -128,8 +138,9 @@ if( 0 < this.__insertLen ) this.__insertLen --; this.__punch --; } - else if( e.kMap( "Del" ) ) + else if( e.kMap( "Del" ) || e.kMap( "S-Del" ) ) { + this.__realizeIndent(); var f = cur.aPos; this.__contentUndo += feeder.content.substr( f, 1 ); @@ -175,9 +186,11 @@ feeder.pan(); cur.moveY( 1 ); cur.lineStart(); + this.__autoIndent( e ); } else { + this.__realizeIndent(); feeder.pan(); cur.moveX( 1, false, true ); } @@ -187,6 +200,88 @@ this.__rec( inputChar ); }; + INSERT.prototype.__realizeIndent = function() + { + var ind = this.__phantomIndent; + if( !this.__phantomIndent ) return; + l = ind[ IN_END ]; + for( var i = ind[ IN_START ]; i < l; i ++ ) + { + this.__rec( this.__cursor.feeder.content[ i ] ); + } + this.__contentUndo = ind[ IN_DEL ] + this.__contentUndo; + this.__phantomIndent = null; + }; + + INSERT.prototype.__autoIndent = function( e ) + { + var carried = this.__cancelIndent(); + + var cur = this.__cursor; + var feeder = cur.feeder; + + var f = cur.aPos; + + // Get the last indent + var i = feeder.content.lastIndexOf( "\n", f - 2 ); + var line = feeder.content.substring( i + 1, f - 1 ) || carried; + + // Find Last indent + while( line == "" && 0 < i ) + { + var j = i; + i = feeder.content.lastIndexOf( "\n", j - 2 ); + line = feeder.content.substring( i + 1, j - 1 ); + } + + var inDel = ""; + // Indent removed + for( var ir = f; "\t ".indexOf( feeder.content[ ir ] ) != -1; ir ++ ) + { + inDel += feeder.content[ ir ]; + } + + // Copy the indentation + for( i = 0; "\t ".indexOf( line[i] ) != -1; i ++ ); + + if( line ) + { + feeder.content = + feeder.content.substring( 0, f ) + + line.substr( 0, i ) + + feeder.content.substring( ir ); + + feeder.softReset(); + feeder.pan(); + cur.moveX( i, false, true ); + + var a = []; + a[ IN_START ] = f; + a[ IN_END ] = f + i; + a[ IN_DEL ] = inDel; + + this.__phantomIndent = a; + } + }; + + INSERT.prototype.__cancelIndent = function() + { + var ind = this.__phantomIndent; + if( !ind ) return ""; + + var cur = this.__cursor; + var feeder = cur.feeder; + + var canceled = feeder.content.substring( ind[ IN_START ], ind[ IN_END ] ); + feeder.content = + feeder.content.substring( 0, ind[ IN_START ] ) + + feeder.content.substring( ind[ IN_END ] ); + + this.__phantomIndent = null; + + return canceled; + } + INSERT.prototype.getMessage = function() { return this.__msg; From 6f7a1cc92ce8fffc24fac48b2e8011345c48a49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Thu, 7 Apr 2016 23:11:23 +0800 Subject: [PATCH 2/7] Various bug fixes, rename InputEvent to ActionEvent --- botanjs/src/Components/Vim/Actions/FIND.js | 6 +- .../src/Components/Vim/Actions/REGISTERS.js | 2 +- botanjs/src/Components/Vim/Controls.js | 98 +++++++++++++++---- botanjs/src/Components/Vim/State/Registers.js | 16 ++- botanjs/src/Components/Vim/VimArea.js | 4 +- .../Components.Vim.Controls.ActionEvent.js | 23 +++++ .../Components.Vim.Controls.InputEvent.js | 21 ---- 7 files changed, 122 insertions(+), 48 deletions(-) create mode 100644 botanjs/src/externs/Components.Vim.Controls.ActionEvent.js delete mode 100644 botanjs/src/externs/Components.Vim.Controls.InputEvent.js diff --git a/botanjs/src/Components/Vim/Actions/FIND.js b/botanjs/src/Components/Vim/Actions/FIND.js index 461ddc4..f93aeaa 100644 --- a/botanjs/src/Components/Vim/Actions/FIND.js +++ b/botanjs/src/Components/Vim/Actions/FIND.js @@ -128,6 +128,8 @@ LoopGuard = r.index; } + this.__msg = PATTERN.join( "" ) + if( e.kMap( "N" ) ) { Hit = PrevStack[ PrevStack.length - 2 ]; @@ -144,10 +146,6 @@ Hit = FirstHit; this.__msg = Mesg( "SEARCH_HIT_BOTTOM" ); } - else - { - this.__msg = PATTERN.join( "" ) - } if( Hit == undefined ) { diff --git a/botanjs/src/Components/Vim/Actions/REGISTERS.js b/botanjs/src/Components/Vim/Actions/REGISTERS.js index 48f19d0..c94488d 100644 --- a/botanjs/src/Components/Vim/Actions/REGISTERS.js +++ b/botanjs/src/Components/Vim/Actions/REGISTERS.js @@ -37,7 +37,7 @@ var msg = ":register"; msg += "\n" + Mesg( "REGISTERS" ); - var regs = "\"0123456789-.:%/="; + var regs = "\"0123456789abcdefghijklmnopqrstuvwxyz-.:%/="; for( var i = 0, j = regs[ i ]; j != undefined; i ++, j = regs[ i ] ) { var r = reg.get( j ); diff --git a/botanjs/src/Components/Vim/Controls.js b/botanjs/src/Components/Vim/Controls.js index aace42f..b699ca0 100644 --- a/botanjs/src/Components/Vim/Controls.js +++ b/botanjs/src/Components/Vim/Controls.js @@ -47,6 +47,8 @@ var COMMA = 188; var FULLSTOP = 190; var SLASH = 191; var BACK_SLASH = 220; + var QUOTE = 222; + var ANY_KEY = -1; var __maps = {}; @@ -177,9 +179,8 @@ { if( compReg.i == keys.length ) { - compReg.handler( e ); this.__compositeReg = null; - this.__cMovement = false; + compReg.handler( e ); } return true; @@ -188,7 +189,6 @@ if( this.__compositeReg ) beep(); this.__compositeReg = null; - this.__cMovement = false; return false; }; @@ -219,13 +219,13 @@ case SHIFT + O: // new line before insert ccur.lineStart(); ccur.openAction( "INSERT" ); - ccur.action.handler( new InputEvent( e.sender, "Enter" ) ); + ccur.action.handler( new ActionEvent( e.sender, "Enter" ) ); ccur.moveY( -1 ); break; case O: // new line insert ccur.lineEnd( true ); ccur.openAction( "INSERT" ); - ccur.action.handler( new InputEvent( e.sender, "Enter" ) ); + ccur.action.handler( new ActionEvent( e.sender, "Enter" ) ); break; case U: // Undo @@ -320,6 +320,60 @@ return false; }; + Controls.prototype.__modCommand = function( e ) + { + var catchCommand = false; + if( this.__mod ) + { + e.preventDefault(); + this.__composite( e ); + return catchCommand; + } + + var _self = this; + var mod = true; + + var cur = this.__cursor; + switch( e.keyCode ) + { + case SHIFT + QUOTE: + this.__composite( e, function( e2 ) { + e2.target.registers.select( e2.key ); + _self.__mod = false; + }, ANY_KEY ); + break; + case _0: case _1: case _2: case _3: case _4: + case _5: case _6: case _7: case _8: case _9: + + var Count = e.key; + var recurNum = function( e2 ) { + switch( e2.keyCode ) + { + case _0: case _1: case _2: + case _3: case _4: case _5: + case _6: case _7: case _8: case _9: + Count += e2.key; + _self.__composite( e2, recurNum, ANY_KEY ); + return; + } + + debug.Info( "Count is: " + Count ); + catchCommand = true; + _self.__mod = false; + }; + + this.__composite( e, recurNum, ANY_KEY ); + break; + default: + mod = false; + } + + this.__mod = mod; + if( mod ) e.preventDefault(); + + return mod; + }; + Controls.prototype.__cursorCommand = function( e ) { var kCode = e.keyCode; @@ -329,6 +383,7 @@ if( !e.ModKeys ) { this.__composite( e ); + this.__cMovement = false; return true; } } @@ -520,7 +575,7 @@ /** * sender @param {Components.Vim.VimArea} - * e @param {Components.Vim.Controls.InputEvent} + * e @param {Components.Vim.Controls.ActionEvent} * */ Controls.prototype.handler = function( sender, e ) { @@ -567,6 +622,8 @@ if( e.canceled ) return; } + if( this.__modCommand( e ) ) return; + var cfeeder = this.__cfeeder; var ccur = this.__ccur; @@ -607,7 +664,7 @@ if( this.__actionCommand( e ) ) return; }; - var InputEvent = function( sender, e ) + var ActionEvent = function( sender, e ) { this.__target = sender; this.__canceled = false; @@ -641,19 +698,20 @@ this.__key = e.key; } + this.__count = 1; this.__range = null; }; - InputEvent.prototype.cancel = function() { this.__canceled = true; }; + ActionEvent.prototype.cancel = function() { this.__canceled = true; }; - __readOnly( InputEvent.prototype, "target", function() { return this.__target; } ); - __readOnly( InputEvent.prototype, "key", function() { return this.__key; } ); - __readOnly( InputEvent.prototype, "keyCode", function() { return this.__kCode; } ); - __readOnly( InputEvent.prototype, "ModKeys", function() { return this.__modKeys; } ); - __readOnly( InputEvent.prototype, "Escape", function() { return this.__escape; } ); - __readOnly( InputEvent.prototype, "canceled", function() { return this.__canceled; } ); + __readOnly( ActionEvent.prototype, "target", function() { return this.__target; } ); + __readOnly( ActionEvent.prototype, "key", function() { return this.__key; } ); + __readOnly( ActionEvent.prototype, "keyCode", function() { return this.__kCode; } ); + __readOnly( ActionEvent.prototype, "ModKeys", function() { return this.__modKeys; } ); + __readOnly( ActionEvent.prototype, "Escape", function() { return this.__escape; } ); + __readOnly( ActionEvent.prototype, "canceled", function() { return this.__canceled; } ); - __readOnly( InputEvent.prototype, "range", function() { + __readOnly( ActionEvent.prototype, "range", function() { /** @type {Components.Vim.Syntax.TokenMatch} */ var r = this.__range; @@ -666,16 +724,20 @@ return r; } ); - InputEvent.prototype.kMap = function( map ) + __readOnly( ActionEvent.prototype, "count", function() { + return this.__count; + } ); + + ActionEvent.prototype.kMap = function( map ) { return this.__kCode == Map( map ); }; - InputEvent.prototype.preventDefault = function() + ActionEvent.prototype.preventDefault = function() { if( this.__e ) this.__e.preventDefault(); }; ns[ NS_EXPORT ]( EX_CLASS, "Controls", Controls ); - ns[ NS_EXPORT ]( EX_CLASS, "InputEvent", InputEvent ); + ns[ NS_EXPORT ]( EX_CLASS, "ActionEvent", ActionEvent ); })(); diff --git a/botanjs/src/Components/Vim/State/Registers.js b/botanjs/src/Components/Vim/State/Registers.js index ec6335b..ef9cafa 100644 --- a/botanjs/src/Components/Vim/State/Registers.js +++ b/botanjs/src/Components/Vim/State/Registers.js @@ -15,6 +15,9 @@ */ var ns = __namespace( "Components.Vim.State" ); + /** @type {System.Debug} */ + var debug = __import( "System.Debug" ); + var Register = function( str, n ) { this.__str = str + ""; @@ -43,7 +46,8 @@ { var reg = new Register( str, newLine ); this.__unnamed( reg ); - this.__registers[ 0 ] = reg; + this.__registers[ this.__selRegister || 0 ] = reg; + this.__selRegister = false; }; Registers.prototype.change = function( str, newLine ) @@ -60,16 +64,24 @@ } r[ 1 ] = reg; + this.__selRegister = false; }; Registers.prototype.get = function( r ) { // 0 is one of the registers - if( !r && r !== 0 ) r = "\""; + if( !r && r !== 0 ) r = this.__selRegister || "\""; + this.__selRegister = false; return this.__registers[ r ]; }; + Registers.prototype.select = function( r ) + { + debug.Info( "Selecting Register: " + r ); + this.__selRegister = r; + }; + ns[ NS_EXPORT ]( EX_CLASS, "Registers", Registers ); })(); diff --git a/botanjs/src/Components/Vim/VimArea.js b/botanjs/src/Components/Vim/VimArea.js index 563cd33..433be5a 100644 --- a/botanjs/src/Components/Vim/VimArea.js +++ b/botanjs/src/Components/Vim/VimArea.js @@ -23,7 +23,7 @@ var StatusBar = ns[ NS_INVOKE ]( "StatusBar" ); var VimControls = ns[ NS_INVOKE ]( "Controls" ); - var InputEvent = ns[ NS_INVOKE ]( "InputEvent" ); + var ActionEvent = ns[ NS_INVOKE ]( "ActionEvent" ); var mesg = ns[ NS_INVOKE ]( "Message" ); var Insts = []; @@ -38,7 +38,7 @@ if ( e.keyCode ) code = e.keyCode; else if ( e.which ) code = e.which; - handler( sender, new InputEvent( sender, e ) ); + handler( sender, new ActionEvent( sender, e ) ); }; }; diff --git a/botanjs/src/externs/Components.Vim.Controls.ActionEvent.js b/botanjs/src/externs/Components.Vim.Controls.ActionEvent.js new file mode 100644 index 0000000..ca9e1e5 --- /dev/null +++ b/botanjs/src/externs/Components.Vim.Controls.ActionEvent.js @@ -0,0 +1,23 @@ +/** @constructor */ +Components.Vim.Controls.ActionEvent = function(){}; + +/** @type {Components.Vim.VimArea} */ +Components.Vim.Controls.ActionEvent.target; +/** @type {Components.Vim.Syntax.TokenMatch} */ +Components.Vim.Controls.ActionEvent.range; +/** @type {Components.Vim.Syntax.Number} */ +Components.Vim.Controls.ActionEvent.count; +/** @type String */ +Components.Vim.Controls.ActionEvent.key; +/** @type Boolean */ +Components.Vim.Controls.ActionEvent.ModKeys; +/** @type Boolean */ +Components.Vim.Controls.ActionEvent.Escape; +/** @type Boolean */ +Components.Vim.Controls.ActionEvent.canceled; +/** @type Number */ +Components.Vim.Controls.ActionEvent.keyCode; +/** @type Function */ +Components.Vim.Controls.ActionEvent.kMap; +/** @type Function */ +Components.Vim.Controls.ActionEvent.cancel; diff --git a/botanjs/src/externs/Components.Vim.Controls.InputEvent.js b/botanjs/src/externs/Components.Vim.Controls.InputEvent.js deleted file mode 100644 index 6678a27..0000000 --- a/botanjs/src/externs/Components.Vim.Controls.InputEvent.js +++ /dev/null @@ -1,21 +0,0 @@ -/** @constructor */ -Components.Vim.Controls.InputEvent = function(){}; - -/** @type {Components.Vim.VimArea} */ -Components.Vim.Controls.InputEvent.target; -/** @type {Components.Vim.Syntax.TokenMatch} */ -Components.Vim.Controls.InputEvent.range; -/** @type String */ -Components.Vim.Controls.InputEvent.key; -/** @type Boolean */ -Components.Vim.Controls.InputEvent.ModKeys; -/** @type Boolean */ -Components.Vim.Controls.InputEvent.Escape; -/** @type Boolean */ -Components.Vim.Controls.InputEvent.canceled; -/** @type Number */ -Components.Vim.Controls.InputEvent.keyCode; -/** @type Function */ -Components.Vim.Controls.InputEvent.kMap; -/** @type Function */ -Components.Vim.Controls.InputEvent.cancel; From a3fa2e82d7557afe8e61cced4f038cbaf8641791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Thu, 7 Apr 2016 23:56:09 +0800 Subject: [PATCH 3/7] PUT can now select registers --- botanjs/src/Components/Vim/Actions/PUT.js | 23 ++++++++++----- botanjs/src/Components/Vim/Controls.js | 35 ++++++++++++++--------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/botanjs/src/Components/Vim/Actions/PUT.js b/botanjs/src/Components/Vim/Actions/PUT.js index 3152444..52a6891 100644 --- a/botanjs/src/Components/Vim/Actions/PUT.js +++ b/botanjs/src/Components/Vim/Actions/PUT.js @@ -29,20 +29,29 @@ { e.preventDefault(); - // TODO: Get the input for determinating registers - var inputStack = false; - - var cput = this.__cursor.Vim.registers.get( inputStack ); + var cput = this.__cursor.Vim.registers.get(); if( !cput ) return true; - var clen = cput.length; - var nLines = occurence( cput, "\n" ); - var cur = this.__cursor; var feeder = cur.feeder; var newLine = cput.newLine; + if( 1 < e.count ) + { + var oput = cput; + for( var i = 1; i < e.count; i ++ ) + { + oput += cput; + } + + cput = oput; + oput = null; + } + + var nLines = occurence( cput, "\n" ); + var clen = cput.length; + // Compensation var c = e.kMap( "P" ) ? 0 : -1; diff --git a/botanjs/src/Components/Vim/Controls.js b/botanjs/src/Components/Vim/Controls.js index b699ca0..fac0875 100644 --- a/botanjs/src/Components/Vim/Controls.js +++ b/botanjs/src/Components/Vim/Controls.js @@ -322,12 +322,11 @@ Controls.prototype.__modCommand = function( e ) { - var catchCommand = false; if( this.__mod ) { e.preventDefault(); this.__composite( e ); - return catchCommand; + return; } var _self = this; @@ -339,6 +338,8 @@ case SHIFT + QUOTE: this.__composite( e, function( e2 ) { e2.target.registers.select( e2.key ); + e2.cancel(); + _self.__mod = false; }, ANY_KEY ); break; @@ -346,19 +347,21 @@ case _5: case _6: case _7: case _8: case _9: var Count = e.key; - var recurNum = function( e2 ) { - switch( e2.keyCode ) + var recurNum = function( e ) + { + switch( e.keyCode ) { case _0: case _1: case _2: case _3: case _4: case _5: case _6: case _7: case _8: case _9: - Count += e2.key; - _self.__composite( e2, recurNum, ANY_KEY ); + Count += e.key; + _self.__composite( e, recurNum, ANY_KEY ); + e.cancel(); return; } + e.__count = Number( Count ); debug.Info( "Count is: " + Count ); - catchCommand = true; _self.__mod = false; }; @@ -369,9 +372,10 @@ } this.__mod = mod; - if( mod ) e.preventDefault(); - - return mod; + if( mod ) + { + e.cancel(); + } }; Controls.prototype.__cursorCommand = function( e ) @@ -622,7 +626,8 @@ if( e.canceled ) return; } - if( this.__modCommand( e ) ) return; + this.__modCommand( e ); + if( e.canceled ) return; var cfeeder = this.__cfeeder; var ccur = this.__ccur; @@ -702,8 +707,6 @@ this.__range = null; }; - ActionEvent.prototype.cancel = function() { this.__canceled = true; }; - __readOnly( ActionEvent.prototype, "target", function() { return this.__target; } ); __readOnly( ActionEvent.prototype, "key", function() { return this.__key; } ); __readOnly( ActionEvent.prototype, "keyCode", function() { return this.__kCode; } ); @@ -733,6 +736,12 @@ return this.__kCode == Map( map ); }; + ActionEvent.prototype.cancel = function() + { + this.preventDefault(); + this.__canceled = true; + }; + ActionEvent.prototype.preventDefault = function() { if( this.__e ) this.__e.preventDefault(); From 78cb3d3610da35fc165ee3e84a12b03aa10d0e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sat, 16 Apr 2016 00:06:59 +0800 Subject: [PATCH 4/7] Partial indent command --- botanjs/src/Components/Vim/Actions/INSERT.js | 6 + botanjs/src/Components/Vim/Actions/REDO.js | 4 +- botanjs/src/Components/Vim/Actions/REPLACE.js | 7 +- .../src/Components/Vim/Actions/SHIFT_LINES.js | 242 ++++++++++++++++++ botanjs/src/Components/Vim/Actions/UNDO.js | 4 +- botanjs/src/Components/Vim/Actions/YANK.js | 8 - botanjs/src/Components/Vim/Controls.js | 31 ++- botanjs/src/Components/Vim/Cursor.js | 4 +- botanjs/src/Components/Vim/_this.js | 2 + 9 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 botanjs/src/Components/Vim/Actions/SHIFT_LINES.js diff --git a/botanjs/src/Components/Vim/Actions/INSERT.js b/botanjs/src/Components/Vim/Actions/INSERT.js index 5a4b30a..c35c167 100644 --- a/botanjs/src/Components/Vim/Actions/INSERT.js +++ b/botanjs/src/Components/Vim/Actions/INSERT.js @@ -204,6 +204,9 @@ { var ind = this.__phantomIndent; if( !this.__phantomIndent ) return; + + debug.Info( "Realize Indentation: " + ind ); + l = ind[ IN_END ]; for( var i = ind[ IN_START ]; i < l; i ++ ) { @@ -261,6 +264,7 @@ a[ IN_DEL ] = inDel; this.__phantomIndent = a; + debug.Info( "Phantom indent: " + a ); } }; @@ -269,6 +273,8 @@ var ind = this.__phantomIndent; if( !ind ) return ""; + debug.Info( "Erase phantom indent: " + ind ); + var cur = this.__cursor; var feeder = cur.feeder; diff --git a/botanjs/src/Components/Vim/Actions/REDO.js b/botanjs/src/Components/Vim/Actions/REDO.js index 43ca88b..94485d2 100644 --- a/botanjs/src/Components/Vim/Actions/REDO.js +++ b/botanjs/src/Components/Vim/Actions/REDO.js @@ -24,8 +24,10 @@ var stack = this.__cursor.rec.redo(); if( stack ) { + this.__cursor.suppressEvent(); stack.play(); - this.__message = "<>; before #" + stack.id + " " + stack.time; + this.__cursor.unsuppressEvent(); + this.__message = Mesg( "NCHANGES", "", stack.id, stack.time ); } else { diff --git a/botanjs/src/Components/Vim/Actions/REPLACE.js b/botanjs/src/Components/Vim/Actions/REPLACE.js index 1acf895..0161595 100644 --- a/botanjs/src/Components/Vim/Actions/REPLACE.js +++ b/botanjs/src/Components/Vim/Actions/REPLACE.js @@ -92,7 +92,7 @@ this.__cursor.unsuppressEvent(); }; - REPLACE.prototype.handler = function( e, p, range ) + REPLACE.prototype.handler = function( e, p ) { e.preventDefault(); @@ -143,13 +143,16 @@ var content = feeder.content.slice( 0, -1 ) .replace( search, this.__replCallback ) + "\n"; - if( !this.__replacedGroups.length ) + var numSubs = this.__replacedGroups.length; + if( !numSubs ) { this.__msg = VimError( "E486", spattern.join( "" ) ); } feeder.content = content; + this.__msg = Mesg( "REPLACE", numSubs, "" ); + // Record this step for UNDO / REDO this.__rec(); diff --git a/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js b/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js new file mode 100644 index 0000000..a539c17 --- /dev/null +++ b/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js @@ -0,0 +1,242 @@ +(function(){ + var ns = __namespace( "Components.Vim.Actions" ); + + /** @type {System.Debug} */ + var debug = __import( "System.Debug" ); + var beep = __import( "Components.Vim.Beep" ); + + /** @type {Components.Vim.State.Stack} */ + var Stack = __import( "Components.Vim.State.Stack" ); + + var VimError = __import( "Components.Vim.Error" ); + var Mesg = __import( "Components.Vim.Message" ); + + var occurence = __import( "System.utils.Perf.CountSubstr" ); + + var REPL_BEFORE = 0; + var REPL_OFFSET = 1; + var REPL_LENGTH = 2; + + /** @type {Components.Vim.IAction} + * Cursor @param {Components.Vim.Cursor} + * e @param {Components.Vim.ActionEvent} + **/ + var SHIFT_LINES = function( Cursor, e ) + { + /** @type {Components.Vim.Cursor} */ + this.__cursor = Cursor; + this.__startX = Cursor.aPos; + this.__msg = ""; + + this.__slineNum = Cursor.getLine().lineNum; + + this.__lines = e.count; + debug.Info( "Open shift: " + this.__lines + " line(s) below the cursor" ); + + this.__direction = e.kMap( ">" ) ? 1 : -1; + debug.Info( "Direction is: " + ( this.__direction == 1 ? ">" : "<" ) ); + + Cursor.suppressEvent(); + }; + + SHIFT_LINES.prototype.allowMovement = true; + + SHIFT_LINES.prototype.dispose = function() + { + this.__cursor.unsuppressEvent(); + }; + + SHIFT_LINES.prototype.handler = function( e, sp ) + { + e.preventDefault(); + + if( e.ModKeys || e.kMap( "i" ) ) return; + + var cur = this.__cursor; + var feeder = cur.feeder; + + var Triggered = false; + var dir = this.__direction; + + var start = this.__slineNum; + var nline = this.__lines; + + if( 1 < e.count ) + { + nline += e.count; + } + + var end = start; + + var shiftCount = 1; + if( sp == undefined ) + { + Triggered = true; + + sp = this.__startX; + + var currAp = cur.aPos; + + if( this.__startX != currAp ) + { + if( e.kMap( "h" ) || e.kMap( "l" ) ){} + else if( e.kMap( "j" ) ) + { + end = start + nline; + } + else if( e.kMap( "k" ) ) + { + start -= nline; + } + else // TODO: Dectect movement line count + { + } + } + else + { + if( !( ( 0 < dir && ( e.kMap( ">" ) || e.kMap( "l" ) ) ) + || ( dir < 0 && ( e.kMap( "<" ) || e.kMap( "h" ) ) ) + ) ) + { + beep(); + return true; + } + } + } + + // last "\n" padding + var c = feeder.content.slice( 0, -1 ); + + var indents = c.match( /^[\t ]+/gm ); + var indentChar = "\t"; + var tabwidth = feeder.firstBuffer.tabWidth; + + if( indents ) + { + var l = indents.length - 1; + + if( 1 < l ) + { + debug.Info( "Guessing the tabstop:" ); + var tabOccr = 0; + var spOccr = 0; + + // Guess indent + var tabStat = {}; + + for( var i = 0; i < l; i ++ ) + { + var ind = indents[ i ]; + var indNext = indents[ i + 1 ]; + tabOccr += occurence( ind, "\t" ); + spOccr += occurence( ind, " " ); + var d = indNext.length - ind.length; + if( d == 0 ) continue; + + d = d < 0 ? -d : d; + + if( !tabStat[ d ] ) tabStat[ d ] = 0; + + tabStat[ d ] ++; + } + + var upperDiff = 0; + var indentCLen = 0; + for( var i in tabStat ) + { + var p = tabStat[ i ]; + if( upperDiff < p ) + { + upperDiff = p; + indentCLen = i; + } + } + + spOccr /= indentCLen; + + if( tabOccr < spOccr ) + { + indentChar = ""; + for( var i = 0; i < indentCLen; i ++ ) indentChar += " "; + } + + tabwidth = indentCLen; + + debug.Info( "\tTab count: " + tabOccr ); + debug.Info( "\tSpace count: " + spOccr ); + debug.Info( "\ti.e. indent using " + JSON.stringify( indentChar ) ); + } + else + { + debug.Info( "Not enough tabs to determine the tabstop, using default" ); + } + } + + debug.Info( "Start: " + start, "End: " + end ); + var rBlock = ""; + + var started = false; + var indentTimes = 1; + + feeder.content = ""; + nline = 0; + + for( var i = 0, j = 0; 0 <= i; i = c.indexOf( "\n", i ), j ++ ) + { + i ++; + + if( j < start ) continue; + else if( !started ) + { + started = true; + feeder.content = c.substring( 0, i - 1 ); + } + + if( end < j ) break; + + var line = c.substring( 1 < i ? i : i - 1, c.indexOf( "\n", i ) ); + + if( 1 < i ) feeder.content += "\n"; + + if( line !== "" ) + { + if( 0 < dir ) + { + feeder.content += indentChar + line; + } + else + { + for( var si = 0, sj = 1; si < indentTimes; si ++ ) + { + var startC = line[ si ]; + if( startC == " " ) + { + for( ; sj < tabwidth; sj ++ ) + { + if( !~"\t ".indexOf( line[ si + sj ] ) ) break; + } + } + else if( startC != "\t" ) break; + } + + feeder.content += line.substring( si + sj - 1 ); + } + nline ++; + } + } + + feeder.content += "\n" + c.substring( i ) + "\n"; + feeder.pan(); + + this.__msg = Mesg( "LINES_SHIFTED", nline, dir < 0 ? "<" : ">", 1 ); + + return Triggered; + }; + + SHIFT_LINES.prototype.getMessage = function() + { + return this.__msg; + }; + + ns[ NS_EXPORT ]( EX_CLASS, "SHIFT_LINES", SHIFT_LINES ); +})(); diff --git a/botanjs/src/Components/Vim/Actions/UNDO.js b/botanjs/src/Components/Vim/Actions/UNDO.js index 6f81f52..86db7cf 100644 --- a/botanjs/src/Components/Vim/Actions/UNDO.js +++ b/botanjs/src/Components/Vim/Actions/UNDO.js @@ -24,8 +24,10 @@ var stack = this.__cursor.rec.undo(); if( stack ) { + this.__cursor.suppressEvent(); stack.play(); - this.__message = "<>; before #" + stack.id + " " + stack.time; + this.__cursor.unsuppressEvent(); + this.__message = Mesg( "NCHANGES", "", stack.id, stack.time ); } else { diff --git a/botanjs/src/Components/Vim/Actions/YANK.js b/botanjs/src/Components/Vim/Actions/YANK.js index 44e3c27..9c9a37a 100644 --- a/botanjs/src/Components/Vim/Actions/YANK.js +++ b/botanjs/src/Components/Vim/Actions/YANK.js @@ -50,22 +50,18 @@ var currAp = cur.aPos; if( this.__startX != currAp ) { - // Remove to start if( e.kMap( "^" ) ) { sp --; } - // Remove char in cursor else if( e.kMap( "l" ) ) { cur.moveX( -1 ); } - // Remove char before cursor else if( e.kMap( "h" ) ) { sp = currAp; } - // Remove the current and the following line else if( e.kMap( "j" ) ) { newLine = true; @@ -75,7 +71,6 @@ cur.lineStart(); this.__startX = cur.aPos; } - // Remove the current and the preceding line else if( e.kMap( "k" ) ) { newLine = true; @@ -94,7 +89,6 @@ cur.moveTo( this.__startX ); } } - // Remove the current line else { if( e.kMap( "y" ) ) @@ -111,8 +105,6 @@ } else if( e.kMap( "^" ) ) { - // Do nothing as nothing can be removed - // since there is no successful movement return true; } // this is the same as kMap( "h" ) above diff --git a/botanjs/src/Components/Vim/Controls.js b/botanjs/src/Components/Vim/Controls.js index fac0875..c34a4d9 100644 --- a/botanjs/src/Components/Vim/Controls.js +++ b/botanjs/src/Components/Vim/Controls.js @@ -205,7 +205,7 @@ case A: // Append ccur.moveX( 1, true, true ); case I: // Insert - ccur.openAction( "INSERT" ); + ccur.openAction( "INSERT", e ); break; case S: // Delete Char and start insert @@ -213,18 +213,18 @@ { ccur.openRunAction( "DELETE", e, ccur.aPos ); } - ccur.openAction( "INSERT" ); + ccur.openAction( "INSERT", e ); break; case SHIFT + O: // new line before insert ccur.lineStart(); - ccur.openAction( "INSERT" ); + ccur.openAction( "INSERT", e ); ccur.action.handler( new ActionEvent( e.sender, "Enter" ) ); ccur.moveY( -1 ); break; case O: // new line insert ccur.lineEnd( true ); - ccur.openAction( "INSERT" ); + ccur.openAction( "INSERT", e ); ccur.action.handler( new ActionEvent( e.sender, "Enter" ) ); break; @@ -236,10 +236,10 @@ break; case D: // Del with motion - ccur.openAction( "DELETE" ); + ccur.openAction( "DELETE", e ); break; case Y: // Yank with motion - ccur.openAction( "YANK" ); + ccur.openAction( "YANK", e ); break; case P: // Put @@ -272,7 +272,7 @@ case V: // Visual case SHIFT + V: // Visual line - ccur.openAction( "VISUAL" ); + ccur.openAction( "VISUAL", e ); ccur.action.handler( e ); break; @@ -281,6 +281,11 @@ this.__divedCCmd.handler( e ); break; + case SHIFT + COMMA: // < + case SHIFT + FULLSTOP: // > + ccur.openAction( "SHIFT_LINES", e ); + break; + case F1: // F1, help break; default: @@ -349,15 +354,21 @@ var Count = e.key; var recurNum = function( e ) { + var intercept = e.ModKeys; switch( e.keyCode ) { case _0: case _1: case _2: case _3: case _4: case _5: case _6: case _7: case _8: case _9: Count += e.key; - _self.__composite( e, recurNum, ANY_KEY ); - e.cancel(); - return; + intercept = true; + } + + if( intercept ) + { + _self.__composite( e, recurNum, ANY_KEY ); + e.cancel(); + return; } e.__count = Number( Count ); diff --git a/botanjs/src/Components/Vim/Cursor.js b/botanjs/src/Components/Vim/Cursor.js index e9b6d13..f8d2dd9 100644 --- a/botanjs/src/Components/Vim/Cursor.js +++ b/botanjs/src/Components/Vim/Cursor.js @@ -376,13 +376,13 @@ // Open an action handler // i.e. YANK, VISUAL, INSERT, UNDO, etc. - Cursor.prototype.openAction = function( name ) + Cursor.prototype.openAction = function( name, e ) { if( this.action ) this.action.dispose(); debug.Info( "openAction: " + name ); - this.action = new (Actions[ name ])( this ); + this.action = new (Actions[ name ])( this, e ); this.__pulseMsg = null; this.__visualUpdate(); diff --git a/botanjs/src/Components/Vim/_this.js b/botanjs/src/Components/Vim/_this.js index ab26706..dedf3f4 100644 --- a/botanjs/src/Components/Vim/_this.js +++ b/botanjs/src/Components/Vim/_this.js @@ -19,10 +19,12 @@ VIMRE_VERSION = "1.0.0b"; , "UNDO_LIMIT": "Already at oldest change" , "REDO_LIMIT": "Already at newest change" + , "NCHANGES": "%1 change(s); before #%2 %3" , "LINES_FEWER": "%1 fewer line(s)" , "LINES_MORE": "%1 more line(s)" , "LINES_YANKED": "%1 line(s) yanked" + , "LINES_SHIFTED": "%1 line(s) %2ed %3 time(s)" , "SEARCH_HIT_BOTTOM": "search hit BOTTOM, continuing at TOP" , "SEARCH_HIT_TOP": "search hit TOP, continuing at BOTTOM" From 7229447ee155e1bc98376c56bcdaed0a3e58ab99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sat, 16 Apr 2016 00:51:21 +0800 Subject: [PATCH 5/7] UNDO / REDO for line shift --- .../src/Components/Vim/Actions/SHIFT_LINES.js | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js b/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js index a539c17..650c68e 100644 --- a/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js +++ b/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js @@ -5,6 +5,8 @@ var debug = __import( "System.Debug" ); var beep = __import( "Components.Vim.Beep" ); + /** @type {Components.Vim.State.Stator} */ + var Stator = __import( "Components.Vim.State.Stator" ); /** @type {Components.Vim.State.Stack} */ var Stack = __import( "Components.Vim.State.Stack" ); @@ -174,10 +176,13 @@ debug.Info( "Start: " + start, "End: " + end ); var rBlock = ""; + var nLen = 0; var started = false; var indentTimes = 1; + var recStart = 0; + feeder.content = ""; nline = 0; @@ -190,19 +195,28 @@ { started = true; feeder.content = c.substring( 0, i - 1 ); + recStart = feeder.content.length; } if( end < j ) break; var line = c.substring( 1 < i ? i : i - 1, c.indexOf( "\n", i ) ); - if( 1 < i ) feeder.content += "\n"; + if( 1 < i ) + { + feeder.content += "\n"; + rBlock += "\n"; + nLen ++; + } + + rBlock += line; if( line !== "" ) { + var indentedLine; if( 0 < dir ) { - feeder.content += indentChar + line; + indentedLine = indentChar + line; } else { @@ -219,15 +233,40 @@ else if( startC != "\t" ) break; } - feeder.content += line.substring( si + sj - 1 ); + indentedLine = line.substring( si + sj - 1 ); } + + feeder.content += indentedLine; + + nLen += indentedLine.length; nline ++; } } + var nPos = feeder.content.length; feeder.content += "\n" + c.substring( i ) + "\n"; feeder.pan(); + cur.moveTo( nPos ); + + var stator = new Stator( cur, recStart ); + var stack = new Stack(); + + recStart ++; + for( ; ~"\t ".indexOf( feeder.content[ recStart ] ); recStart ++ ); + + var f = stator.save( nLen, rBlock ); + stack.store( function() { + f(); + // Offset correction after REDO / UNDO + cur.moveTo( recStart ); + cur.lineStart(); + } ); + + cur.moveTo( recStart ); + + cur.rec.record( stack ); + this.__msg = Mesg( "LINES_SHIFTED", nline, dir < 0 ? "<" : ">", 1 ); return Triggered; From ce669c2cb625e2cd969157a38dc391460d7d7f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sat, 16 Apr 2016 04:00:15 +0800 Subject: [PATCH 6/7] Line shifting support --- .../src/Components/Vim/Actions/SHIFT_LINES.js | 108 ++++++++++++++---- botanjs/src/Components/Vim/Actions/VISUAL.js | 6 + botanjs/src/Components/Vim/Controls.js | 26 ++++- botanjs/src/Components/Vim/_this.js | 2 + 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js b/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js index 650c68e..1202b6c 100644 --- a/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js +++ b/botanjs/src/Components/Vim/Actions/SHIFT_LINES.js @@ -62,12 +62,14 @@ var start = this.__slineNum; var nline = this.__lines; + var indentMult = 1; if( 1 < e.count ) { nline += e.count; } + // default: >>, <<, >l, " ) || e.kMap( "l" ) ) ) - || ( dir < 0 && ( e.kMap( "<" ) || e.kMap( "h" ) ) ) - ) ) + if( e.range ) + { + sp = e.range.close; + + start = 1; end = -1; + for( var i = 0; i < sp; i ++ ) + { + if( feeder.content[ i ] == "\n" ) + { + end ++; + if( i < e.range.open ) + { + start ++; + } + } + } + + if( end == -1 ) + { + start = end = 0; + } + + if( end < start ) + { + end = -- start; + } + + indentMult = e.count; + } + else if( 0 < dir && ( e.kMap( ">" ) || e.kMap( "l" ) ) ); + else if( dir < 0 && ( e.kMap( "<" ) || e.kMap( "h" ) ) ); + else { beep(); return true; } } } + // VISUAL Mode + else + { + start = 0; + for( var i = 0; i < sp; i ++ ) + { + if( feeder.content[ i ] == "\n" ) start ++; + } + + end = this.__slineNum; + + indentMult = e.count; + } + + if( end < start ) + { + start = start + end; + end = start - end; + start = start - end; + } // last "\n" padding var c = feeder.content.slice( 0, -1 ); @@ -124,7 +182,7 @@ var spOccr = 0; // Guess indent - var tabStat = {}; + var tabStat = []; for( var i = 0; i < l; i ++ ) { @@ -146,6 +204,7 @@ var indentCLen = 0; for( var i in tabStat ) { + i = Number( i ); var p = tabStat[ i ]; if( upperDiff < p ) { @@ -179,13 +238,15 @@ var nLen = 0; var started = false; - var indentTimes = 1; var recStart = 0; feeder.content = ""; nline = 0; + var indented = ""; + for( var i = 0; i < indentMult; i ++ ) indented += indentChar; + for( var i = 0, j = 0; 0 <= i; i = c.indexOf( "\n", i ), j ++ ) { i ++; @@ -216,24 +277,24 @@ var indentedLine; if( 0 < dir ) { - indentedLine = indentChar + line; + indentedLine = indented + line; } else { - for( var si = 0, sj = 1; si < indentTimes; si ++ ) + for( var si = 0, sj = 0; si < indentMult; si ++ ) { - var startC = line[ si ]; + var startC = line[ sj ]; if( startC == " " ) { - for( ; sj < tabwidth; sj ++ ) + for( var swidth = tabwidth + ( sj ++ ); sj < swidth; sj ++ ) { - if( !~"\t ".indexOf( line[ si + sj ] ) ) break; + if( !~"\t ".indexOf( line[ sj ] ) ) break; } } else if( startC != "\t" ) break; } - indentedLine = line.substring( si + sj - 1 ); + indentedLine = line.substring( sj ); } feeder.content += indentedLine; @@ -267,7 +328,14 @@ cur.rec.record( stack ); - this.__msg = Mesg( "LINES_SHIFTED", nline, dir < 0 ? "<" : ">", 1 ); + if( nline ) + { + this.__msg = Mesg( "LINES_SHIFTED", nline, dir < 0 ? "<" : ">", indentMult ); + } + else + { + this.__msg = Mesg( "NO_SHIFT", dir < 0 ? "<" : ">" ); + } return Triggered; }; diff --git a/botanjs/src/Components/Vim/Actions/VISUAL.js b/botanjs/src/Components/Vim/Actions/VISUAL.js index 1d4071d..5e0cbea 100644 --- a/botanjs/src/Components/Vim/Actions/VISUAL.js +++ b/botanjs/src/Components/Vim/Actions/VISUAL.js @@ -10,6 +10,8 @@ var YANK = ns[ NS_INVOKE ]( "YANK" ); /** @type {Components.Vim.IAction} */ var DELETE = ns[ NS_INVOKE ]( "DELETE" ); + /** @type {Components.Vim.IAction} */ + var SHIFT_LINES = ns[ NS_INVOKE ]( "SHIFT_LINES" ); var MODE_NULL = -1; var MODE_VISUAL = 0; @@ -129,6 +131,10 @@ this.__msg = Mesg( "VISLINE" ); } } + else if( e.kMap( "<" ) || e.kMap( ">" ) ) + { + Action = new SHIFT_LINES( cur, e ); + } else if( e.kMap( "v" ) ) { if( this.__mode == MODE_VISUAL ) return true; diff --git a/botanjs/src/Components/Vim/Controls.js b/botanjs/src/Components/Vim/Controls.js index c34a4d9..1173343 100644 --- a/botanjs/src/Components/Vim/Controls.js +++ b/botanjs/src/Components/Vim/Controls.js @@ -300,8 +300,9 @@ var ccur = this.__ccur; var x = ccur.X; + var y = ccur.Y; ccur.moveX( a, b, c || ccur.pSpace ); - if( ccur.X == x ) + if( ccur.X == x && ccur.Y == y ) { beep(); return false; @@ -559,15 +560,32 @@ this.__composite( e, curlyBracket, SHIFT + S_BRACKET_R ); break; - case G: // Go to top + case G: + this.__cMovement = true; - this.__composite( e, function(){ + + // Go to top + this.__composite( e, function() { ccur.moveY( -Number.MAX_VALUE ); ccur.moveX( -Number.MAX_VALUE, true ); }, G ); - this.__composite( e, function(){ + + // Print Hex + this.__composite( e, function() { ccur.openRunAction( "PRINT_HEX", e ); }, _8 ); + + // to lowercase + this.__composite( e, function( e2 ) { + if( ccur.action ) { beep(); return; } + // TODO + }, U ); + + // to uppercase + this.__composite( e, function( e2 ) { + if( ccur.action ) { beep(); return; } + // TODO + }, SHIFT + U ); break; case SHIFT + N: // Next Search diff --git a/botanjs/src/Components/Vim/_this.js b/botanjs/src/Components/Vim/_this.js index dedf3f4..bfa1b4b 100644 --- a/botanjs/src/Components/Vim/_this.js +++ b/botanjs/src/Components/Vim/_this.js @@ -26,6 +26,8 @@ VIMRE_VERSION = "1.0.0b"; , "LINES_YANKED": "%1 line(s) yanked" , "LINES_SHIFTED": "%1 line(s) %2ed %3 time(s)" + , "NO_SHIFT": "No line to %1" + , "SEARCH_HIT_BOTTOM": "search hit BOTTOM, continuing at TOP" , "SEARCH_HIT_TOP": "search hit TOP, continuing at BOTTOM" , "REPLACE": "%1 substitution(s) on %2 line(s)" From d7b2754ba1b696cf9019dda62ee79c7a49b62d74 Mon Sep 17 00:00:00 2001 From: tgckpg Date: Sat, 16 Apr 2016 04:05:47 +0800 Subject: [PATCH 7/7] Update README.md --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ccc06d9..f20e039 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ Visit the demo over [here](https://tgckpg.github.io/BotanJS-vim) Common commands are now supported. I am now going to list the commands that yet to be made. ``` Commands that are going to implement soon: -auto indent, <>, [action]i[quote], [count] +auto indent ( new line from bracket ), [action]i[quote] :'<,'> Commands that are planning to implement in near future: macro -code auto format +code auto format "=" :set nowrap Commands that are planning to implement in far future: @@ -26,7 +26,7 @@ plugins - I am NOT going to make this ``` ### How it works -By *screen buffer*, it means that the textarea is treated as a screen buffer. You are not directly interacting with the textarea. Instead you type into the script, then the result is *rendered* through the textarea. +By *screen buffer*, it means that the textarea is treated as a screen. You are not directly interacting with the textarea. Instead you type into the script, then the result is *rendered* through the textarea. ### Why use screen buffer? By treating the textarea as a *screen*. I could archive almost everything except for coloring. And it is easier to precisely track the cursor this way. @@ -49,6 +49,3 @@ I tried porting it into the browser tho. But I am too stupid to do that. But sti ### How can I contribute? First, you need to understand the framework behind it. Then file a merge request. Sounds easy right?... prepare to DIEEE! - -### You made this? I made this. -Licensed under GNU Pubic License