diff --git a/botanjs/src/Components/Vim/Controls.js b/botanjs/src/Components/Vim/Controls.js index 8dba252..0909015 100644 --- a/botanjs/src/Components/Vim/Controls.js +++ b/botanjs/src/Components/Vim/Controls.js @@ -281,6 +281,30 @@ break case SHIFT + _5: // %, Find next item + var analyzer = this.__vimArea.contentAnalyzer; + + /** @type {Components.Vim.Syntax.TokenMatch} */ + var bracketMatch = analyzer.bracketAt( ccur.aPos ); + + if( bracketMatch.open == -1 ) + { + beep(); + break; + } + + var d = 1; + var at = bracketMatch.close; + if( bracketMatch.selected == at ) + { + d = -1; + at = bracketMatch.open; + } + + while( ccur.aPos != at ) + { + ccur.moveX( d, true ); + } + break; case T: // To break; @@ -291,6 +315,8 @@ break; } + var analyzer = this.__vimArea.contentAnalyzer; + this.__cMovement = true; // Word boundary this.__comp( kCode, function(){ @@ -307,6 +333,7 @@ }, SHIFT + S_BRACKET_L ); this.__comp( kCode, function(){ debug.Info( "Bracket boundary }" ); + analyzer.bracketAt( ccur.aPos ); }, SHIFT + S_BRACKET_R ); break; diff --git a/botanjs/src/Components/Vim/Cursor.js b/botanjs/src/Components/Vim/Cursor.js index 5f99f5d..240dd10 100644 --- a/botanjs/src/Components/Vim/Cursor.js +++ b/botanjs/src/Components/Vim/Cursor.js @@ -88,9 +88,8 @@ // Set by VimArea Cursor.prototype.Vim; - // Can only be 1, -1 - // 0 will be treated as undefined - Cursor.prototype.moveX = function( d, penentrate, phantomSpace ) + // 0 will be treated as default ( 1 ) + Cursor.prototype.moveX = function( d, penetrate, phantomSpace ) { var x = this.pX; @@ -101,7 +100,7 @@ var buffs = this.feeder.lineBuffers; - if( penentrate ) + if( penetrate ) { if( x < 0 && ( 0 < this.feeder.panY || 0 < this.Y ) ) { @@ -118,15 +117,41 @@ var c = content[ x ]; - // Include empty lines befor cursor end + // Motion includes empty lines before cursor end if( ( phantomSpace && cLen - 1 <= x ) || ( cLen == 1 && c == undefined ) ) { - x = 0 < d ? cLen - 1 : 0; + if( 0 < d ) + { + x = cLen - 1; + if( penetrate ) + { + this.X = 0; + this.moveY( 1 ); + return; + } + } + else + { + x = 0; + } } - // ( 2 < cLen ) Exclude empty lines at cursor end + // ( 2 < cLen ) motion excludes empty lines at cursor end else if( ( 2 <= cLen && x == cLen - 1 && c == " " ) || c == undefined ) { - x = 0 < d ? cLen - 2 : 0; + if( 0 < d ) + { + x = cLen - 2; + if( penetrate ) + { + this.X = 0; + this.moveY( 1 ); + return; + } + } + else + { + x = 0; + } } else if( c == "\n" ) { @@ -174,7 +199,7 @@ this.feeder.dispatcher.dispatchEvent( new BotanEvent( "VisualUpdate" ) ); }; - Cursor.prototype.moveY = function( d, penentrate ) + Cursor.prototype.moveY = function( d, penetrate ) { var i; var Y = this.Y + d; @@ -197,7 +222,7 @@ { var feeder = this.feeder; - if( penentrate ) + if( penetrate ) { feeder.pan( undefined, Y - moreAt ); } diff --git a/botanjs/src/Components/Vim/Syntax/Analyzer.js b/botanjs/src/Components/Vim/Syntax/Analyzer.js new file mode 100644 index 0000000..b21aace --- /dev/null +++ b/botanjs/src/Components/Vim/Syntax/Analyzer.js @@ -0,0 +1,267 @@ +(function(){ + var ns = __namespace( "Components.Vim.Syntax" ); + + /** @type {System.Debug} */ + var debug = __import( "System.Debug" ); + + var TOK_OPEN = 0; + var TOK_CLOSED = 1; + var TOK_LEVEL = 2; + var TOK_PARENT = 3; + + var TOK_SEP = "\n"; + + var TOK_JOIN = function( a, b ) { return a + TOK_SEP + b; }; + + /*{{{ Private Class */ + var TokenPairs = function( tok, content, esc ) + { + var l = content.length; + var toks = tok.split( TOK_SEP ); + var openToken = toks[0]; + var closeToken = toks[1]; + + var opStack = []; + + var unmatchedEd = []; + + var lv = 0; + + var pairs = []; + + var lvUp = function( i ) + { + opStack[ lv ] = i; + lv ++; + }; + + var lvDown = function( i ) + { + if( lv == 0 ) + { + // Cannot level down. i.e. Unmatched tokens + unmatchedEd.push( i ); + return; + } + + var Token = []; + Token[ TOK_OPEN ] = opStack[ -- lv ]; + Token[ TOK_CLOSED ] = i; + Token[ TOK_LEVEL ] = lv; + Token[ TOK_PARENT ] = 0 < lv ? opStack[ lv - 1 ] : -1; + + pairs.push( Token ); + }; + + var opLen = openToken.length; + var edLen = closeToken.length; + for( var i = 0; i < l; i ++ ) + { + var opTok = content.substr( i, opLen ); + var edTok = content.substr( i, edLen ); + if( opTok == openToken ) + { + lvUp( i ); + i += opLen - 1; + } + else if( edTok == closeToken ) + { + lvDown( i ); + i += edLen - 1; + } + } + + if( unmatchedEd.length ) + { + debug.Info( "Unmatched opening \"" + openToken + "\"@" + unmatchedEd.join( ", " ) ); + } + + if( 0 < lv ) + { + debug.Info( "Unmatched closing \"" + closeToken + "\"@" + opStack.slice( 0, lv ) ); + } + + this.__pairs = pairs; + this.token = toks; + }; + + TokenPairs.prototype.token = ""; + + TokenPairs.prototype.matched = function() + { + return this.__pairs.sort( + function( a, b ) { return a[ TOK_OPEN ] - b[ TOK_OPEN ]; } + ); + }; + + TokenPairs.prototype.find = function( pos, state ) + { + if( state == undefined ) state = TOK_OPEN; + + var pairs = this.__pairs; + var l = pairs.length; + + for( var i = 0; i < l; i ++ ) + { + var pair = pairs[i]; + if( pair[ state ] == pos ) + { + return pair; + } + } + + return null; + }; + /* End Private Class }}}*/ + + var Analyzer = function( feeder ) + { + /* @type {Components.Vim.LineFeeder} */ + this.__feeder = feeder; + this.__tokpairs = {}; + }; + + Analyzer.prototype.bracketAt = function( p ) + { + var c = this.__feeder.content; + var tokState = TOK_CLOSED; + + var BracketPairs = null; + var cTok = c[ p ]; + + switch( cTok ) + { + case "{": tokState = TOK_OPEN; + case "}": + BracketPairs = this.GetPairs( TOK_JOIN( "{", "}" ) ); + break; + + case "[": tokState = TOK_OPEN; + case "]": + BracketPairs = this.GetPairs( TOK_JOIN( "[", "]" ) ); + break; + + case "(": tokState = TOK_OPEN; + case ")": + BracketPairs = this.GetPairs( TOK_JOIN( "(", ")" ) ); + break; + + case "/": + if( c[ p - 1 ] == "*" ) + { + cTok = "*/"; + p --; + break; + } + else if( c[ p + 1 ] == "*" ) + { + cTok = "/*"; + break; + } + return new TokenMatch(); + + case "*": + if( c[ p - 1 ] == "/" ) + { + cTok = "/*"; + p --; + break; + } + else if( c[ p + 1 ] == "/" ) + { + cTok = "*/"; + break; + } + return new TokenMatch(); + + default: + return new TokenMatch(); + } + + // Long Switch + if( !BracketPairs ) switch( cTok ) + { + case "/*": tokState = TOK_OPEN; + case "*/": + BracketPairs = this.GetPairs( TOK_JOIN( "/*", "*/" ) ); + break; + + default: + return new TokenMatch(); + } + + var SetParent = function( pair ) + { + if( !pair ) throw new Error( "Parent not found" ); + + var tMatch = new TokenMatch(); + tMatch.__level = pair[ TOK_LEVEL ]; + tMatch.__open = pair[ TOK_OPEN ]; + tMatch.__close = pair[ TOK_CLOSED ]; + + if( -1 < pair[ TOK_PARENT ] ) + { + var rPair = BracketPairs.find( pair[ TOK_PARENT ] ); + tMatch.__parent = SetParent( rPair ); + } + + return tMatch; + }; + + var rPair = BracketPairs.find( p, tokState ); + var tMatch = SetParent( rPair ) + tMatch.__selected = p; + + return tMatch; + }; + + Analyzer.prototype.GetPairs = function( def, reload ) + { + if( !reload && this.__tokpairs[ def ] ) + { + return this.__tokpairs[ def ]; + } + + var c = this.__feeder.content; + var pairs = new TokenPairs( def, c ); + + this.__tokpairs[ def ] = pairs; + + return pairs; + }; + + Analyzer.prototype.quoteAt = function( p ) + { + var c = this.__feeder.content; + switch( c[ p ] ) + { + case "`": + case "\"": + case "\'": + default: + return { + level: 0 + , open: -1 + , close: -1 + }; + } + }; + + var TokenMatch = function() + { + this.__open = -1; + this.__close = -1; + this.__selected = -1; + this.__level = -1; + this.__parent = null; + }; + + __readOnly( TokenMatch.prototype, "parent", function() { return this.__parent; } ); + __readOnly( TokenMatch.prototype, "open", function() { return this.__open; } ); + __readOnly( TokenMatch.prototype, "close", function() { return this.__close; } ); + __readOnly( TokenMatch.prototype, "level", function() { return this.__level; } ); + __readOnly( TokenMatch.prototype, "selected", function() { return this.__selected; } ); + + ns[ NS_EXPORT ]( EX_CLASS, "Analyzer", Analyzer ); + ns[ NS_EXPORT ]( EX_CLASS, "TokenMatch", TokenMatch ); +})(); diff --git a/botanjs/src/Components/Vim/VimArea.js b/botanjs/src/Components/Vim/VimArea.js index 54c7864..116bd46 100644 --- a/botanjs/src/Components/Vim/VimArea.js +++ b/botanjs/src/Components/Vim/VimArea.js @@ -11,7 +11,9 @@ var debug = __import( "System.Debug" ); /** @type {Components.Vim.State.Registers} */ - var Registers = __import( "Components.Vim.State.Registers" ); + var Registers = __import( "Components.Vim.State.Registers" ); + /** @type {Components.Vim.Syntax.Analyzer} */ + var SyntaxAnalyzer = __import( "Components.Vim.Syntax.Analyzer" ); /** @type {Components.Vim.LineFeeder} */ var LineFeeder = ns[ NS_INVOKE ]( "LineFeeder" ); @@ -94,6 +96,7 @@ // Content feeder var cfeeder = new LineFeeder( cRange, c ); + var contentAnalyzer = new SyntaxAnalyzer( cfeeder ); // Feed the contents to content feeder // This "\n" fixes the last line "\n" not displaying @@ -132,6 +135,7 @@ Update(); this.contentFeeder = cfeeder; + this.contentAnalyzer = contentAnalyzer; this.statusFeeder = sfeeder; this.statusBar = statusBar; this.registers = new Registers(); diff --git a/botanjs/src/externs/Components.Vim.Syntax.Analyzer.js b/botanjs/src/externs/Components.Vim.Syntax.Analyzer.js new file mode 100644 index 0000000..f255903 --- /dev/null +++ b/botanjs/src/externs/Components.Vim.Syntax.Analyzer.js @@ -0,0 +1,7 @@ +/** @constructor */ +Components.Vim.Syntax.Analyzer = function(){}; + +/** @type Function */ +Components.Vim.Syntax.Analyzer.bracketAt; +/** @type Function */ +Components.Vim.Syntax.Analyzer.quoteAt; diff --git a/botanjs/src/externs/Components.Vim.Syntax.TokenMatch.js b/botanjs/src/externs/Components.Vim.Syntax.TokenMatch.js new file mode 100644 index 0000000..916f3cd --- /dev/null +++ b/botanjs/src/externs/Components.Vim.Syntax.TokenMatch.js @@ -0,0 +1,14 @@ +/** @constructor */ +Components.Vim.Syntax.TokenMatch = function(){}; + +/** @type {Components.Vim.Syntax.TokenMatch} */ +Components.Vim.Syntax.TokenMatch.parent; + +/** @type Number */ +Components.Vim.Syntax.TokenMatch.open; +/** @type Number */ +Components.Vim.Syntax.TokenMatch.close; +/** @type Number */ +Components.Vim.Syntax.TokenMatch.level; +/** @type Number */ +Components.Vim.Syntax.TokenMatch.selected; diff --git a/botanjs/src/externs/Components.Vim.Syntax.js b/botanjs/src/externs/Components.Vim.Syntax.js new file mode 100644 index 0000000..97fc80c --- /dev/null +++ b/botanjs/src/externs/Components.Vim.Syntax.js @@ -0,0 +1,2 @@ +/** @object */ +Components.Vim.Syntax = {}; diff --git a/botanjs/src/externs/Components.Vim.VimArea.js b/botanjs/src/externs/Components.Vim.VimArea.js index 982acda..51c9825 100644 --- a/botanjs/src/externs/Components.Vim.VimArea.js +++ b/botanjs/src/externs/Components.Vim.VimArea.js @@ -3,6 +3,8 @@ Components.Vim.VimArea = function(){}; /** @type {Components.Vim.LineFeeder} */ Components.Vim.VimArea.contentFeeder; +/** @type {Components.Vim.Syntax.Analyzer} */ +Components.Vim.VimArea.contentAnalyzer; /** @type {Components.Vim.LineFeeder} */ Components.Vim.VimArea.statusFeeder; /** @type {Components.Vim.StatusBar} */