├── .gitignore ├── .gitmodules ├── Cakefile ├── LICENSE ├── README.md ├── build ├── jim-ace.development.js └── jim-ace.min.js ├── docs ├── ace.html ├── commands.html ├── docco.css ├── helpers.html ├── jim.html ├── keymap.html ├── modes.html ├── motions.html └── operators.html ├── index.html ├── src ├── ace.coffee ├── commands.coffee ├── helpers.coffee ├── jim.coffee ├── keymap.coffee ├── modes.coffee ├── motions.coffee └── operators.coffee ├── test ├── ace │ ├── commands.coffee │ ├── dot_command.coffee │ ├── insert_mode.coffee │ ├── motions.coffee │ ├── operators.coffee │ ├── search.coffee │ ├── undo.coffee │ ├── unhandled_keys.coffee │ └── visual_mode.coffee ├── fixtures │ └── sort_by.js └── test.html └── vendor ├── coffee-script.js ├── jquery-1.6.2.min.js ├── qunit.css ├── qunit.js ├── require.js └── text.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/ace"] 2 | path = vendor/ace 3 | url = git://github.com/ajaxorg/ace.git 4 | [submodule "vendor/pilot"] 5 | path = vendor/pilot 6 | url = git://github.com/ajaxorg/pilot.git 7 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | CoffeeScript = require 'coffee-script' 3 | 4 | sourceNames = [ 5 | # in dependency order 6 | 'helpers' 7 | 'keymap' 8 | 'modes' 9 | 'jim' 10 | 'motions' 11 | 'operators' 12 | 'commands' 13 | 'ace' 14 | ] 15 | 16 | 17 | header = """ 18 | /** 19 | * Jim v#{require('./src/jim').VERSION} 20 | * https://github.com/misfo/jim 21 | * 22 | * Copyright 2011, Trent Ogren 23 | * Released under the MIT License 24 | */ 25 | """ 26 | 27 | task 'build:ace', 'build development version of Jim for use with Ace', -> 28 | # based on coffee-script's dead-simple `cake build:browser` 29 | jsCode = '' 30 | for sourceName in sourceNames 31 | source = "src/#{sourceName}.coffee" 32 | 33 | coffeeCode = fs.readFileSync source, 'utf8' 34 | jsCode += """ 35 | 36 | require['./#{sourceName}'] = (function() { 37 | var exports = {}, module = {}; 38 | #{CoffeeScript.compile coffeeCode, bare: yes} 39 | return module.exports || exports; 40 | })(); 41 | 42 | """ 43 | 44 | jsCode = """ 45 | this.Jim = (function() { 46 | function require(path) { return path[0] === '.' ? require[path] : window.require(path); } 47 | #{jsCode} 48 | return require['./jim']; 49 | })() 50 | """ 51 | 52 | filename = 'build/jim-ace.development.js' 53 | fs.writeFileSync filename, "#{header}\n#{jsCode}" 54 | console.log "#{(new Date).toLocaleTimeString()} - built #{filename}" 55 | 56 | jsCode 57 | 58 | task 'build:ace:watch', 'continuously build development version of Jim for use with Ace', -> 59 | invoke 'build:ace' 60 | for sourceName in sourceNames 61 | fs.watchFile "src/#{sourceName}.coffee", {persistent: yes, interval: 500}, (curr, prev) -> 62 | invoke 'build:ace' unless curr.size is prev.size and curr.mtime.getTime() is prev.mtime.getTime() 63 | 64 | task 'build:ace:min', 'build minified version of Jim for use with Ace', -> 65 | jsCode = invoke 'build:ace' 66 | 67 | {parser, uglify} = require 'uglify-js' 68 | minifiedCode = uglify.gen_code uglify.ast_squeeze uglify.ast_mangle parser.parse jsCode 69 | 70 | filename = 'build/jim-ace.min.js' 71 | fs.writeFileSync filename, "#{header}\n#{minifiedCode}" 72 | console.log "#{(new Date).toLocaleTimeString()} - built #{filename}" 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Trent Ogren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Jim** is a JavaScript library that adds a Vim mode to the excellent in-browser editor 2 | [Ace](http://ajaxorg.github.com/ace/). Github uses Ace for its editor, so you can use 3 | the [Jim bookmarklet](http://misfo.github.com/jim) to Jimmy rig it with Vim-ness. 4 | 5 | 6 | Project status 7 | -------------- 8 | Jim is **no longer maintained**. Cloud9 IDE now has a built-in Vim mode, but it doesn't 9 | work with standalone Ace AFAIK, so fork away if you'd like to continue this project. 10 | 11 | I'm not terribly proud of how the code ended up. Using classes for each of the commands 12 | is a textbook case of OO being over-applied. For an idea of how to implement a Vim-mode 13 | The Right Way, take a look at 14 | [Sublime Text 2's Vintage mode](https://github.com/sublimehq/Vintage). I use it on a 15 | daily basis and it works great. The way commands are "built" with keystrokes and then 16 | executed afterwards with one `ViEval` command turns out to be a far superior way to 17 | write a Vim mode. 18 | 19 | 20 | Try it out 21 | ---------- 22 | [misfo.github.com/jim](http://misfo.github.com/jim) 23 | 24 | 25 | Embed Jim with Ace in your app 26 | ------------------------------ 27 | ```html 28 | 29 | 30 | 31 | 38 | ``` 39 | 40 | 41 | Annotated source 42 | ---------------- 43 | * [ace.coffee](http://misfo.github.com/jim/docs/ace.html) has all the Ace-specific 44 | logic for hooking into its keyboard handling, moving the cursor, modifying the 45 | document, etc. 46 | * [jim.coffee](http://misfo.github.com/jim/docs/jim.html) holds all of Jim's state. 47 | * [keymap.coffee](http://misfo.github.com/jim/docs/keymap.html), well, maps keys. 48 | * [modes.coffee](http://misfo.github.com/jim/docs/modes.html) defines each of the 49 | modes' different key handling behaviors 50 | * Commands are defined in 51 | [motions.coffee](http://misfo.github.com/jim/docs/motions.html), 52 | [operators.coffee](http://misfo.github.com/jim/docs/operators.html), and 53 | [commands.coffee](http://misfo.github.com/jim/docs/commands.html) 54 | * Odds and ends get thrown in 55 | [helpers.coffee](http://misfo.github.com/jim/docs/helpers.html) 56 | 57 | 58 | What works so far 59 | ----------------- 60 | * [modes](http://misfo.github.com/jim/docs/modes.html): normal, visual 61 | (characterwise and linewise), insert, replace 62 | * [operators](http://misfo.github.com/jim/docs/operators.html): `c`, `d`, 63 | `y`, `>`, and `<` in normal and visual modes (double 64 | operators work as linewise commands in normal mode, too) 65 | * [motions](http://misfo.github.com/jim/docs/motions.html) (can be used with 66 | counts and/or operators, and in visual mode) 67 | * `h`, `j`, `k`, `l` 68 | * `W`, `E`, `B`, `w`, `e`, `b` 69 | * `0`, `^`, `$` 70 | * `G`, `gg` 71 | * `H`, `M`, `L` 72 | * `/`, `?`, `n`, `N`, `*`, `#` 73 | * `f`, `F`, `t`, `T` 74 | * other [commands](http://misfo.github.com/jim/docs/commands.html) 75 | * insert switches: `i`, `a`, `o`, `O`, `I`, `A`, and `C` 76 | * commands: `D`, `gJ`, `J`, `p`, `P`, `r`, `s`, `x`, `X`, `u`, and `.` 77 | * visual mode commands: `gJ`, `J`, `p` and `P` 78 | * default register (operations yank text in the register for pasting) 79 | * `u` works as it does in Vim (`Cmd-z` and `Cmd-y` still work as they do in Ace) 80 | 81 | If you have a feature request [create an issue](https://github.com/misfo/jim/issues/new) 82 | 83 | 84 | Known issues 85 | ------------ 86 | Take a gander at the [issue tracker](https://github.com/misfo/jim/issues) 87 | 88 | 89 | Hack 90 | ---- 91 | ``` 92 | git clone git://github.com/misfo/jim.git 93 | cd jim 94 | git submodule update --init 95 | ``` 96 | 97 | Then just open index.html and you're good to go. 98 | 99 | Chrome needs a special command line argument to allow XHRs to files: 100 | `google-chrome --allow-file-access-from-files` 101 | 102 | To keep the development js file built while you develop, you'll need CoffeeScript: 103 | 104 | ``` 105 | npm install coffee-script 106 | ``` 107 | 108 | 109 | Then build your files in the background: 110 | 111 | ``` 112 | cake build:ace:watch 113 | ``` 114 | 115 | 116 | Open test/test.html to run the tests 117 | 118 | 119 | Thanks! 120 | ------- 121 | Thanks to all [contributors](https://github.com/misfo/jim/contributors). 122 | In other words: thanks [sourrust](https://github.com/sourrust). 123 | -------------------------------------------------------------------------------- /build/jim-ace.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Jim v0.2.0 3 | * https://github.com/misfo/jim 4 | * 5 | * Copyright 2011, Trent Ogren 6 | * Released under the MIT License 7 | */ 8 | this.Jim=function(){function a(b){return b[0]==="."?a[b]:window.require(b)}return a["./helpers"]=function(){var a={},b={};return a.Command=function(){function a(a){this.count=a!=null?a:1}return a.prototype.isRepeatable=!0,a.prototype.isComplete=function(){return this.constructor.followedBy?this.followedBy:!0},a}(),a.repeatCountTimes=function(a){return function(b){var c,d;c=this.count,d=[];while(c--)d.push(a.call(this,b));return d}},b.exports||a}(),a["./motions"]=function(){var b={},c={},d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q=Object.prototype.hasOwnProperty,R=function(a,b){function d(){this.constructor=a}for(var c in b)Q.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};return P=a("./helpers"),d=P.Command,N=P.repeatCountTimes,J={},M=function(a,b){return J[a]=b},o=function(){function a(a){this.count=a!=null?a:1}return R(a,d),a.prototype.isRepeatable=!1,a.prototype.linewise=!1,a.prototype.exclusive=!1,a.prototype.visualExec=function(a){return this.exec(a)},a}(),n=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=function(a){var b;if(b=this.count-1)return(new r(b)).exec(a)},a}(),M("h",s=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=N(function(a){return a.adaptor.moveLeft()}),a}()),M("j",r=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=N(function(a){return a.adaptor.moveDown()}),a}()),M("k",B=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=N(function(a){return a.adaptor.moveUp()}),a}()),M("l",t=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=N(function(a){return a.adaptor.moveRight(this.operation!=null)}),a}()),I=function(){return/\S+/g},O=function(){return/(\w+)|([^\w\s]+)/g},M("e",A=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exec=N(function(a){var b,c,d,e,f,g,h,i;f=this.bigWord?I():O(),d=a.adaptor.lineText(),i=a.adaptor.position(),h=i[0],b=i[1],g=d.substring(b),e=f.exec(g),(e!=null?e[0].length:void 0)<=1&&(e=f.exec(g));if(e)b+=e[0].length+e.index-1;else for(;;){d=a.adaptor.lineText(++h),c=f.exec(d);if(c){b=c[0].length+c.index-1;break}if(h===a.adaptor.lastRow())return}return a.adaptor.moveTo(h,b)}),a}()),M("E",v=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,A),a.prototype.bigWord=!0,a}()),M("w",z=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=function(a){var b,c,d,e,f,g,h,i,j,k,l,m;j=this.count,m=[];while(j--){g=this.bigWord?I():O(),d=a.adaptor.lineText(),k=a.adaptor.position(),i=k[0],b=k[1],h=d.substring(b),e=g.exec(h),(e!=null?e.index:void 0)===0&&(e=g.exec(h));if(!e)j===0&&this.operation?b=d.length:(d=a.adaptor.lineText(++i),f=g.exec(d),b=(f!=null?f.index:void 0)||0);else{if(j===0&&((l=this.operation)!=null?l.switchToMode:void 0)==="insert"){c=new A,c.bigWord=this.bigWord,c.exec(a),this.exclusive=!1;return}b+=e.index}m.push(a.adaptor.moveTo(i,b))}return m},a}()),M("W",y=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,z),a.prototype.bigWord=!0,a}()),K=RegExp(""+I().source+"\\s*$"),L=RegExp("("+O().source+")\\s*$"),M("b",q=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=N(function(a){var b,c,d,e,f,g,h;f=this.bigWord?K:L,d=a.adaptor.lineText(),h=a.adaptor.position(),g=h[0],b=h[1],c=d.substring(0,b),e=f.exec(c);if(e)b=e.index;else{g--;while(/^\s+$/.test(d=a.adaptor.lineText(g)))g--;e=f.exec(d),b=(e!=null?e.index:void 0)||0}return a.adaptor.moveTo(g,b)}),a}()),M("B",p=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,q),a.prototype.bigWord=!0,a}()),M("0",u=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=function(a){return a.adaptor.moveTo(a.adaptor.row(),0)},a}()),M("^",x=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exec=function(a){var b,c,d,e;return d=a.adaptor.row(),c=a.adaptor.lineText(d),b=((e=/\S/.exec(c))!=null?e.index:void 0)||0,a.adaptor.moveTo(d,b)},a}()),M("$",w=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exec=function(a){var b;return b=this.count-1,b&&(new r(b)).exec(a),a.adaptor.moveToLineEnd()},a}()),M("gg",g=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=function(a){var b,c;return c=this.count-1,b=a.adaptor.lineText(c),a.adaptor.moveTo(c,0),(new x).exec(a)},a}()),M("G",h=function(){function a(a){this.count=a}return R(a,g),a.prototype.exec=function(b){return this.count||(this.count=b.adaptor.lastRow()+1),a.__super__.exec.apply(this,arguments)},a}()),M("H",e=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=function(a){var b;return b=a.adaptor.firstFullyVisibleRow()+this.count,(new h(b)).exec(a)},a}()),M("M",i=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=function(a){var b,c,d;return d=a.adaptor.firstFullyVisibleRow(),b=a.adaptor.lastFullyVisibleRow()-d,c=Math.floor(b/2),(new h(d+1+c)).exec(a)},a}()),M("L",f=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.linewise=!0,a.prototype.exec=function(a){var b;return b=a.adaptor.lastFullyVisibleRow()+2-this.count,(new h(b)).exec(a)},a}()),M("/",E=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.runSearch=function(a,b,c){var d,e,f,g,h;if(!a.search)return;g=a.search,d=g.backwards,e=g.searchString,f=g.wholeWord,c&&(d=!d),h=[];while(b--)h.push(a.adaptor.search(d,e,f));return h},a.prototype.exclusive=!0,a.prototype.getSearch=function(){return{searchString:prompt("Find:"),backwards:this.backwards}},a.prototype.exec=function(b){return b.search=this.getSearch(b),a.runSearch(b,this.count)},a}()),M("?",H=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,E),a.prototype.backwards=!0,a}()),M("*",C=function(){function b(){b.__super__.constructor.apply(this,arguments)}var a;return R(b,E),b.prototype.getSearch=function(b){var c,d,e,f;return f=a(b),d=f[0],c=f[1],c&&(new t(c)).exec(b),e=/^\w/.test(d),{searchString:d,wholeWord:e,backwards:this.backwards}},a=function(a){var b,c,d,e,f,g,h,i;return f=a.adaptor.lineText(),c=a.adaptor.column(),e=f.substring(0,c),i=f.substring(c),b=null,/\W/.test(f[c])?(d=[""],g=/\w+/.exec(i),h=g?g:/[^\w\s]+/.exec(i),b=h.index):(d=/\w*$/.exec(e),h=/^\w*/.exec(i)),[d[0]+h[0],b]},b}()),M("#",D=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,C),a.prototype.backwards=!0,a}()),M("n",F=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=function(a){return E.runSearch(a,this.count)},a}()),M("N",G=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.prototype.exclusive=!0,a.prototype.exec=function(a){return E.runSearch(a,this.count,!0)},a}()),M("f",j=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.followedBy=/./,a.prototype.exec=function(a){var b,c,d,e,f,g,h;f=(g=this.count)!=null?g:1,h=a.adaptor.position(),e=h[0],b=h[1],d=a.adaptor.lineText().substring(b+1),c=0;while(f--)c=d.indexOf(this.followedBy,c)+1;if(c)return this.beforeChar&&c--,a.adaptor.moveTo(e,b+c)},a}()),M("t",l=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,j),a.prototype.beforeChar=!0,a}()),M("F",k=function(){function a(){a.__super__.constructor.apply(this,arguments)}return R(a,o),a.followedBy=/./,a.prototype.exec=function(a){var b,c,d,e,f,g,h;f=(g=this.count)!=null?g:1,h=a.adaptor.position(),d=h[0],b=h[1],c=a.adaptor.lineText().substring(0,b),e=b;while(f--)e=c.lastIndexOf(this.followedBy,e-1);if(0<=e&&e",h=function(){function a(){a.__super__.constructor.apply(this,arguments)}return q(a,j),a.prototype.operate=function(a){var b,c,d;return d=a.adaptor.selectionRowRange(),c=d[0],b=d[1],a.adaptor.indentSelection(),(new g(c+1)).exec(a)},a}()),n("<",k=function(){function a(){a.__super__.constructor.apply(this,arguments)}return q(a,j),a.prototype.operate=function(a){var b,c,d;return d=a.adaptor.selectionRowRange(),c=d[0],b=d[1],a.adaptor.outdentSelection(),(new g(c+1)).exec(a)},a}()),c.exports={Change:d,Delete:f,defaultMappings:m},c.exports||b}(),a["./commands"]=function(){var b={},c={},d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K=Object.prototype.hasOwnProperty,L=function(a,b){function d(){this.constructor=a}for(var c in b)K.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};return I=a("./helpers"),g=I.Command,H=I.repeatCountTimes,h=a("./operators").Delete,J=a("./motions"),r=J.MoveLeft,s=J.MoveRight,t=J.MoveToEndOfLine,u=J.MoveToFirstNonBlank,F={},G=function(a,b){return F[a]=b},q=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.exec=function(a){return typeof this.beforeSwitch=="function"&&this.beforeSwitch(a),a.setMode(this.switchToMode)},a}(),G("v",E=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.isRepeatable=!1,a.prototype.exec=function(a){var b;return b=a.adaptor.position(),a.adaptor.setSelectionAnchor(),a.setMode("visual",{anchor:b})},a.prototype.visualExec=function(a){var b;return a.mode.linewise?(a.setMode("visual",{linewise:!1}),(b=a.adaptor.editor.selection).setSelectionAnchor.apply(b,a.mode.anchor)):a.onEscape()},a}()),G("V",D=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.isRepeatable=!1,a.prototype.exec=function(a){var b;return b=a.adaptor.setLinewiseSelectionAnchor(),a.setMode("visual",{linewise:!0,anchor:b})},a.prototype.visualExec=function(a){var b,c;return a.mode.linewise?a.onEscape():(c={linewise:!0},b=a.adaptor.setLinewiseSelectionAnchor(),a.mode.anchor||(c.anchor=b),a.setMode("visual",c))},a}()),G("i",k=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,q),a.prototype.switchToMode="insert",a.prototype.exec=function(a){return typeof this.beforeSwitch=="function"&&this.beforeSwitch(a),this.repeatableInsert?a.adaptor.insert(this.repeatableInsert.string):(a.afterInsertSwitch=!0,a.setMode(this.switchToMode))},a}()),G("a",l=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,k),a.prototype.beforeSwitch=function(a){return a.adaptor.moveRight(!0)},a}()),G("A",m=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,k),a.prototype.beforeSwitch=function(a){return(new t).exec(a),a.adaptor.moveRight(!0)},a}()),G("C",f=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,k),a.prototype.beforeSwitch=function(a){return(new j(this.count)).exec(a)},a}()),G("I",n=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,k),a.prototype.beforeSwitch=function(a){return(new u).exec(a)},a}()),G("o",v=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,k),a.prototype.beforeSwitch=function(a){var b;return b=a.adaptor.row()+(this.above?0:1),a.adaptor.insertNewLine(b),a.adaptor.moveTo(b,0)},a}()),G("O",w=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,v),a.prototype.above=!0,a}()),G("s",e=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,k),a.prototype.beforeSwitch=function(a){return(new i(this.count)).exec(a)},a}()),G("R",B=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,q),a.prototype.beforeSwitch=function(a){return a.adaptor.setOverwriteMode(!0)},a.prototype.switchToMode="replace",a}()),G("gJ",o=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.exec=function(a){var b,c;b=Math.max(this.count,2)-1,c=[];while(b--)a.adaptor.selectLineEnding(this.normalize),a.adaptor.deleteSelection(),c.push(this.normalize?(a.adaptor.insert(" "),a.adaptor.moveLeft()):void 0);return c},a.prototype.visualExec=function(a){var b,c,d;return d=a.adaptor.selectionRowRange(),c=d[0],b=d[1],a.adaptor.clearSelection(),a.adaptor.moveTo(c,0),this.count=b-c+1,this.exec(a),a.setMode("normal")},a}()),G("J",p=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,o),a.prototype.normalize=!0,a}()),G("D",j=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.exec=function(a){return(new h(1,new t(this.count))).exec(a)},a}()),G("p",x=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.exec=function(a){var b,c,d,e,f,g,h,i,j,k;if(!(g=a.registers['"']))return;return i=(new Array(this.count+1)).join(g),f=/\n$/.test(g),f?(h=a.adaptor.row()+(this.before?0:1),d=a.adaptor.lastRow(),h>d?(k=/^([\s\S]*)(\r?\n)$/.exec(i),j=k[0],b=k[1],e=k[2],i=e+b,c=a.adaptor.lineText(d).length-1,a.adaptor.moveTo(h,c)):a.adaptor.moveTo(h,0),a.adaptor.insert(i),a.adaptor.moveTo(h,0)):a.adaptor.insert(i,!this.before)},a.prototype.visualExec=function(a){var b;return a.mode.linewise?a.adaptor.makeLinewise():a.adaptor.includeCursorInSelection(),b=a.adaptor.deleteSelection(),this.before=!0,this.exec(a),a.registers['"']=b,a.setMode("normal")},a}()),G("P",y=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,x),a.prototype.before=!0,a}()),G("r",A=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.followedBy=/[\s\S]+/,a.prototype.exec=function(a){var b;return a.adaptor.setSelectionAnchor(),(new s(this.count)).exec(a),a.adaptor.deleteSelection(),b=/^\r?\n$/.test(this.followedBy)?this.followedBy:(new Array(this.count+1)).join(this.followedBy),a.adaptor.insert(b),(new r).exec(a)},a}()),G(".",z=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.isRepeatable=!1,a.prototype.exec=function(a){var b,c,d,e;b=a.lastCommand;if(!b)return;return b.switchToMode==="insert"&&(console.log("command.repeatableInsert",b.repeatableInsert),b.repeatableInsert.contiguous||(e=b.repeatableInsert.string,b=new k,b.repeatableInsert={string:e})),(d=b.selectionSize)?(d.lines?a.adaptor.makeLinewise(d.lines-1):d.chars?(a.adaptor.setSelectionAnchor(),(new s(d.chars)).exec(a)):(a.adaptor.setSelectionAnchor(),c=a.adaptor.row()+d.lineEndings,a.adaptor.moveTo(c,d.trailingChars-1)),b.visualExec(a)):b.exec(a)},a}()),G("u",C=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.isRepeatable=!1,a.prototype.exec=H(function(a){return a.adaptor.undo()}),a}()),G("x",i=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.exec=function(a){return(new h(1,new s(this.count))).exec(a)},a}()),G("X",d=function(){function a(){a.__super__.constructor.apply(this,arguments)}return L(a,g),a.prototype.exec=function(a){return(new h(1,new r(this.count))).exec(a)},a}()),c.exports={defaultMappings:F},c.exports||b}(),a["./keymap"]=function(){var b={},c={},d,e=Object.prototype.hasOwnProperty;return d=function(){function c(){this.commands={},this.motions={},this.visualCommands={},this.partialCommands={},this.partialMotions={},this.partialVisualCommands={}}var b;return c.getDefault=function(){var b,d,f,g,h,i,j,k;d=new c,i=a("./commands").defaultMappings;for(f in i){if(!e.call(i,f))continue;b=i[f],d.mapCommand(f,b)}j=a("./operators").defaultMappings;for(f in j){if(!e.call(j,f))continue;h=j[f],d.mapOperator(f,h)}k=a("./motions").defaultMappings;for(f in k){if(!e.call(k,f))continue;g=k[f],d.mapMotion(f,g)}return d},c.prototype.mapCommand=function(a,b){b.prototype.exec&&(this.commands[a]=b,a.length===2&&(this.partialCommands[a[0]]=!0));if(b.prototype.visualExec){this.visualCommands[a]=b;if(a.length===2)return this.partialVisualCommands[a[0]]=!0}},c.prototype.mapMotion=function(a,b){this.commands[a]=b,this.motions[a]=b,this.visualCommands[a]=b;if(a.length===2)return this.partialMotions[a[0]]=!0,this.partialCommands[a[0]]=!0,this.partialVisualCommands[a[0]]=!0},c.prototype.mapOperator=function(a,b){this.commands[a]=b,this.visualCommands[a]=b;if(a.length===2)return this.partialCommands[a[0]]=!0,this.partialVisualCommands[a[0]]=!0},b=function(a){var b,c;return RegExp("^([1-9]\\d*)?(["+function(){var d;d=[];for(b in a){if(!e.call(a,b))continue;c=a[b],d.push(b)}return d}().join("")+"]?([\\s\\S]*))?$")},c.prototype.commandFor=function(a){var c,d,e,f,g;return this.partialCommandRegex||(this.partialCommandRegex=b(this.partialCommands)),g=a.match(this.partialCommandRegex),a=g[0],f=g[1],d=g[2],c=g[3],c?(e=this.commands[d])?new e(parseInt(f)||null):!1:!0},c.prototype.motionFor=function(c,d){var e,f,g,h,i,j;return this.partialMotionRegex||(this.partialMotionRegex=b(this.partialMotions)),j=c.match(this.partialCommandRegex),c=j[0],g=j[1],h=j[2],f=j[3],f?h===d?(e=a("./motions").LinewiseCommandMotion,new e(parseInt(g)||null)):(i=this.motions[h])?new i(parseInt(g)||null):!1:!0},c.prototype.visualCommandFor=function(a){var c,d,e,f,g;return this.partialVisualCommandRegex||(this.partialVisualCommandRegex=b(this.partialVisualCommands)),g=a.match(this.partialVisualCommandRegex),a=g[0],f=g[1],d=g[2],c=g[3],c?(e=this.visualCommands[d])?new e(parseInt(f)||null):!1:!0},c}(),c.exports=d,c.exports||b}(),a["./modes"]=function(){var a={},b={},c;return c=function(a){return a==null&&(a="command"),console.log("invalid "+a+": "+this.commandPart),this.onEscape()},a.normal={onKeypress:function(a){var b,d,e,f,g;this.commandPart=(this.commandPart||"")+a,this.command?this.command.constructor.followedBy?(this.command.constructor.followedBy.test(this.commandPart)?this.command.followedBy=this.commandPart:console.log(""+this.command+" didn't expect to be followed by \""+this.commandPart+'"'),this.commandPart=""):this.command.isOperation&&((e=(f=this.command.motion)!=null?f.constructor.followedBy:void 0)?e.test(this.commandPart)?this.command.motion.followedBy=this.commandPart:console.log(""+this.command+" didn't expect to be followed by \""+this.commandPart+'"'):(d=this.keymap.motionFor(this.commandPart,this.operatorPending),d===!1?c.call(this,"motion"):d!==!0&&(d.operation=this.command,this.command.motion=d,this.operatorPending=null,this.commandPart=""))):(b=this.keymap.commandFor(this.commandPart),b===!1?c.call(this):b!==!0&&(b.isOperation&&(this.operatorPending=this.commandPart.match(/[^\d]+$/)[0]),this.command=b,this.commandPart=""));if((g=this.command)!=null?g.isComplete():void 0)return this.command.exec(this),this.command.isRepeatable&&(this.lastCommand=this.command),this.command=null}},a.visual={onKeypress:function(a){var b,d,e,f,g,h,i;this.commandPart=(this.commandPart||"")+a,this.command?this.command.constructor.followedBy&&(this.command.constructor.followedBy.test(this.commandPart)?this.command.followedBy=this.commandPart:console.log(""+this.command+" didn't expect to be followed by \""+this.commandPart+'"'),this.commandPart=""):(b=this.keymap.visualCommandFor(this.commandPart),b===!1?c.call(this):b!==!0&&(this.command=b,this.commandPart="")),f=this.adaptor.isSelectionBackwards();if(((g=this.command)!=null?g.isOperation:void 0)||((h=this.command)!=null?h.isComplete():void 0))this.command.isRepeatable&&(this.command.selectionSize=this.mode.name==="visual"&&this.mode.linewise?(i=this.adaptor.selectionRowRange(),e=i[0],d=i[1],i,{lines:d-e+1}):this.adaptor.characterwiseSelectionSize(),this.command.linewise=this.mode.linewise,this.lastCommand=this.command),this.command.visualExec(this),this.command=null;if(this.mode.name==="visual"&&!this.mode.linewise)if(f){if(!this.adaptor.isSelectionBackwards())return this.adaptor.adjustAnchor(-1)}else if(this.adaptor.isSelectionBackwards())return this.adaptor.adjustAnchor(1)}},a.insert={onKeypress:function(){return!0}},a.replace={onKeypress:function(){return!0}},b.exports||a}(),a["./jim"]=function(){var b={},c={},d,e,f=Object.prototype.hasOwnProperty;return e=a("./keymap"),d=function(){function b(a){this.adaptor=a,this.command=null,this.registers={},this.keymap=e.getDefault(),this.setMode("normal")}return b.VERSION="0.2.0",b.prototype.modes=a("./modes"),b.prototype.setMode=function(a,b){var c,d,e,g;this.debugMode&&console.log("setMode",a,b),d=this.mode;if(a===(d!=null?d.name:void 0)){if(!b)return;for(c in b){if(!f.call(b,c))continue;e=b[c],this.mode[c]=e}}else this.mode=b||{},this.mode.name=a;typeof (g=this.adaptor).onModeChange=="function"&&g.onModeChange(d,this.mode);switch(d!=null?d.name:void 0){case"insert":return this.adaptor.moveLeft(),this.lastCommand.repeatableInsert=this.adaptor.lastInsert();case"replace":return this.adaptor.setOverwriteMode(!1)}},b.prototype.onEscape=function(){return this.setMode("normal"),this.command=null,this.commandPart="",this.adaptor.clearSelection()},b.prototype.onKeypress=function(a){return this.modes[this.mode.name].onKeypress.call(this,a)},b.prototype.deleteSelection=function(a,b){return this.registers['"']=this.adaptor.deleteSelection(a,b)},b.prototype.yankSelection=function(a,b){return this.registers['"']=this.adaptor.selectionText(a,b),this.adaptor.clearSelection(!0)},b}(),c.exports=d,c.exports||b}(),a["./ace"]=function(){var b={},c={},d,e,f,g,h,i=Object.prototype.hasOwnProperty,j=function(a,b){function d(){this.constructor=a}for(var c in b)i.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};return g=a("ace/undomanager").UndoManager,e=a("./jim"),d=function(){function c(a){this.editor=a}var a,b;return a=function(a,b){var c,d;return d=a.selection.getSelectionLead(),c=a.selection.doc.getLine(d.row).length,d.column>=c-(b?0:1)},b=function(b){return a(b,!0)},c.prototype.onModeChange=function(a,b){var c,d,e,f;f=["insert","normal","visual"];for(d=0,e=f.length;d0)return this.editor.selection.moveCursorLeft()},c.prototype.moveRight=function(c){var d;d=c?b(this.editor):a(this.editor);if(!d)return this.editor.selection.moveCursorRight()},c.prototype.moveTo=function(a,b){return this.editor.moveCursorTo(a,b)},c.prototype.moveToLineEnd=function(){var a,b,c,d;return d=this.editor.selection.selectionLead,c=d.row,a=d.column,b=this.editor.session.getDocumentLastRowColumnPosition(c,a),this.moveTo(b.row,b.column-1)},c.prototype.moveToEndOfPreviousLine=function(){var a,b;return a=this.row()-1,b=this.editor.session.doc.getLine(a).length,this.editor.selection.moveCursorTo(a,b)},c.prototype.navigateFileEnd=function(){return this.editor.navigateFileEnd()},c.prototype.navigateLineStart=function(){return this.editor.navigateLineStart()},c.prototype.search=function(a,b,c){var d;this.editor.$search.set({backwards:a,needle:b,wholeWord:c}),a||this.editor.selection.moveCursorRight();if(d=this.editor.$search.find(this.editor.session))return this.moveTo(d.start.row,d.start.column);if(!a)return this.editor.selection.moveCursorLeft()},c.prototype.deleteSelection=function(){var a;return a=this.editor.getCopyText(),this.editor.session.remove(this.editor.getSelectionRange()),this.editor.clearSelection(),a},c.prototype.indentSelection=function(){return this.editor.indent(),this.clearSelection()},c.prototype.outdentSelection=function(){return this.editor.blockOutdent(),this.clearSelection()},c.prototype.insert=function(a,c){c&&!b(this.editor)&&this.editor.selection.moveCursorRight();if(a)return this.editor.insert(a)},c.prototype.emptySelection=function(){return this.editor.selection.isEmpty()},c.prototype.selectionText=function(){return this.editor.getCopyText()},c.prototype.setSelectionAnchor=function(){var a;return a=this.editor.selection.selectionLead,this.editor.selection.setSelectionAnchor(a.row,a.column)},c.prototype.setLinewiseSelectionAnchor=function(){var a,b,c,d,e;return d=this.editor.selection,e=d[d.isEmpty()?"selectionLead":"selectionAnchor"],c=e.row,a=e.column,b=this.editor.session.getDocumentLastRowColumnPosition(c,a),d.setSelectionAnchor(c,b),[c,a]},c.prototype.selectLineEnding=function(a){var b,c;this.editor.selection.moveCursorLineEnd(),this.editor.selection.selectRight();if(a)return b=((c=/\S/.exec(this.lineText()))!=null?c.index:void 0)||0,this.moveTo(this.row(),b)},c.prototype.selectionRowRange=function(){var a,b,c,d;return d=this.position(),c=d[0],b=d[1],a=this.editor.selection.getSelectionAnchor().row,[Math.min(c,a),Math.max(c,a)]},c.prototype.characterwiseSelectionSize=function(){var a,b,c,d;return d=this.editor.selection,b=d.selectionAnchor,c=d.selectionLead,a=c.row-b.row,a===0?{chars:Math.abs(b.column-c.column)}:{lineEndings:Math.abs(a),trailingChars:(a>0?c:b).column+1}},c}(),f=function(){function a(){a.__super__.constructor.apply(this,arguments)}return j(a,g),a.prototype.undo=function(){return this.isJimMark(this.lastOnUndoStack())&&this.silentUndo(),a.__super__.undo.apply(this,arguments)},a.prototype.isJimMark=function(a){return typeof a=="string"&&/^jim:/.test(a)},a.prototype.lastOnUndoStack=function(){return this.$undoStack[this.$undoStack.length-1]},a.prototype.silentUndo=function(){var a;a=this.$undoStack.pop();if(a)return this.$redoStack.push(a)},a.prototype.matchingMark={"jim:insert:end":"jim:insert:start","jim:replace:end":"jim:replace:start"},a.prototype.jimUndo=function(){var a,b,c,d,e;b=this.lastOnUndoStack();if(typeof b=="string"&&(d=this.matchingMark[b])){c=null;for(a=e=this.$undoStack.length-1;e<=0?a<=0:a>=0;e<=0?a++:a--)if(this.$undoStack[a]===d){c=a;break}if(c==null){console.log('found a "'+b+'" on the undoStack, but no "'+d+'"');return}this.silentUndo();while(this.$undoStack.length>c+1)this.isJimMark(this.lastOnUndoStack())?this.silentUndo():this.undo();return this.silentUndo()}return this.undo()},a.prototype.lastInsert=function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r;if(this.lastOnUndoStack()!=="jim:insert:end")return"";b=null,c=null,a=null,j=[],i=[],f=function(d){var e,f;return/(insert|remove)/.test(d.action)?!a||a===d.action?d.action==="insertText"?!b||(e=d.range).isEnd.apply(e,b):!c||(f=d.range).isStart.apply(f,c):d.action==="insertText"&&b!=null?d.range.end.row===b[0]:d.action==="removeText"&&c!=null?d.range.end.row===c[0]:!0:!1};for(e=n=this.$undoStack.length-2;n<=0?e<=0:e>=0;n<=0?e++:e--){if(typeof this.$undoStack[e]=="string")break;for(g=o=this.$undoStack[e].length-1;o<=0?g<=0:g>=0;o<=0?g++:g--)for(h=p=this.$undoStack[e][g].deltas.length-1;p<=0?h<=0:h>=0;p<=0?h++:h--){d=this.$undoStack[e][g].deltas[h];if(!f(d))return{string:j.join(""),contiguous:!1};a=d.action;if(a==="removeText"){c=[d.range.end.row,d.range.end.column],q=d.text.split("");for(l=0,m=q.length;l=0;r<=0?k++:k--)j.unshift(d.text[k])}}}return{string:j.join(""),contiguous:!0}},a}(),a("pilot/dom").importCssString(".jim-normal-mode div.ace_cursor\n, .jim-visual-mode div.ace_cursor {\n border: 0;\n background-color: #91FF00;\n opacity: 0.5;\n}\n.jim-visual-linewise-mode .ace_marker-layer .ace_selection {\n left: 0 !important;\n width: 100% !important;\n}"),h=function(a,b){return a===0&&!b},e.aceInit=function(a){var b,c,g;return a.setKeyboardHandler({handleKeyboard:function(a,b,d,e){var f;if(e===27)return c.onEscape();if(h(b,e)){c.afterInsertSwitch&&(c.mode.name==="insert"&&c.adaptor.markUndoPoint("jim:insert:afterSwitch"),c.afterInsertSwitch=!1),c.mode.name==="normal"&&!c.adaptor.emptySelection()&&c.setMode("visual"),d.length>1&&(d=d.charAt(0)),f=c.onKeypress(d);if(!f)return{command:{exec:function(){}}}}}}),g=new f,a.session.setUndoManager(g),b=new d(a),c=new e(b),b.onModeChange(null,{name:"normal"}),c},c.exports||b}(),a["./jim"]}() -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 15px; 5 | line-height: 22px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | } 9 | a { 10 | color: #261a3b; 11 | } 12 | a:visited { 13 | color: #261a3b; 14 | } 15 | p { 16 | margin: 0 0 15px 0; 17 | } 18 | h1, h2, h3, h4, h5, h6 { 19 | margin: 0px 0 15px 0; 20 | } 21 | h1 { 22 | margin-top: 40px; 23 | } 24 | #container { 25 | position: relative; 26 | } 27 | #background { 28 | position: fixed; 29 | top: 0; left: 525px; right: 0; bottom: 0; 30 | background: #f5f5ff; 31 | border-left: 1px solid #e5e5ee; 32 | z-index: -1; 33 | } 34 | #jump_to, #jump_page { 35 | background: white; 36 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 37 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 38 | font: 10px Arial; 39 | text-transform: uppercase; 40 | cursor: pointer; 41 | text-align: right; 42 | } 43 | #jump_to, #jump_wrapper { 44 | position: fixed; 45 | right: 0; top: 0; 46 | padding: 5px 10px; 47 | } 48 | #jump_wrapper { 49 | padding: 0; 50 | display: none; 51 | } 52 | #jump_to:hover #jump_wrapper { 53 | display: block; 54 | } 55 | #jump_page { 56 | padding: 5px 0 3px; 57 | margin: 0 0 25px 25px; 58 | } 59 | #jump_page .source { 60 | display: block; 61 | padding: 5px 10px; 62 | text-decoration: none; 63 | border-top: 1px solid #eee; 64 | } 65 | #jump_page .source:hover { 66 | background: #f5f5ff; 67 | } 68 | #jump_page .source:first-child { 69 | } 70 | table td { 71 | border: 0; 72 | outline: 0; 73 | } 74 | td.docs, th.docs { 75 | max-width: 450px; 76 | min-width: 450px; 77 | min-height: 5px; 78 | padding: 10px 25px 1px 50px; 79 | overflow-x: hidden; 80 | vertical-align: top; 81 | text-align: left; 82 | } 83 | .docs pre { 84 | margin: 15px 0 15px; 85 | padding-left: 15px; 86 | } 87 | .docs p tt, .docs p code { 88 | background: #f8f8ff; 89 | border: 1px solid #dedede; 90 | font-size: 12px; 91 | padding: 0 0.2em; 92 | } 93 | .pilwrap { 94 | position: relative; 95 | } 96 | .pilcrow { 97 | font: 12px Arial; 98 | text-decoration: none; 99 | color: #454545; 100 | position: absolute; 101 | top: 3px; left: -20px; 102 | padding: 1px 2px; 103 | opacity: 0; 104 | -webkit-transition: opacity 0.2s linear; 105 | } 106 | td.docs:hover .pilcrow { 107 | opacity: 1; 108 | } 109 | td.code, th.code { 110 | padding: 14px 15px 16px 25px; 111 | width: 100%; 112 | vertical-align: top; 113 | background: #f5f5ff; 114 | border-left: 1px solid #e5e5ee; 115 | } 116 | pre, tt, code { 117 | font-size: 12px; line-height: 18px; 118 | font-family: Monaco, Consolas, "Lucida Console", monospace; 119 | margin: 0; padding: 0; 120 | } 121 | 122 | 123 | /*---------------------- Syntax Highlighting -----------------------------*/ 124 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 125 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 126 | body .hll { background-color: #ffffcc } 127 | body .c { color: #408080; font-style: italic } /* Comment */ 128 | body .err { border: 1px solid #FF0000 } /* Error */ 129 | body .k { color: #954121 } /* Keyword */ 130 | body .o { color: #666666 } /* Operator */ 131 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 132 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 133 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 134 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 135 | body .gd { color: #A00000 } /* Generic.Deleted */ 136 | body .ge { font-style: italic } /* Generic.Emph */ 137 | body .gr { color: #FF0000 } /* Generic.Error */ 138 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 139 | body .gi { color: #00A000 } /* Generic.Inserted */ 140 | body .go { color: #808080 } /* Generic.Output */ 141 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 142 | body .gs { font-weight: bold } /* Generic.Strong */ 143 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 144 | body .gt { color: #0040D0 } /* Generic.Traceback */ 145 | body .kc { color: #954121 } /* Keyword.Constant */ 146 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 147 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 148 | body .kp { color: #954121 } /* Keyword.Pseudo */ 149 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 150 | body .kt { color: #B00040 } /* Keyword.Type */ 151 | body .m { color: #666666 } /* Literal.Number */ 152 | body .s { color: #219161 } /* Literal.String */ 153 | body .na { color: #7D9029 } /* Name.Attribute */ 154 | body .nb { color: #954121 } /* Name.Builtin */ 155 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 156 | body .no { color: #880000 } /* Name.Constant */ 157 | body .nd { color: #AA22FF } /* Name.Decorator */ 158 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 159 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 160 | body .nf { color: #0000FF } /* Name.Function */ 161 | body .nl { color: #A0A000 } /* Name.Label */ 162 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 163 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 164 | body .nv { color: #19469D } /* Name.Variable */ 165 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 166 | body .w { color: #bbbbbb } /* Text.Whitespace */ 167 | body .mf { color: #666666 } /* Literal.Number.Float */ 168 | body .mh { color: #666666 } /* Literal.Number.Hex */ 169 | body .mi { color: #666666 } /* Literal.Number.Integer */ 170 | body .mo { color: #666666 } /* Literal.Number.Oct */ 171 | body .sb { color: #219161 } /* Literal.String.Backtick */ 172 | body .sc { color: #219161 } /* Literal.String.Char */ 173 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 174 | body .s2 { color: #219161 } /* Literal.String.Double */ 175 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 176 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 177 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 178 | body .sx { color: #954121 } /* Literal.String.Other */ 179 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 180 | body .s1 { color: #219161 } /* Literal.String.Single */ 181 | body .ss { color: #19469D } /* Literal.String.Symbol */ 182 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 183 | body .vc { color: #19469D } /* Name.Variable.Class */ 184 | body .vg { color: #19469D } /* Name.Variable.Global */ 185 | body .vi { color: #19469D } /* Name.Variable.Instance */ 186 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/helpers.html: -------------------------------------------------------------------------------- 1 | helpers.coffee

helpers.coffee

Define the base class for all commands.

class exports.Command
 2 |   constructor: (@count = 1) ->
 3 |   isRepeatable: yes

If the class specifies a regex for char(s) that should follow the command, 4 | then the command isn't complete until those char(s) have been matched.

  isComplete: ->
 5 |     if @constructor.followedBy then @followedBy else true

A bunch of commands can just repeat an action however many times their @count 6 | specifies. For example 5x does exactly the same thing as pressing x five times. 7 | This helper is used for that case.

exports.repeatCountTimes = (func) ->
 8 |   (jim) ->
 9 |     timesLeft = @count
10 |     func.call this, jim while timesLeft--
11 | 
12 | 
-------------------------------------------------------------------------------- /docs/jim.html: -------------------------------------------------------------------------------- 1 | jim.coffee

jim.coffee

An instance of Jim holds all the Jim-specific state of the editor: the 2 | current command, the current mode, the values of all the registers, etc. It 3 | also holds a reference to the adaptor that is doing its bidding in the editor. 4 | Commands are passed an instance of Jim when they are executed which allows 5 | them to change Jim's state and manipulate the editor (through the @adaptor).

Keymap     = require './keymap'
 6 | {GoToLine} = require './motions'
 7 | 
 8 | class Jim
 9 |   @VERSION: '0.2.0-pre'
10 | 
11 |   constructor: (@adaptor) ->
12 |     @command = null
13 |     @registers = {}
14 |     @keymap = Keymap.getDefault()
15 |     @setMode 'normal'
16 | 
17 |   modes: require './modes'

Change Jim's mode to modeName with optional modeState:

18 | 19 |
jim.setMode 'visual', linewise: yes
20 | 
  setMode: (modeName, modeState) ->
21 |     console.log 'setMode', modeName, modeState if @debugMode
22 |     prevMode = @mode
23 |     if modeName is prevMode?.name
24 |       return unless modeState
25 |       @mode[key] = value for own key, value of modeState
26 |     else
27 |       @mode = modeState or {}
28 |       @mode.name = modeName
29 |       
30 |     @adaptor.onModeChange? prevMode, @mode
31 | 
32 |     switch prevMode?.name
33 |       when 'insert'
34 |         @adaptor.moveLeft()

Get info about what was inserted so the insert "remembers" how to 35 | repeat itself.

        @lastCommand.repeatableInsert = @adaptor.lastInsert()
36 | 
37 |       when 'replace'
38 |         @adaptor.setOverwriteMode off

Pressing escape blows away all the state.

  onEscape: ->
39 |     @setMode 'normal'
40 |     @command = null
41 |     @commandPart = '' # just in case...
42 |     @adaptor.clearSelection()

When a key is pressed, let the current mode figure out what to do about it.

  onKeypress: (keys) -> @modes[@mode.name].onKeypress.call this, keys

Delete the selected text and put it in the default register.

  deleteSelection: (exclusive, linewise) ->
43 |     @registers['"'] = @adaptor.deleteSelection exclusive, linewise
44 |     

Yank the selected text into the default register.

  yankSelection: (exclusive, linewise) ->
45 |     @registers['"'] = @adaptor.selectionText exclusive, linewise
46 |     @adaptor.clearSelection true
47 | 
48 | module.exports = Jim
49 | 
50 | 
-------------------------------------------------------------------------------- /docs/keymap.html: -------------------------------------------------------------------------------- 1 | keymap.coffee

keymap.coffee

This is a pretty standard key-to-command keymap except for a few details:

2 | 3 |
    4 |
  • It has some built-in Vim-like smarts about the concepts of motions and 5 | operators and if/how they should be available in each mode
  • 6 |
  • It differentiates between invalid commands (gz) and partial commands (g)
  • 7 |
class Keymap

Building a Keymap

Build an instance of Keymap with all the default keymappings.

  @getDefault: ->
  8 |     keymap = new Keymap
  9 |     keymap.mapCommand keys, commandClass for own keys, commandClass of require('./commands').defaultMappings
 10 |     keymap.mapOperator keys, operationClass for own keys, operationClass of require('./operators').defaultMappings
 11 |     keymap.mapMotion keys, motionClass for own keys, motionClass of require('./motions').defaultMappings
 12 |     keymap
 13 | 
 14 |   constructor: ->
 15 |     @commands = {}
 16 |     @motions = {}
 17 |     @visualCommands = {}

Use some objects to de-duplicate repeated partial commands.

    @partialCommands = {}
 18 |     @partialMotions = {}
 19 |     @partialVisualCommands = {}

Mapping commands

Map the comandClass to the keys sequence. Map it as a visual command as well 20 | if the class has a ::visualExec.

  mapCommand: (keys, commandClass) ->
 21 |     if commandClass::exec
 22 |       @commands[keys] = commandClass
 23 |       if keys.length is 2
 24 |         @partialCommands[keys[0]] = true
 25 |     if commandClass::visualExec
 26 |       @visualCommands[keys] = commandClass
 27 |       if keys.length is 2
 28 |         @partialVisualCommands[keys[0]] = true

Map motionClass to the keys sequence.

  mapMotion: (keys, motionClass) ->
 29 |     @commands[keys] = motionClass
 30 |     @motions[keys] = motionClass
 31 |     @visualCommands[keys] = motionClass
 32 |     if keys.length is 2
 33 |       @partialMotions[keys[0]] = true
 34 |       @partialCommands[keys[0]] = true
 35 |       @partialVisualCommands[keys[0]] = true

Map operatorClass to the keys sequence.

  mapOperator: (keys, operatorClass) ->
 36 |     @commands[keys] = operatorClass
 37 |     @visualCommands[keys] = operatorClass
 38 |     if keys.length is 2
 39 |       @partialCommands[keys[0]] = true
 40 |       @partialVisualCommands[keys[0]] = true

Finding commands in the Keymap

41 | 42 |

commandFor, motionFor, and visualCommandFor are defined for finding 43 | their respective Command types. Each of these methods will return one of the 44 | following:

45 | 46 |
    47 |
  • true if the commandPart passed in is a valid partial command. For 48 | example, Keymap.getDefault().commandFor('g') will return true because 49 | it is the first part of what could be the valid command gg, among 50 | others.
  • 51 |
  • false if the commandPart is not a valid partial or complete command.
  • 52 |
  • A Command if the commandPart is a valid, complete command. The 53 | Command will have it's count populated if commandPart includes a 54 | count.
  • 55 |

Build a regex that will help us split up the commandPart in each of the 56 | following methods. The regex will match any key sequence, splitting it into 57 | the following captured groups:

58 | 59 |
    60 |
  1. The preceding count
  2. 61 |
  3. The command/motion/operator
  4. 62 |
  5. Any chars beyond a partial command/motion/operator. If this group 63 | captures anything, we can stop accepting keystrokes for the command and 64 | execute it if it's valid.
  6. 65 |
  buildPartialCommandRegex = (partialCommands) ->
 66 |     ///
 67 |       ^
 68 |       ([1-9]\d*)?
 69 |       (
 70 |         [#{(char for own char, nothing of partialCommands).join ''}]?
 71 |         ([\s\S]*)
 72 |       )?
 73 |       $
 74 |     ///

Find a normal mode command, which could be a motion, an operator, or a 75 | "regular" normal mode command.

  commandFor: (commandPart) ->
 76 |     @partialCommandRegex or= buildPartialCommandRegex @partialCommands
 77 |     [commandPart, count, command, beyondPartial] = commandPart.match @partialCommandRegex
 78 | 
 79 |     if beyondPartial
 80 |       if commandClass = @commands[command]
 81 |         new commandClass(parseInt(count) or null)
 82 |       else
 83 |         false
 84 |     else
 85 |       true

Find a motion.

  motionFor: (commandPart, operatorPending) ->
 86 |     @partialMotionRegex or= buildPartialCommandRegex @partialMotions
 87 |     [commandPart, count, motion, beyondPartial] = commandPart.match @partialCommandRegex
 88 | 
 89 |     if beyondPartial
 90 |       if motion is operatorPending

If we're finding cc, yy, etc, we return a "fake" linewise command.

        {LinewiseCommandMotion} = require './motions'
 91 |         new LinewiseCommandMotion(parseInt(count) or null)
 92 | 
 93 |       else if motionClass = @motions[motion]
 94 |         new motionClass(parseInt(count) or null)
 95 |       else
 96 |         false
 97 |     else
 98 |       true

Find a visual mode command, which could be a motion, an operator, or a 99 | "regular" visual mode command.

  visualCommandFor: (commandPart) ->
100 |     @partialVisualCommandRegex or= buildPartialCommandRegex @partialVisualCommands
101 |     [commandPart, count, command, beyondPartial] = commandPart.match @partialVisualCommandRegex
102 | 
103 |     if beyondPartial
104 |       if commandClass = @visualCommands[command]
105 |         new commandClass(parseInt(count) or null)
106 |       else
107 |         false
108 |     else
109 |       true

Exports

module.exports = Keymap
110 | 
111 | 
-------------------------------------------------------------------------------- /docs/modes.html: -------------------------------------------------------------------------------- 1 | modes.coffee

modes.coffee

Each mode handles key presses a bit differently. For instance, typing an 2 | operator in visual mode immediately operates on the selected text. In normal 3 | mode Jim waits for a motion to follow the operator. All of the modes' 4 | keyboard handling is defined here.

5 | 6 |

Each mode's onkeypress is executed in the context of an instance of Jim. 7 | In normal and visual mode the current @commandPart is the current part of 8 | the command that's being typed. For an operation, the operator is one part 9 | and the motion is another. @commandPart can one of the following:

10 | 11 |
    12 |
  • {count}command
  • 13 |
  • {count}motion
  • 14 |
  • {count}operator
  • 15 |
  • chars expected to follow a command (e.g. when r is pressed, the next 16 | @commandPart will be the char that's used as the replacement)
  • 17 |
{MoveLeft, MoveDown} = require './motions'

Shame the user in the console for not knowing their Jim commands.

invalidCommand = (type = 'command') ->
 18 |   console.log "invalid #{type}: #{@commandPart}"
 19 |   @onEscape()

Normal mode (a.k.a. "command mode")

exports.normal =
 20 |   onKeypress: (keys) ->
 21 |     @commandPart = (@commandPart or '') + keys
 22 | 
 23 |     if not @command
 24 |       command = @keymap.commandFor @commandPart
 25 | 
 26 |       if command is false
 27 |         invalidCommand.call this
 28 |       else if command isnt true
 29 |         if command.isOperation

Hang onto the pending operator so that double-operators can 30 | recognized (cc, yy, etc).

          [@operatorPending] = @commandPart.match /[^\d]+$/
 31 | 
 32 |         @command = command
 33 |         @commandPart = ''
 34 |     else if @command.constructor.followedBy

If we've got a command that expects a key to follow it, check if 35 | @commandPart is what it's expecting.

      if @command.constructor.followedBy.test @commandPart
 36 |         @command.followedBy = @commandPart
 37 |       else
 38 |         console.log "#{@command} didn't expect to be followed by \"#{@commandPart}\""
 39 | 
 40 |       @commandPart = ''
 41 |     else if @command.isOperation
 42 |       if regex = @command.motion?.constructor.followedBy

If we've got a motion that expects a key to follow it, check if 43 | @commandPart is what it's expecting.

        if regex.test @commandPart
 44 |           @command.motion.followedBy = @commandPart
 45 |         else
 46 |           console.log "#{@command} didn't expect to be followed by \"#{@commandPart}\""
 47 | 
 48 |       else
 49 |         motion = @keymap.motionFor @commandPart, @operatorPending
 50 | 
 51 |         if motion is false
 52 |           invalidCommand.call this, 'motion'
 53 |         else if motion isnt true

Motions need a reference to the operation they're a part of since it 54 | sometimes changes the amount of text they move over (e.g. cw 55 | deletes less text than dw).

          motion.operation = @command
 56 | 
 57 |           @command.motion = motion
 58 |           @operatorPending = null
 59 |           @commandPart = ''

Execute the command if it's complete, otherwise wait for more keys.

    if @command?.isComplete()
 60 |       @command.exec this
 61 |       @lastCommand = @command if @command.isRepeatable
 62 |       @command = null

Visual mode

exports.visual =
 63 |   onKeypress: (newKeys) ->
 64 |     @commandPart = (@commandPart or '') + newKeys
 65 | 
 66 |     if not @command
 67 |       command = @keymap.visualCommandFor @commandPart
 68 | 
 69 |       if command is false
 70 |         invalidCommand.call this
 71 |       else if command isnt true
 72 |         @command = command
 73 |         @commandPart = ''
 74 |     else if @command.constructor.followedBy

If we've got a motion that expects a key to follow it, check if 75 | @commandPart is what it's expecting.

      if @command.constructor.followedBy.test @commandPart
 76 |         @command.followedBy = @commandPart
 77 |       else
 78 |         console.log "#{@command} didn't expect to be followed by \"#{@commandPart}\""
 79 |       @commandPart = ''
 80 | 
 81 |     wasBackwards = @adaptor.isSelectionBackwards()

Operations are always "complete" in visual mode.

    if @command?.isOperation or @command?.isComplete()
 82 |       if @command.isRepeatable

Save the selection's "size", which will be used if the command is 83 | repeated.

        @command.selectionSize = if @mode.name is 'visual' and @mode.linewise
 84 |           [minRow, maxRow] = @adaptor.selectionRowRange()
 85 |           lines: (maxRow - minRow) + 1
 86 |         else
 87 |           @adaptor.characterwiseSelectionSize()
 88 |         @command.linewise = @mode.linewise
 89 | 
 90 |         @lastCommand = @command
 91 | 
 92 |       @command.visualExec this
 93 |       @command = null

If we haven't changed out of characterwise visual mode and the direction 94 | of the selection changes, we have to make sure that the anchor character 95 | stays selected.

    if @mode.name is 'visual' and not @mode.linewise
 96 |       if wasBackwards
 97 |         @adaptor.adjustAnchor -1 if not @adaptor.isSelectionBackwards()
 98 |       else
 99 |         @adaptor.adjustAnchor 1 if @adaptor.isSelectionBackwards()

Other modes

100 | 101 |

Insert and replace modes just pass all keystrokes through (except <esc>).

exports.insert = onKeypress: -> true
102 | exports.replace = onKeypress: -> true
103 | 
104 | 
-------------------------------------------------------------------------------- /docs/operators.html: -------------------------------------------------------------------------------- 1 | operators.coffee

operators.coffee

An operator followed by a motion is an Operation. For example, ce changes 2 | all the text to the end of the current word since c is the change operator 3 | and e is a motion that moves to the end of the word.

{Command} = require './helpers'
 4 | {GoToLine, MoveToFirstNonBlank} = require './motions'

The default key mappings are specified alongside the definitions of each 5 | Operation. Accumulate the mappings so they can be exported.

defaultMappings = {}
 6 | map = (keys, operationClass) -> defaultMappings[keys] = operationClass

Define the base class for all operations.

class Operation extends Command
 7 |   constructor: (@count = 1, @motion) ->
 8 |     @motion.operation = this if @motion
 9 |   isOperation: true
10 |   isComplete: -> @motion?.isComplete()
11 |   switchToMode: 'normal'

Adjust the selection, if needed, and operate on that selection.

  visualExec: (jim) ->
12 |     if @linewise
13 |       jim.adaptor.makeLinewise()
14 |     else if not @motion?.exclusive
15 |       jim.adaptor.includeCursorInSelection()
16 | 
17 |     @operate jim
18 | 
19 |     jim.setMode @switchToMode

Select the amount of text that the motion moves over and operate on that 20 | selection.

  exec: (jim) ->
21 |     @startingPosition = jim.adaptor.position()
22 |     jim.adaptor.setSelectionAnchor()
23 |     if @count isnt 1
24 |       @motion.count *= @count
25 |       @count = 1
26 |     @linewise ?= @motion.linewise
27 |     @motion.exec jim
28 |     @visualExec jim

Change the selected text or the text that @motion moves over (i.e. delete 29 | the text and switch to insert mode).

map 'c', class Change extends Operation
30 |   visualExec: (jim) ->
31 |     super

If we're repeating a Change, insert the text that was inserted now that 32 | we've deleted the selected text.

    if @repeatableInsert
33 |       jim.adaptor.insert @repeatableInsert.string
34 |       jim.setMode 'normal'

If we're executing this Change for the first time, set a flag so that an 35 | undo mark can be pushed onto the undo stack before any text is inserted.

    else
36 |       jim.afterInsertSwitch = true
37 | 
38 |   operate: (jim) ->

If we're changing a linewise selection or motion, move the end of the 39 | previous line so that the cursor is left on an open line once the lines 40 | are deleted.

    jim.adaptor.moveToEndOfPreviousLine() if @linewise
41 | 
42 |     jim.deleteSelection @motion?.exclusive, @linewise
43 | 
44 |   switchToMode: 'insert'

Delete the selection or the text that @motion moves over.

map 'd', class Delete extends Operation
45 |   operate: (jim) ->
46 |     jim.deleteSelection @motion?.exclusive, @linewise
47 |     new MoveToFirstNonBlank().exec jim if @linewise

Yank into a register the selection or the text that @motion moves over.

map 'y', class Yank extends Operation
48 |   operate: (jim) ->
49 |     jim.yankSelection @motion?.exclusive, @linewise
50 |     jim.adaptor.moveTo @startingPosition... if @startingPosition

Indent the lines in the selection or the text that @motion moves over.

map '>', class Indent extends Operation
51 |   operate: (jim) ->
52 |     [minRow, maxRow] = jim.adaptor.selectionRowRange()
53 |     jim.adaptor.indentSelection()
54 |     new GoToLine(minRow + 1).exec jim

Outdent the lines in the selection or the text that @motion moves over.

map '<', class Outdent extends Operation
55 |   operate: (jim) ->
56 |     [minRow, maxRow] = jim.adaptor.selectionRowRange()
57 |     jim.adaptor.outdentSelection()
58 |     new GoToLine(minRow + 1).exec jim
59 | 
60 | module.exports = {Change, Delete, defaultMappings}
61 | 
62 | 
-------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Jim!!!11 4 | 5 | 30 | 31 | 32 |

33 | This is a Vim mode for Ace powered by Jim. To use Jim in Github's editor, drag this bookmarklet to your bookmarks bar: Please wait... 34 |

35 | 36 |
37 |
 38 | _.sortBy = function(obj, iterator, context) {
 39 |   return _.pluck(_.map(obj, function(value, index, list) {
 40 |     return {
 41 |       value : value,
 42 |       criteria : iterator.call(context, value, index, list)
 43 |     };
 44 |   }).sort(function(left, right) {
 45 |     var a = left.criteria, b = right.criteria;
 46 |     return a < b ? -1 : a > b ? 1 : 0;
 47 |   }), 'value');
 48 | };
 49 | 
 50 | // borrowed from:
 51 | //     Underscore.js 1.1.6
 52 | //     (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
 53 | 
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 77 | 78 | 79 | 80 | 96 | 97 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/ace.coffee: -------------------------------------------------------------------------------- 1 | # All of Jim's Ace-specific code is in here. The idea is that an `Adaptor` for 2 | # another editor could be written that implemented the same methods and presto! 3 | # Jim works in that editor, too! It's probably not that simple, but we'll find 4 | # out... patches welcome :) 5 | 6 | {UndoManager} = require 'ace/undomanager' 7 | Jim = require './jim' 8 | 9 | # Ace's editor adaptor 10 | # -------------------- 11 | # 12 | # Each instance of `Jim` has an instance of an `Adaptor` on which it invokes 13 | # methods to move the cursor, change some text, etc. 14 | class Adaptor 15 | # Constuct an `Adaptor` with instance of Ace's `Editor` 16 | constructor: (@editor) -> 17 | 18 | # Return true if the cursor is on or beyond the last character of the line. If 19 | # `beyond` is true, return true only if the cursor is beyond the last char. 20 | atLineEnd = (editor, beyond) -> 21 | selectionLead = editor.selection.getSelectionLead() 22 | lineLength = editor.selection.doc.getLine(selectionLead.row).length 23 | selectionLead.column >= lineLength - (if beyond then 0 else 1) 24 | 25 | beyondLineEnd = (editor) -> atLineEnd(editor, true) 26 | 27 | # Whenever Jim's mode changes, update the editor's `className` and push a 28 | # "bookmark" onto the undo stack, if needed (explained below). 29 | onModeChange: (prevMode, newMode) -> 30 | for mode in ['insert', 'normal', 'visual'] 31 | @editor[if mode is newMode.name then 'setStyle' else 'unsetStyle'] "jim-#{mode}-mode" 32 | 33 | @editor[if newMode.name is 'visual' and newMode.linewise then 'setStyle' else 'unsetStyle'] 'jim-visual-linewise-mode' 34 | 35 | if newMode.name is 'insert' 36 | @markUndoPoint 'jim:insert:start' 37 | else if prevMode?.name is 'insert' 38 | @markUndoPoint 'jim:insert:end' 39 | 40 | if newMode.name is 'replace' 41 | @markUndoPoint 'jim:replace:start' 42 | else if prevMode?.name is 'replace' 43 | @markUndoPoint 'jim:replace:end' 44 | 45 | # Vim's undo is particularly useful because it's idea of an atomic edit is 46 | # clear to the user. One `Command` is undone each time `u` is pressed. That 47 | # means all text entered between hitting `i` and hitting `` is undone as 48 | # one atomic edit. 49 | # 50 | # To match Vim's undo granularity, Jim pushes "bookmarks" onto the undo stack 51 | # to indicate when an insert starts or ends, for example. This helps us avoid 52 | # having to record all keystrokes made while in insert or replace mode. 53 | markUndoPoint: (markName) -> 54 | @editor.session.getUndoManager().execute args: [markName, @editor.session] 55 | 56 | # Turns overwrite mode on or off (used for Jim's replace mode). 57 | setOverwriteMode: (active) -> @editor.setOverwrite active 58 | 59 | # Clears the selection, optionally positioning the cursor at its beginning. 60 | clearSelection: (beginning) -> 61 | if beginning and not @editor.selection.isBackwards() 62 | {row, column} = @editor.selection.getSelectionAnchor() 63 | @editor.navigateTo row, column 64 | else 65 | @editor.clearSelection() 66 | 67 | # Undo the last `Command`. 68 | undo: -> 69 | undoManager = @editor.session.getUndoManager() 70 | undoManager.jimUndo() 71 | @editor.clearSelection() 72 | 73 | # Get information about the last insert `Command`. See 74 | # `JimUndoManager::lastInsert`. 75 | lastInsert: -> @editor.session.getUndoManager().lastInsert() 76 | 77 | # Define methods for getting the cursor's position in the document. 78 | column: -> @editor.selection.selectionLead.column 79 | row: -> @editor.selection.selectionLead.row 80 | position: -> [@row(), @column()] 81 | 82 | # Return the first row that is fully visible in the viewport. 83 | firstFullyVisibleRow: -> @editor.renderer.getFirstFullyVisibleRow() 84 | 85 | # Return the last row in the document that is fully visible in the viewport. 86 | lastFullyVisibleRow: -> 87 | lastVisibleRow = @editor.renderer.getLastFullyVisibleRow() 88 | Math.min @lastRow(), lastVisibleRow 89 | 90 | # Before we act on a non-backwards selection, Jim's block cursor is not 91 | # considered by Ace to be part of the selection. Make the cursor part of the 92 | # selection before we act on it. 93 | includeCursorInSelection: -> 94 | if not @editor.selection.isBackwards() 95 | @editor.selection.selectRight() unless beyondLineEnd(@editor) 96 | 97 | # Insert a new line at a zero-based row number. 98 | insertNewLine: (row) -> 99 | @editor.session.doc.insertNewLine row: row, column: 0 100 | 101 | # Move the anchor by `columnOffset` columns, which can be negative. 102 | adjustAnchor: (columnOffset) -> 103 | {row, column} = @editor.selection.getSelectionAnchor() 104 | @editor.selection.setSelectionAnchor row, column + columnOffset 105 | 106 | # Is the anchor ahead of the cursor? 107 | isSelectionBackwards: -> @editor.selection.isBackwards() 108 | 109 | # Return the last zero-based row number. 110 | lastRow: -> @editor.session.getDocument().getLength() - 1 111 | 112 | # Return the text that's on `lineNumber` or the current line. 113 | lineText: (lineNumber) -> @editor.selection.doc.getLine lineNumber ? @row() 114 | 115 | # Make a linewise selection `lines` long if specified or make the current 116 | # selection linewise by pushing the lead and the anchor to the ends of their 117 | # lines. 118 | makeLinewise: (lines) -> 119 | {selectionAnchor: {row: anchorRow}, selectionLead: {row: leadRow}} = @editor.selection 120 | [firstRow, lastRow] = if lines? 121 | [leadRow, leadRow + (lines - 1)] 122 | else 123 | [Math.min(anchorRow, leadRow), Math.max(anchorRow, leadRow)] 124 | @editor.selection.setSelectionAnchor firstRow, 0 125 | @editor.selection.moveCursorTo lastRow + 1, 0 126 | 127 | # Define basic directional movements. These won't clear the selection. 128 | moveUp: -> @editor.selection.moveCursorBy -1, 0 129 | moveDown: -> @editor.selection.moveCursorBy 1, 0 130 | moveLeft: -> 131 | if @editor.selection.selectionLead.getPosition().column > 0 132 | @editor.selection.moveCursorLeft() 133 | moveRight: (beyond) -> 134 | dontMove = if beyond then beyondLineEnd(@editor) else atLineEnd(@editor) 135 | @editor.selection.moveCursorRight() unless dontMove 136 | 137 | # Move to a zero-based `row` and `column`. 138 | moveTo: (row, column) -> @editor.moveCursorTo row, column 139 | 140 | # Put the cursor on the last column of the line. 141 | moveToLineEnd: -> 142 | {row, column} = @editor.selection.selectionLead 143 | position = @editor.session.getDocumentLastRowColumnPosition row, column 144 | @moveTo position.row, position.column - 1 145 | moveToEndOfPreviousLine: -> 146 | previousRow = @row() - 1 147 | previousRowLength = @editor.session.doc.getLine(previousRow).length 148 | @editor.selection.moveCursorTo previousRow, previousRowLength 149 | 150 | # Move to first or last line. 151 | navigateFileEnd: -> @editor.navigateFileEnd() 152 | navigateLineStart: -> @editor.navigateLineStart() 153 | 154 | # Move the cursor to the fist char of the matching search or don't move at 155 | # all. 156 | search: (backwards, needle, wholeWord) -> 157 | @editor.$search.set {backwards, needle, wholeWord} 158 | 159 | # Move the cursor right so that it won't match what's already under the 160 | # cursor. Move the cursor back afterwards if nothing's found. 161 | @editor.selection.moveCursorRight() unless backwards 162 | 163 | if range = @editor.$search.find @editor.session 164 | @moveTo range.start.row, range.start.column 165 | else if not backwards 166 | @editor.selection.moveCursorLeft() 167 | 168 | # Delete selected text and return it as a string. 169 | deleteSelection: -> 170 | yank = @editor.getCopyText() 171 | @editor.session.remove @editor.getSelectionRange() 172 | @editor.clearSelection() 173 | yank 174 | 175 | indentSelection: -> 176 | @editor.indent() 177 | @clearSelection() 178 | 179 | outdentSelection: -> 180 | @editor.blockOutdent() 181 | @clearSelection() 182 | 183 | # Insert `text` before or after the cursor. 184 | insert: (text, after) -> 185 | @editor.selection.moveCursorRight() if after and not beyondLineEnd(@editor) 186 | @editor.insert text if text 187 | 188 | emptySelection: -> @editor.selection.isEmpty() 189 | 190 | selectionText: -> @editor.getCopyText() 191 | 192 | # Set the selection anchor to the cusor's current position. 193 | setSelectionAnchor: -> 194 | lead = @editor.selection.selectionLead 195 | @editor.selection.setSelectionAnchor lead.row, lead.column 196 | 197 | # Jim's linewise selections are really just regular selections with a CSS 198 | # width of `100%`. Before a visual command is exececuted the selection is 199 | # actually made linewise. Because of this, it only matters what line the 200 | # anchor is on. Therefore, we "hide" the anchor at the end of the line 201 | # where Jim's cursor won't go so that Ace doesn't remove the selection 202 | # elements from the DOM (which happens when the cursor and the anchor are 203 | # in the same place). It's a wierd hack, but it works. There was a 204 | # [github issue](https://github.com/misfo/jim/issues/5) for this. 205 | setLinewiseSelectionAnchor: -> 206 | {selection} = @editor 207 | {row, column} = selection[if selection.isEmpty() then 'selectionLead' else 'selectionAnchor'] 208 | lastColumn = @editor.session.getDocumentLastRowColumnPosition row, column 209 | selection.setSelectionAnchor row, lastColumn 210 | [row, column] 211 | 212 | 213 | # Select the line ending at the end of the current line and any whitespace at 214 | # the beginning of the next line if `andFollowingWhitespace` is specified. 215 | # This is used for the line joining commands `gJ` and `J`. 216 | selectLineEnding: (andFollowingWhitespace) -> 217 | @editor.selection.moveCursorLineEnd() 218 | @editor.selection.selectRight() 219 | if andFollowingWhitespace 220 | firstNonBlank = /\S/.exec(@lineText())?.index or 0 221 | @moveTo @row(), firstNonBlank 222 | 223 | # Return the first and the last line that are part of the current selection. 224 | selectionRowRange: -> 225 | [cursorRow, cursorColumn] = @position() 226 | {row: anchorRow} = @editor.selection.getSelectionAnchor() 227 | [Math.min(cursorRow, anchorRow), Math.max(cursorRow, anchorRow)] 228 | 229 | # Return the number of chars selected if the selection is one row. If the 230 | # selection is multiple rows, return the number of line endings selected 231 | # and the number of chars selected on the last row of the selection. 232 | characterwiseSelectionSize: -> 233 | {selectionAnchor, selectionLead} = @editor.selection 234 | rowsDown = selectionLead.row - selectionAnchor.row 235 | if rowsDown is 0 236 | chars: Math.abs(selectionAnchor.column - selectionLead.column) 237 | else 238 | lineEndings: Math.abs(rowsDown) 239 | trailingChars: (if rowsDown > 0 then selectionLead else selectionAnchor).column + 1 240 | 241 | 242 | # Jim's undo manager 243 | # ------------------ 244 | # 245 | # Ace's `UndoManager` is extended to handle undoing and repeating switches to 246 | # insert and replace mode. 247 | class JimUndoManager extends UndoManager 248 | # Override Ace's default `undo` so that the default undo button and keyboard 249 | # shortcut will skip over Jim's bookmarks and behave as they usually do. 250 | undo: -> 251 | @silentUndo() if @isJimMark @lastOnUndoStack() 252 | super 253 | 254 | # Is this a bookmark we pushed onto the stack or an actual Ace undo entry? 255 | isJimMark: (entry) -> 256 | typeof entry is 'string' and /^jim:/.test entry 257 | 258 | lastOnUndoStack: -> @$undoStack[@$undoStack.length-1] 259 | 260 | # Pop the item off the stack without doing anything with it. 261 | silentUndo: -> 262 | deltas = @$undoStack.pop() 263 | @$redoStack.push deltas if deltas 264 | 265 | matchingMark: 266 | 'jim:insert:end': 'jim:insert:start' 267 | 'jim:replace:end': 'jim:replace:start' 268 | 269 | # If the last command was an insert or a replace ensure that all undo items 270 | # associated with that command are undone. If not, just do a regular ace 271 | # undo. 272 | jimUndo: -> 273 | lastDeltasOnStack = @lastOnUndoStack() 274 | if typeof lastDeltasOnStack is 'string' and startMark = @matchingMark[lastDeltasOnStack] 275 | startIndex = null 276 | for i in [(@$undoStack.length-1)..0] 277 | if @$undoStack[i] is startMark 278 | startIndex = i 279 | break 280 | 281 | if not startIndex? 282 | console.log "found a \"#{lastDeltasOnStack}\" on the undoStack, but no \"#{startMark}\"" 283 | return 284 | 285 | @silentUndo() # pop the end off 286 | while @$undoStack.length > startIndex + 1 287 | if @isJimMark @lastOnUndoStack() 288 | @silentUndo() 289 | else 290 | @undo() 291 | @silentUndo() # pop the start off 292 | else 293 | @undo() 294 | 295 | # If the last command was an insert, return all text that was inserted taking 296 | # backspaces into account. 297 | # 298 | # If the cursor moved partway through the insert (with arrow keys or with the 299 | # mouse), then only the last peice of contiguously inserted text is returned 300 | # and `contiguous` is returned as `false`. This is to match Vim's behavior 301 | # when repeating non-contiguous inserts. 302 | lastInsert: -> 303 | return '' if @lastOnUndoStack() isnt 'jim:insert:end' 304 | 305 | cursorPosInsert = null 306 | cursorPosRemove = null 307 | action = null 308 | stringParts = [] 309 | removedParts = [] 310 | isContiguous = (delta) -> 311 | return false unless /(insert|remove)/.test delta.action 312 | if not action or action is delta.action 313 | if delta.action is 'insertText' 314 | not cursorPosInsert or delta.range.isEnd cursorPosInsert... 315 | else 316 | not cursorPosRemove or delta.range.isStart cursorPosRemove... 317 | else 318 | if delta.action is 'insertText' and cursorPosInsert? 319 | delta.range.end.row is cursorPosInsert[0] 320 | else if delta.action is 'removeText' and cursorPosRemove? 321 | delta.range.end.row is cursorPosRemove[0] 322 | else 323 | true 324 | 325 | for i in [(@$undoStack.length - 2)..0] 326 | break if typeof @$undoStack[i] is 'string' 327 | for j in [(@$undoStack[i].length - 1)..0] 328 | for k in [(@$undoStack[i][j].deltas.length - 1)..0] 329 | delta = @$undoStack[i][j].deltas[k] 330 | if isContiguous(delta) 331 | action = delta.action 332 | if action is 'removeText' 333 | cursorPosRemove = [delta.range.end.row, delta.range.end.column] 334 | for text in delta.text.split('') 335 | removedParts.push text 336 | 337 | if action is 'insertText' 338 | cursorPosInsert = [delta.range.start.row, delta.range.start.column] 339 | continue if removedParts.length and delta.text is removedParts.pop() 340 | for text in [(delta.text.length - 1)..0] 341 | stringParts.unshift delta.text[text] 342 | else 343 | return string: stringParts.join(''), contiguous: false 344 | string: stringParts.join(''), contiguous: true 345 | 346 | 347 | # Cursor and selection styles 348 | # --------------------------- 349 | # 350 | # Make Ace's cursor be block-style when Jim is in normal mode and make 351 | # selections span the editor's entire width when in linewise visual mode. 352 | require('pilot/dom').importCssString """ 353 | .jim-normal-mode div.ace_cursor 354 | , .jim-visual-mode div.ace_cursor { 355 | border: 0; 356 | background-color: #91FF00; 357 | opacity: 0.5; 358 | } 359 | .jim-visual-linewise-mode .ace_marker-layer .ace_selection { 360 | left: 0 !important; 361 | width: 100% !important; 362 | } 363 | """ 364 | 365 | 366 | # Hooking into Ace 367 | # ---------------- 368 | 369 | # Set up Jim to handle the Ace `editor`'s keyboard events. 370 | Jim.aceInit = (editor) -> 371 | editor.setKeyboardHandler 372 | handleKeyboard: (data, hashId, keyString, keyCode) -> 373 | 374 | # `esc` or `ctrl-[` were pressed. 375 | if keyCode is 27 or (hashId is 1 and keyString is '[') 376 | jim.onEscape() 377 | 378 | # If no modifiers were pressed. 379 | else if hashId is 0 380 | 381 | # We've made some deletion as part of a change operation already and 382 | # we're about to start the actual insert. Mark this moment in the undo 383 | # stack. 384 | if jim.afterInsertSwitch 385 | if jim.mode.name is 'insert' 386 | jim.adaptor.markUndoPoint 'jim:insert:afterSwitch' 387 | jim.afterInsertSwitch = false 388 | 389 | if jim.mode.name is 'normal' and not jim.adaptor.emptySelection() 390 | # If a selection has been made with the mouse since the last 391 | # keypress in normal mode, switch to visual mode. 392 | jim.setMode 'visual' 393 | 394 | if not keyCode and keyString.length > 1 395 | #TODO handle this better, we're dropping keypresses here 396 | keyString = keyString.charAt 0 397 | 398 | passKeypressThrough = jim.onKeypress keyString 399 | 400 | if not passKeypressThrough 401 | # Prevent Ace's default handling of the event. 402 | command: {exec: (->)} 403 | 404 | undoManager = new JimUndoManager() 405 | editor.session.setUndoManager undoManager 406 | 407 | adaptor = new Adaptor editor 408 | jim = new Jim adaptor 409 | 410 | # Initialize the editor element's `className`s. 411 | adaptor.onModeChange null, name: 'normal' 412 | 413 | # Return `jim` in case embedders wanna inspect its state or give it a high 414 | # five. 415 | jim 416 | -------------------------------------------------------------------------------- /src/commands.coffee: -------------------------------------------------------------------------------- 1 | # All commands, other than operations and motions, live in here. Commands can 2 | # be prefixed with a `count` which multiplies their action in some way. Commands 3 | # that define a `::visualExec` will be available in visual mode as well as 4 | # normal mode. 5 | 6 | {Command, repeatCountTimes} = require './helpers' 7 | {Delete} = require './operators' 8 | {MoveLeft, MoveRight, MoveToEndOfLine, MoveToFirstNonBlank} = require './motions' 9 | Jim = require './jim' 10 | 11 | # The default key mappings are specified alongside the definitions of each command. 12 | map = (keys, commandClass) -> Jim.keymap.mapCommand keys, commandClass 13 | 14 | # Define a convenience class for commands that switch to another mode. 15 | class ModeSwitch extends Command 16 | exec: (jim) -> 17 | @beforeSwitch? jim 18 | jim.setMode @switchToMode 19 | 20 | 21 | # Visual mode switches 22 | # -------------------- 23 | 24 | # Switch to characterwise visual mode. 25 | map ['v'], class VisualSwitch extends Command 26 | isRepeatable: no 27 | exec: (jim) -> 28 | anchor = jim.adaptor.position() 29 | jim.adaptor.setSelectionAnchor() 30 | jim.setMode 'visual', {anchor} 31 | visualExec: (jim) -> 32 | if jim.mode.linewise 33 | jim.setMode 'visual', linewise: no 34 | jim.adaptor.editor.selection.setSelectionAnchor jim.mode.anchor... 35 | else 36 | jim.onEscape() 37 | 38 | # Switch to linewise visual mode. 39 | map ['V'], class VisualLinewiseSwitch extends Command 40 | isRepeatable: no 41 | exec: (jim) -> 42 | anchor = jim.adaptor.setLinewiseSelectionAnchor() 43 | jim.setMode 'visual', {linewise: yes, anchor} 44 | visualExec: (jim) -> 45 | if jim.mode.linewise 46 | jim.onEscape() 47 | else 48 | modeState = linewise: yes 49 | anchor = jim.adaptor.setLinewiseSelectionAnchor() 50 | modeState.anchor = anchor unless jim.mode.anchor 51 | jim.setMode 'visual', modeState 52 | 53 | 54 | # Insert mode switches 55 | # -------------------- 56 | 57 | # Insert before the char under the cursor. 58 | map ['i'], class Insert extends ModeSwitch 59 | switchToMode: 'insert' 60 | exec: (jim) -> 61 | @beforeSwitch? jim 62 | if @repeatableInsert 63 | # If `@repeatableInsert` is set, this call to `::exec` is to repeat the 64 | # insert. Don't switch to insert mode, just insert the text that was 65 | # inserted the first time. 66 | jim.adaptor.insert @repeatableInsert.string 67 | else 68 | # In order to inform the undo manager (which helps figures out what text to 69 | # insert when repeating an insert) when the insert is done doing whatever it 70 | # may have done before the switch to insert mode (e.g. deleted to the end of 71 | # the line in the case of `C`) and is switching to insert mode, a boolean is 72 | # set so the undo manager can bookmark that spot during the next keypress event 73 | jim.afterInsertSwitch = true 74 | 75 | jim.setMode @switchToMode 76 | 77 | # Insert after the char under the cursor. 78 | map ['a'], class InsertAfter extends Insert 79 | beforeSwitch: (jim) -> jim.adaptor.moveRight true 80 | 81 | # Insert at the end of the line. 82 | map ['A'], class InsertAtEndOfLine extends Insert 83 | beforeSwitch: (jim) -> 84 | new MoveToEndOfLine().exec jim 85 | jim.adaptor.moveRight true 86 | 87 | # Delete all remaining text on the line and insert in its place. 88 | map ['C'], class ChangeToEndOfLine extends Insert 89 | beforeSwitch: (jim) -> 90 | new DeleteToEndOfLine(@count).exec jim 91 | 92 | # Insert before to first non-blank char of the line. 93 | map ['I'], class InsertBeforeFirstNonBlank extends Insert 94 | beforeSwitch: (jim) -> new MoveToFirstNonBlank().exec jim 95 | 96 | # Create a new line below the cursor and insert there. 97 | # 98 | # Move the cursor to other end of selected text. (visual mode) 99 | map ['o'], class OpenLine extends Insert 100 | beforeSwitch: (jim) -> 101 | row = jim.adaptor.row() + (if @above then 0 else 1) 102 | jim.adaptor.insertNewLine row 103 | jim.adaptor.moveTo row, 0 104 | visualExec: (jim) -> 105 | selection = jim.adaptor.editor.selection 106 | {row:rowL, column:columnL} = selection.getSelectionLead() 107 | {row:rowA, column:columnA} = selection.getSelectionAnchor() 108 | if not jim.mode.linewise 109 | if rowL < rowA or (rowL == rowA and columnL < columnA) 110 | columnA -= 1 111 | columnL += 1 112 | selection.setSelectionAnchor(rowL, columnL) 113 | else 114 | [rowA, columnA] = jim.mode.anchor if isNaN columnA 115 | selection.selectionAnchor.column = columnL 116 | selection.selectionAnchor.row = rowL 117 | jim.mode.anchor = jim.adaptor.setLinewiseSelectionAnchor() 118 | jim.adaptor.moveTo(rowA, columnA) 119 | 120 | # Create a new line above the cursor and insert there. 121 | map ['O'], class OpenLineAbove extends OpenLine 122 | above: yes 123 | 124 | # Replace the char under the cursor with an insert. 125 | map ['s'], class ChangeChar extends Insert 126 | beforeSwitch: (jim) -> new DeleteChar(@count).exec jim 127 | 128 | 129 | # Replace mode switch 130 | # ------------------- 131 | 132 | map ['R'], class ReplaceSwitch extends ModeSwitch 133 | beforeSwitch: (jim) -> jim.adaptor.setOverwriteMode on 134 | switchToMode: 'replace' 135 | 136 | 137 | # Miscellaneous commands 138 | # ---------------------- 139 | 140 | # Join a line with the line following it. 141 | map ['g', 'J'], class JoinLines extends Command 142 | exec: (jim) -> 143 | timesLeft = Math.max(@count, 2) - 1 144 | while timesLeft-- 145 | jim.adaptor.selectLineEnding @normalize 146 | jim.adaptor.deleteSelection() 147 | if @normalize 148 | jim.adaptor.insert ' ' 149 | jim.adaptor.moveLeft() 150 | 151 | visualExec: (jim) -> 152 | [minRow, maxRow] = jim.adaptor.selectionRowRange() 153 | jim.adaptor.clearSelection() 154 | jim.adaptor.moveTo minRow, 0 155 | @count = maxRow - minRow + 1 156 | @exec jim 157 | jim.setMode 'normal' 158 | 159 | # Join a line with the line following it, ensuring that one space separates the 160 | # content from the lines. 161 | map ['J'], class JoinLinesNormalizingWhitespace extends JoinLines 162 | normalize: yes 163 | 164 | # Delete all remaining text on the line. 165 | map ['D'], class DeleteToEndOfLine extends Command 166 | exec: (jim) -> new Delete(1, new MoveToEndOfLine @count).exec jim 167 | 168 | # Paste after the cursor. Paste after the line if pasting linewise register. 169 | map ['p'], class Paste extends Command 170 | exec: (jim) -> 171 | return if not registerValue = jim.registers['"'] 172 | 173 | # Using a count with `p` causes the pasted text to be repeated. 174 | text = new Array(@count + 1).join registerValue 175 | linewiseRegister = /\n$/.test registerValue 176 | if linewiseRegister 177 | # Registers with linewise text in them (e.g. yanked with `yy` instead of `yw`, 178 | # for instance) are never pasted mid-line. Move to the beginning of a line to 179 | # ensure this doesn't happen. 180 | row = jim.adaptor.row() + (if @before then 0 else 1) 181 | lastRow = jim.adaptor.lastRow() 182 | 183 | # If we're pasting row(s) after the last row, we have to move the line 184 | # ending to the begining of the string. 185 | if row > lastRow 186 | [wholeString, beforeLineEnding, lineEnding] = /^([\s\S]*)(\r?\n)$/.exec text 187 | text = lineEnding + beforeLineEnding 188 | 189 | column = jim.adaptor.lineText(lastRow).length - 1 190 | jim.adaptor.moveTo row, column 191 | else 192 | jim.adaptor.moveTo row, 0 193 | jim.adaptor.insert text 194 | jim.adaptor.moveTo row, 0 195 | else 196 | jim.adaptor.insert text, not @before 197 | 198 | visualExec: (jim) -> 199 | if jim.mode.linewise 200 | jim.adaptor.makeLinewise() 201 | else 202 | jim.adaptor.includeCursorInSelection() 203 | overwrittenText = jim.adaptor.deleteSelection() 204 | @before = true 205 | @exec jim 206 | jim.registers['"'] = overwrittenText 207 | jim.setMode 'normal' 208 | 209 | # Paste before the cursor. Paste before the line if pasting linewise register. 210 | map ['P'], class PasteBefore extends Paste 211 | before: yes 212 | 213 | # Replace the char under the cursor with the char pressed after `r`. 214 | map ['r'], class ReplaceChar extends Command 215 | # Match `[\s\S]` so that it will match `\n` (windows' `\r\n`?) 216 | @followedBy: /[\s\S]+/ 217 | exec: (jim) -> 218 | jim.adaptor.setSelectionAnchor() 219 | new MoveRight(@count).exec jim 220 | jim.adaptor.deleteSelection() # don't yank 221 | replacementText = if /^\r?\n$/.test @followedBy 222 | @followedBy 223 | else 224 | new Array(@count + 1).join @followedBy 225 | jim.adaptor.insert replacementText 226 | new MoveLeft().exec jim 227 | 228 | 229 | # Repeat the last repeatable command. 230 | map ['.'], class RepeatCommand extends Command 231 | isRepeatable: no 232 | exec: (jim) -> 233 | command = jim.lastCommand 234 | return if not command 235 | 236 | if command.switchToMode is 'insert' 237 | console.log 'command.repeatableInsert', command.repeatableInsert 238 | 239 | # For an insert that wasn't contiguous (i.e. the user moved the cursor 240 | # partway through the insert), Vim repeats it as a standard `i` insert 241 | # with just the last contigous piece of text. 242 | if not command.repeatableInsert.contiguous 243 | {string} = command.repeatableInsert 244 | command = new Insert() 245 | command.repeatableInsert = {string} 246 | 247 | if selectionSize = command.selectionSize 248 | # If we're repeating a command made in visual mode, it should affect the 249 | # same "amount" of text by using motions to move over the same aomount 250 | # of text 251 | if selectionSize.lines 252 | jim.adaptor.makeLinewise selectionSize.lines - 1 253 | else if selectionSize.chars 254 | jim.adaptor.setSelectionAnchor() 255 | new MoveRight(selectionSize.chars).exec jim 256 | else 257 | jim.adaptor.setSelectionAnchor() 258 | row = jim.adaptor.row() + selectionSize.lineEndings 259 | jim.adaptor.moveTo row, selectionSize.trailingChars - 1 260 | 261 | command.visualExec jim 262 | 263 | else 264 | #TODO count should replace the lastCommand's count 265 | command.exec jim 266 | 267 | # Undo the last command that modified the document. 268 | map ['u'], class Undo extends Command 269 | isRepeatable: no 270 | exec: repeatCountTimes (jim) -> jim.adaptor.undo() 271 | 272 | # Delete the char under the cursor. 273 | map ['x'], class DeleteChar extends Command 274 | exec: (jim) -> new Delete(1, new MoveRight @count).exec jim 275 | visualExec: (jim) -> Delete::visualExec jim 276 | 277 | # Delete the char before the cursor. 278 | map ['X'], class Backspace extends Command 279 | exec: (jim) -> new Delete(1, new MoveLeft @count).exec jim 280 | visualExec: (jim) -> 281 | del = new Delete(@count) 282 | del.linewise = true 283 | del.visualExec jim 284 | 285 | 286 | # Move left in normal mode 287 | # Delete selections in visual mode 288 | map ['backspace'], class extends MoveLeft 289 | prevLine: yes 290 | visualExec: (jim) -> Delete::visualExec jim 291 | 292 | map ['delete'], DeleteChar 293 | 294 | # Exports 295 | # ------- 296 | module.exports = {} 297 | -------------------------------------------------------------------------------- /src/helpers.coffee: -------------------------------------------------------------------------------- 1 | # Define the base class for all commands. 2 | class exports.Command 3 | constructor: (@count = 1) -> 4 | isRepeatable: yes 5 | 6 | # If the class specifies a regex for char(s) that should follow the command, 7 | # then the command isn't complete until those char(s) have been matched. 8 | isComplete: -> 9 | if @constructor.followedBy then @followedBy else true 10 | 11 | class exports.InputState 12 | constructor: -> 13 | @clear() 14 | 15 | clear: -> 16 | @command = null 17 | @count = '' 18 | @keymap = null 19 | @operatorPending = null 20 | 21 | setCommand: (commandClass) -> 22 | @command = new commandClass(parseInt(@count) or null) 23 | @count = '' 24 | 25 | setOperationMotion: (motionClass) -> 26 | @command.motion = new motionClass(parseInt(@count) or null) 27 | 28 | # Motions need a reference to the operation they're a part of since it 29 | # sometimes changes the amount of text they move over (e.g. `cw` 30 | # deletes less text than `dw`). 31 | @command.motion.operation = @command 32 | 33 | @count = '' 34 | 35 | toString: -> 36 | "TODO" 37 | 38 | # A bunch of commands can just repeat an action however many times their `@count` 39 | # specifies. For example `5x` does exactly the same thing as pressing `x` five times. 40 | # This helper is used for that case. 41 | exports.repeatCountTimes = (func) -> 42 | (jim) -> 43 | timesLeft = @count 44 | func.call this, jim while timesLeft-- 45 | -------------------------------------------------------------------------------- /src/jim.coffee: -------------------------------------------------------------------------------- 1 | # An instance of `Jim` holds all the Jim-specific state of the editor: the 2 | # current command, the current mode, the values of all the registers, etc. It 3 | # also holds a reference to the adaptor that is doing its bidding in the editor. 4 | # `Command`s are passed an instance of `Jim` when they are executed which allows 5 | # them to change Jim's state and manipulate the editor (through the `@adaptor`). 6 | 7 | {InputState} = require './helpers' 8 | Keymap = require './keymap' 9 | 10 | class Jim 11 | @VERSION: '0.2.1-pre' 12 | 13 | @keymap: new Keymap 14 | 15 | constructor: (@adaptor) -> 16 | @registers = {} 17 | @setMode 'normal' 18 | @inputState = new InputState 19 | 20 | modes: require './modes' 21 | 22 | # Change Jim's mode to `modeName` with optional `modeState`: 23 | # 24 | # jim.setMode 'visual', linewise: yes 25 | setMode: (modeName, modeState) -> 26 | console.log 'setMode', modeName, modeState if @debugMode 27 | prevMode = @mode 28 | if modeName is prevMode?.name 29 | return unless modeState 30 | @mode[key] = value for own key, value of modeState 31 | else 32 | @mode = modeState or {} 33 | @mode.name = modeName 34 | 35 | @adaptor.onModeChange? prevMode, @mode 36 | 37 | switch prevMode?.name 38 | when 'insert' 39 | @adaptor.moveLeft() 40 | 41 | # Get info about what was inserted so the insert "remembers" how to 42 | # repeat itself. 43 | @lastCommand.repeatableInsert = @adaptor.lastInsert() 44 | 45 | when 'replace' 46 | @adaptor.setOverwriteMode off 47 | 48 | # Pressing escape blows away all the state. 49 | onEscape: -> 50 | @setMode 'normal' 51 | @inputState.clear() 52 | @adaptor.clearSelection() 53 | 54 | # When a key is pressed, let the current mode figure out what to do about it. 55 | onKeypress: (keys) -> @modes[@mode.name].onKeypress.call this, keys 56 | 57 | # Delete the selected text and put it in the default register. 58 | deleteSelection: (exclusive, linewise) -> 59 | @registers['"'] = @adaptor.deleteSelection exclusive, linewise 60 | 61 | # Yank the selected text into the default register. 62 | yankSelection: (exclusive, linewise) -> 63 | @registers['"'] = @adaptor.selectionText exclusive, linewise 64 | @adaptor.clearSelection true 65 | 66 | module.exports = Jim 67 | -------------------------------------------------------------------------------- /src/keymap.coffee: -------------------------------------------------------------------------------- 1 | # This is a pretty standard key-to-command keymap except for a few details: 2 | # 3 | # * It has some built-in Vim-like smarts about the concepts of motions and 4 | # operators and if/how they should be available in each mode 5 | # * It differentiates between invalid commands (`gz`) and partial commands (`g`) 6 | class Keymap 7 | 8 | constructor: -> 9 | @normal = {} 10 | @visual = {} 11 | @operatorPending = {} 12 | 13 | 14 | # Mapping commands 15 | # ---------------- 16 | 17 | mapIntoObject = (object, keys, command) -> 18 | for key in keys[0..-2] 19 | object[key] or= {} 20 | object = object[key] 21 | object[keys[keys.length-1]] = command 22 | 23 | # Map the `commandClass` to the `keys` sequence. Map it as a visual command as well 24 | # if the class has a `::visualExec`. 25 | mapCommand: (keys, commandClass) -> 26 | if commandClass::exec 27 | mapIntoObject @normal, keys, commandClass 28 | if commandClass::visualExec 29 | mapIntoObject @visual, keys, commandClass 30 | 31 | # Map `motionClass` to the `keys` sequence. 32 | mapMotion: (keys, motionClass) -> 33 | mapIntoObject @normal, keys, motionClass 34 | mapIntoObject @visual, keys, motionClass 35 | mapIntoObject @operatorPending, keys, motionClass 36 | 37 | # Map `operatorClass` to the `keys` sequence. 38 | mapOperator: (keys, operatorClass) -> 39 | mapIntoObject @normal, keys, operatorClass 40 | mapIntoObject @visual, keys, operatorClass 41 | 42 | 43 | # Exports 44 | # ------- 45 | module.exports = Keymap 46 | -------------------------------------------------------------------------------- /src/modes.coffee: -------------------------------------------------------------------------------- 1 | # Each mode handles key presses a bit differently. For instance, typing an 2 | # operator in visual mode immediately operates on the selected text. In normal 3 | # mode Jim waits for a motion to follow the operator. All of the modes' 4 | # keyboard handling is defined here. 5 | # 6 | # Each mode's `onkeypress` is executed in the context of an instance of `Jim`. 7 | 8 | # Define an unmapped `Motion` that will be used for double operators (e.g. `cc`, 9 | # `2yy`, `3d4d`). 10 | class LinewiseCommandMotion 11 | constructor: (@count = 1) -> 12 | linewise: yes 13 | isComplete: -> yes 14 | exec: (jim) -> 15 | additionalLines = @count - 1 16 | jim.adaptor.moveDown() while additionalLines-- 17 | 18 | 19 | # Shame the user in the console for not knowing their Jim commands. 20 | invalidCommand = (type = 'command') -> 21 | console.log "invalid #{type}: #{@inputState}" 22 | @onEscape() 23 | 24 | 25 | # Normal mode (a.k.a. "command mode") 26 | # ----------------------------------- 27 | exports.normal = 28 | onKeypress: (key) -> 29 | if /^[1-9]$/.test(key) or (key is "0" and @inputState.count.length) 30 | @inputState.count += key 31 | 32 | else if not @inputState.command 33 | commandClass = (@inputState.keymap or Jim.keymap.normal)[key] 34 | 35 | if not commandClass 36 | if key.length is 1 37 | invalidCommand.call this 38 | else 39 | return true 40 | 41 | else if commandClass.prototype 42 | @inputState.setCommand commandClass 43 | 44 | if @inputState.command.isOperation 45 | # Hang onto the pending operator so that double-operators can 46 | # recognized (`cc`, `yy`, etc). 47 | @inputState.operatorPending = key 48 | 49 | else if commandClass 50 | @inputState.keymap = commandClass 51 | 52 | else if @inputState.command.constructor.followedBy 53 | # If we've got a command that expects a key to follow it, check if 54 | # the key is what it's expecting. 55 | if @inputState.command.constructor.followedBy.test key 56 | @inputState.command.followedBy = key 57 | else 58 | console.log "#{@inputState.command} didn't expect to be followed by \"#{key}\"" 59 | 60 | else if @inputState.operatorPending 61 | if regex = @inputState.command.motion?.constructor.followedBy 62 | 63 | # If we've got a motion that expects a key to follow it, check if 64 | # the key is what it's expecting. 65 | if regex.test key 66 | @inputState.command.motion.followedBy = key 67 | else 68 | console.log "#{@inputState.command} didn't expect to be followed by \"#{key}\"" 69 | 70 | else 71 | motionClass = if key is @inputState.operatorPending 72 | LinewiseCommandMotion 73 | else 74 | (@inputState.keymap or Jim.keymap.operatorPending)[key] 75 | 76 | if not motionClass 77 | if key.length is 1 78 | invalidCommand.call this 79 | else 80 | return true 81 | 82 | else if motionClass.prototype 83 | @inputState.setOperationMotion motionClass 84 | 85 | else 86 | @inputState.keymap = motion 87 | 88 | # Execute the command if it's complete, otherwise wait for more keys. 89 | if @inputState.command?.isComplete() 90 | @inputState.command.exec this 91 | @lastCommand = @inputState.command if @inputState.command.isRepeatable 92 | @inputState.clear() 93 | 94 | 95 | # Visual mode 96 | # ----------- 97 | exports.visual = 98 | onKeypress: (key) -> 99 | if /^[1-9]$/.test(key) or (key is "0" and @inputState.count.length) 100 | @inputState.count += key 101 | 102 | else if not @inputState.command 103 | commandClass = (@inputState.keymap or Jim.keymap.visual)[key] 104 | 105 | if not commandClass 106 | if key.length is 1 107 | invalidCommand.call this 108 | else 109 | return true 110 | 111 | else if commandClass.prototype 112 | @inputState.setCommand commandClass 113 | 114 | else 115 | @inputState.keymap = commandClass 116 | 117 | else if @inputState.command.constructor.followedBy 118 | 119 | # If we've got a motion that expects a key to follow it, check if 120 | # the key is what it's expecting. 121 | if @inputState.command.constructor.followedBy.test key 122 | @inputState.command.followedBy = key 123 | else 124 | console.log "#{@inputState.command} didn't expect to be followed by \"#{key}\"" 125 | 126 | wasBackwards = @adaptor.isSelectionBackwards() 127 | 128 | # Operations are always "complete" in visual mode. 129 | if @inputState.command?.isOperation or @inputState.command?.isComplete() 130 | if @inputState.command.isRepeatable 131 | # Save the selection's "size", which will be used if the command is 132 | # repeated. 133 | @inputState.command.selectionSize = if @mode.name is 'visual' and @mode.linewise 134 | [minRow, maxRow] = @adaptor.selectionRowRange() 135 | lines: (maxRow - minRow) + 1 136 | else 137 | @adaptor.characterwiseSelectionSize() 138 | @inputState.command.linewise = @mode.linewise 139 | 140 | @lastCommand = @inputState.command 141 | 142 | @inputState.command.visualExec this 143 | @inputState.clear() 144 | 145 | # If we haven't changed out of characterwise visual mode and the direction 146 | # of the selection changes, we have to make sure that the anchor character 147 | # stays selected. 148 | if @mode.name is 'visual' and not @mode.linewise 149 | if wasBackwards 150 | @adaptor.adjustAnchor -1 if not @adaptor.isSelectionBackwards() 151 | else 152 | @adaptor.adjustAnchor 1 if @adaptor.isSelectionBackwards() 153 | 154 | 155 | # Other modes 156 | # ----------- 157 | # 158 | # Insert and replace modes just pass all keystrokes through (except ``). 159 | exports.insert = onKeypress: -> true 160 | exports.replace = onKeypress: -> true 161 | -------------------------------------------------------------------------------- /src/motions.coffee: -------------------------------------------------------------------------------- 1 | # Motions are exactly that: motions. They move the cursor but don't change the 2 | # document at all. They can be used in normal or visual mode and can follow an 3 | # operator in normal mode to operate on the text that they move over. 4 | 5 | {Command, repeatCountTimes} = require './helpers' 6 | Jim = require './jim' 7 | 8 | 9 | # The default key mappings are specified alongside the definitions of each 10 | # motion. Accumulate the mappings so they can be exported. 11 | map = (keys, motionClass) -> Jim.keymap.mapMotion keys, motionClass 12 | 13 | 14 | # base class for all motions 15 | class Motion extends Command 16 | constructor: (@count = 1) -> 17 | isRepeatable: false 18 | linewise: no 19 | exclusive: no 20 | 21 | # motions do the same thing in visual mode 22 | visualExec: (jim) -> @exec jim 23 | 24 | 25 | # Basic directional motions 26 | # ------------------------- 27 | map ['h'], class MoveLeft extends Motion 28 | exclusive: yes 29 | exec: repeatCountTimes (jim) -> 30 | if @prevLine and jim.adaptor.column() is 0 31 | jim.adaptor.moveToEndOfPreviousLine() 32 | else jim.adaptor.moveLeft() 33 | map ['j'], class MoveDown extends Motion 34 | linewise: yes 35 | exec: repeatCountTimes (jim) -> jim.adaptor.moveDown() 36 | map ['k'], class MoveUp extends Motion 37 | linewise: yes 38 | exec: repeatCountTimes (jim) -> jim.adaptor.moveUp() 39 | map ['l'], class MoveRight extends Motion 40 | exclusive: yes 41 | exec: repeatCountTimes (jim) -> 42 | linelen = jim.adaptor.lineText().length - 1 43 | column = jim.adaptor.column() 44 | if @nextLine and column >= linelen 45 | jim.adaptor.moveTo jim.adaptor.row() + 1, 0 46 | else jim.adaptor.moveRight @operation? 47 | 48 | map ['left'], MoveLeft 49 | map ['down'], MoveDown 50 | map ['up'], MoveUp 51 | map ['right'], MoveRight 52 | 53 | map ['space'], class extends MoveRight 54 | nextLine: yes 55 | 56 | # Word motions 57 | # ------------ 58 | 59 | # Return a new regex with a fresh lastIndex each time for use in word motions. 60 | # There are two different kinds of words: 61 | # 62 | # * A **WORD** is a string of non-whitespace characters. 63 | # * A **word** is a string of regex word characters (i.e. `[A-Za-z0-9_]`) *or* a 64 | # string of non-whitespace non-word characters (i.e. special chars) 65 | WORDRegex = -> /\S+/g 66 | wordRegex = -> /(\w+)|([^\w\s]+)/g 67 | 68 | 69 | # Move to the next end of a **word**. 70 | map ['e'], class MoveToWordEnd extends Motion 71 | exec: repeatCountTimes (jim) -> 72 | regex = if @bigWord then WORDRegex() else wordRegex() 73 | line = jim.adaptor.lineText() 74 | [row, column] = jim.adaptor.position() 75 | rightOfCursor = line.substring column 76 | 77 | matchOnLine = regex.exec rightOfCursor 78 | 79 | # If we're on top of the last char of a word we want to match the next one. 80 | if matchOnLine?[0].length <= 1 81 | matchOnLine = regex.exec rightOfCursor 82 | 83 | # If there's a match on the current line, go to the end of the word that's 84 | # been matched. 85 | if matchOnLine 86 | column += matchOnLine[0].length + matchOnLine.index - 1 87 | 88 | # If there's no match on the current line go end of the next word, whatever 89 | # line that may be on. If there are no more non-blank characters, don't 90 | # move the cursor. 91 | else 92 | loop 93 | line = jim.adaptor.lineText ++row 94 | firstMatchOnSubsequentLine = regex.exec line 95 | if firstMatchOnSubsequentLine 96 | column = firstMatchOnSubsequentLine[0].length + firstMatchOnSubsequentLine.index - 1 97 | break 98 | else if row is jim.adaptor.lastRow() 99 | return 100 | 101 | # Move to the `row` and `column` that have been determined. 102 | jim.adaptor.moveTo row, column 103 | 104 | # Move to the next end of a **WORD**. 105 | map ['E'], class MoveToBigWordEnd extends MoveToWordEnd 106 | bigWord: yes 107 | 108 | 109 | # Move to the next beginning of a **word**. 110 | map ['w'], class MoveToNextWord extends Motion 111 | exclusive: yes 112 | exec: (jim) -> 113 | timesLeft = @count 114 | while timesLeft-- 115 | regex = if @bigWord then WORDRegex() else wordRegex() 116 | line = jim.adaptor.lineText() 117 | [row, column] = jim.adaptor.position() 118 | rightOfCursor = line.substring column 119 | 120 | match = regex.exec rightOfCursor 121 | 122 | # If we're on top of part of a word, match the next one. 123 | match = regex.exec rightOfCursor if match?.index is 0 124 | 125 | # If the match isn't on this line, find it on the next. 126 | if not match 127 | 128 | # If the user typed `dw` on the last word of a line, for instance, just 129 | # delete the rest of the word instead of deleting to the start of the 130 | # word on the next line. 131 | if timesLeft is 0 and @operation 132 | column = line.length 133 | 134 | else 135 | line = jim.adaptor.lineText ++row 136 | nextLineMatch = regex.exec line 137 | column = nextLineMatch?.index or 0 138 | 139 | # `cw` actually behaves like `ce` instead of `dwi`. So if this motion is 140 | # part of a `Change` operation, ensure that the last time the loop is 141 | # executed we execute a `MoveToWordEnd` instead. 142 | else if timesLeft is 0 and @operation?.switchToMode is 'insert' 143 | lastMotion = new MoveToWordEnd() 144 | lastMotion.bigWord = @bigWord 145 | lastMotion.exec jim 146 | @exclusive = no 147 | return 148 | 149 | # If the match is on this line, go to the column. 150 | else 151 | column += match.index 152 | 153 | # Move to the `row` and `column` that have been determined. 154 | jim.adaptor.moveTo row, column 155 | 156 | # Move to the next beginning of a **WORD**. 157 | map ['W'], class MoveToNextBigWord extends MoveToNextWord 158 | bigWord: yes 159 | 160 | 161 | # Build regexes to find the last instance of a word. 162 | lastWORDRegex = ///#{WORDRegex().source}\s*$/// 163 | lastWordRegex = ///(#{wordRegex().source})\s*$/// 164 | 165 | # Move to the last beginning of a **word**. 166 | map ['b'], class MoveBackWord extends Motion 167 | exclusive: yes 168 | exec: repeatCountTimes (jim) -> 169 | regex = if @bigWord then lastWORDRegex else lastWordRegex 170 | line = jim.adaptor.lineText() 171 | [row, column] = jim.adaptor.position() 172 | leftOfCursor = line.substring 0, column 173 | 174 | match = regex.exec leftOfCursor 175 | if match 176 | column = match.index 177 | 178 | # If there are no matches left of the cursor, go to the last word on the 179 | # previous line. Vim skips lines that are have only whitespace on them, but 180 | # not completely empty lines. 181 | else 182 | row-- 183 | row-- while /^\s+$/.test(line = jim.adaptor.lineText row) 184 | match = regex.exec line 185 | column = match?.index or 0 186 | 187 | # Move to the `row` and `column` that have been determined. 188 | jim.adaptor.moveTo row, column 189 | 190 | # Move to the last beginning of a **WORD**. 191 | map ['B'], class MoveBackBigWord extends MoveBackWord 192 | bigWord: yes 193 | 194 | 195 | # Other left/right motions 196 | # ------------------------ 197 | 198 | # Move to the first column on the line. 199 | map ['0'], class MoveToBeginningOfLine extends Motion 200 | exclusive: yes 201 | exec: (jim) -> jim.adaptor.moveTo jim.adaptor.row(), 0 202 | 203 | # Move to the first non-blank character on the line. 204 | map ['^'], class MoveToFirstNonBlank extends Motion 205 | exec: (jim) -> 206 | row = jim.adaptor.row() 207 | line = jim.adaptor.lineText row 208 | column = /\S/.exec(line)?.index or 0 209 | jim.adaptor.moveTo row, column 210 | 211 | # Move to the last column on the line. 212 | map ['$'], class MoveToEndOfLine extends Motion 213 | exec: (jim) -> 214 | additionalLines = @count - 1 215 | new MoveDown(additionalLines).exec jim if additionalLines 216 | jim.adaptor.moveToLineEnd() 217 | 218 | 219 | # Jump motions 220 | # ------------ 221 | 222 | # Go to `{count}` line number or the first line. 223 | map ['g', 'g'], class GoToLine extends Motion 224 | linewise: yes 225 | exec: (jim) -> 226 | rowNumber = @count - 1 227 | lineText = jim.adaptor.lineText rowNumber 228 | jim.adaptor.moveTo rowNumber, 0 229 | new MoveToFirstNonBlank().exec jim 230 | 231 | # Go to `{count}` line number or the last line. 232 | map ['G'], class GoToLineOrEnd extends GoToLine 233 | constructor: (@count) -> 234 | exec: (jim) -> 235 | @count or= jim.adaptor.lastRow() + 1 236 | super 237 | 238 | # Go to the first line that's visible in the viewport. 239 | map ['H'], class GoToFirstVisibleLine extends Motion 240 | linewise: yes 241 | exec: (jim) -> 242 | line = jim.adaptor.firstFullyVisibleRow() + @count 243 | new GoToLineOrEnd(line).exec jim 244 | 245 | # Go to the middle line of the lines that exist and are visible in the viewport. 246 | map ['M'], class GoToMiddleLine extends Motion 247 | linewise: yes 248 | exec: (jim) -> 249 | topRow = jim.adaptor.firstFullyVisibleRow() 250 | lines = jim.adaptor.lastFullyVisibleRow() - topRow 251 | linesFromTop = Math.floor(lines / 2) 252 | new GoToLineOrEnd(topRow + 1 + linesFromTop).exec jim 253 | 254 | # Go to the last line of the lines that exist and are visible in the viewport. 255 | map ['L'], class GoToLastVisibleLine extends Motion 256 | linewise: yes 257 | exec: (jim) -> 258 | line = jim.adaptor.lastFullyVisibleRow() + 2 - @count 259 | new GoToLineOrEnd(line).exec jim 260 | 261 | 262 | # Search motions 263 | # -------------- 264 | 265 | # Prompt the user for a search term and search forward for it. 266 | map ['/'], class Search extends Motion 267 | # Given that `jim.search` has already been set, search for the `{count}`'th 268 | # occurrence of the search. Reverse `jim.search`'s direction if `reverse` is 269 | # true. 270 | @runSearch: (jim, count, reverse) -> 271 | return if not jim.search 272 | {backwards, searchString, wholeWord} = jim.search 273 | backwards = not backwards if reverse 274 | jim.adaptor.search backwards, searchString, wholeWord while count-- 275 | 276 | exclusive: yes 277 | getSearch: -> {searchString: prompt("Find:"), @backwards} 278 | exec: (jim) -> 279 | jim.search = @getSearch jim 280 | Search.runSearch jim, @count 281 | 282 | # Prompt the user for a search term and search backwards for it. 283 | map ['?'], class SearchBackwards extends Search 284 | backwards: yes 285 | 286 | # Search fowards for the next occurrence of the nearest word. 287 | map ['*'], class NearestWordSearch extends Search 288 | getSearch: (jim) -> 289 | [searchString, charsAhead] = nearestWord jim 290 | 291 | # If we're searching for a word that's ahead of the cursor, ensure that the 292 | # search starts beyond it. 293 | new MoveRight(charsAhead).exec jim if charsAhead 294 | 295 | # Match only whole words unless searching for special chars. 296 | wholeWord = /^\w/.test searchString 297 | 298 | {searchString, wholeWord, @backwards} 299 | 300 | # The word used for `*` and `#` is the first of the following that matches on 301 | # the line: 302 | # 303 | # 1. The `\w+` word under or after the cursor 304 | # 2. The first string of non-blanks (i.e. `\S+`) under or after the cursor 305 | nearestWord = (jim) -> 306 | line = jim.adaptor.lineText() 307 | column = jim.adaptor.column() 308 | leftOfCursor = line.substring 0, column 309 | rightOfCursor = line.substring column 310 | charsAhead = null 311 | 312 | if /\W/.test line[column] 313 | leftMatch = [''] 314 | nextWord = /\w+/.exec rightOfCursor 315 | rightMatch = if not nextWord 316 | /[^\w\s]+/.exec rightOfCursor 317 | else 318 | nextWord 319 | charsAhead = rightMatch.index 320 | else 321 | leftMatch = /\w*$/.exec leftOfCursor 322 | rightMatch = /^\w*/.exec rightOfCursor 323 | 324 | [leftMatch[0] + rightMatch[0], charsAhead] 325 | 326 | # Search backwards for the next occurrence of the nearest word. 327 | map ['#'], class NearestWordSearchBackwards extends NearestWordSearch 328 | backwards: yes 329 | 330 | 331 | # Repeat the last search. 332 | map ['n'], class SearchAgain extends Motion 333 | exclusive: yes 334 | exec: (jim) -> Search.runSearch jim, @count 335 | 336 | # Repeat the last search, reversing the direction. 337 | map ['N'], class SearchAgainReverse extends Motion 338 | exclusive: yes 339 | exec: (jim) -> Search.runSearch jim, @count, true 340 | 341 | 342 | # Move-to-character motions 343 | # ------------------------- 344 | # 345 | # These motions are expected to get a be followed by a character keypress. When 346 | # they are executed this character is stored as the command's `@followedBy`. 347 | 348 | # Go to the next `@followedBy` char on the line. 349 | map ['f'], class GoToNextChar extends Motion 350 | @followedBy: /./ 351 | exec: (jim) -> 352 | timesLeft = @count ? 1 353 | [row, column] = jim.adaptor.position() 354 | rightOfCursor = jim.adaptor.lineText().substring column + 1 355 | columnsRight = 0 356 | while timesLeft-- 357 | columnsRight = rightOfCursor.indexOf(@followedBy, columnsRight) + 1 358 | if columnsRight 359 | columnsRight-- if @beforeChar 360 | jim.adaptor.moveTo row, column + columnsRight 361 | 362 | # Go to the char before the next `@followedBy` char on the line. 363 | map ['t'], class GoUpToNextChar extends GoToNextChar 364 | beforeChar: yes 365 | 366 | 367 | # Go to the previous `@followedBy` char on the line. 368 | map ['F'], class GoToPreviousChar extends Motion 369 | @followedBy: /./ 370 | exec: (jim) -> 371 | timesLeft = @count ? 1 372 | [row, column] = jim.adaptor.position() 373 | leftOfCursor = jim.adaptor.lineText().substring 0, column 374 | targetColumn = column 375 | while timesLeft-- 376 | targetColumn = leftOfCursor.lastIndexOf(@followedBy, targetColumn - 1) 377 | if 0 <= targetColumn < column 378 | targetColumn++ if @beforeChar 379 | jim.adaptor.moveTo row, targetColumn 380 | 381 | # Go to the char after the previous `@followedBy` char on the line. 382 | map ['T'], class GoUpToPreviousChar extends GoToPreviousChar 383 | beforeChar: yes 384 | 385 | 386 | # Exports 387 | # ------- 388 | module.exports = { 389 | Motion, GoToLine, MoveDown, MoveLeft, MoveRight, MoveToEndOfLine, MoveToFirstNonBlank, 390 | MoveToNextBigWord, MoveToNextWord, MoveToBigWordEnd, MoveToWordEnd 391 | } 392 | -------------------------------------------------------------------------------- /src/operators.coffee: -------------------------------------------------------------------------------- 1 | # An operator followed by a motion is an `Operation`. For example, `ce` changes 2 | # all the text to the end of the current word since `c` is the change operator 3 | # and `e` is a motion that moves to the end of the word. 4 | 5 | {Command} = require './helpers' 6 | {GoToLine, MoveToFirstNonBlank} = require './motions' 7 | Jim = require './jim' 8 | 9 | # The default key mappings are specified alongside the definitions of each 10 | # `Operation`. 11 | map = (keys, operationClass) -> Jim.keymap.mapOperator keys, operationClass 12 | 13 | # Define the base class for all operations. 14 | class Operation extends Command 15 | constructor: (@count = 1, @motion) -> 16 | @motion.operation = this if @motion 17 | isOperation: true 18 | isComplete: -> @motion?.isComplete() 19 | switchToMode: 'normal' 20 | 21 | # Adjust the selection, if needed, and operate on that selection. 22 | visualExec: (jim) -> 23 | if @linewise 24 | jim.adaptor.makeLinewise() 25 | else if not @motion?.exclusive 26 | jim.adaptor.includeCursorInSelection() 27 | 28 | @operate jim 29 | 30 | jim.setMode @switchToMode 31 | 32 | # Select the amount of text that the motion moves over and operate on that 33 | # selection. 34 | exec: (jim) -> 35 | @startingPosition = jim.adaptor.position() 36 | jim.adaptor.setSelectionAnchor() 37 | if @count isnt 1 38 | @motion.count *= @count 39 | @count = 1 40 | @linewise ?= @motion.linewise 41 | @motion.exec jim 42 | @visualExec jim 43 | 44 | 45 | # Change the selected text or the text that `@motion` moves over (i.e. delete 46 | # the text and switch to insert mode). 47 | map ['c'], class Change extends Operation 48 | visualExec: (jim) -> 49 | super 50 | 51 | # If we're repeating a `Change`, insert the text that was inserted now that 52 | # we've deleted the selected text. 53 | if @repeatableInsert 54 | jim.adaptor.insert @repeatableInsert.string 55 | jim.setMode 'normal' 56 | 57 | # If we're executing this `Change` for the first time, set a flag so that an 58 | # undo mark can be pushed onto the undo stack before any text is inserted. 59 | else 60 | jim.afterInsertSwitch = true 61 | 62 | operate: (jim) -> 63 | # If we're changing a linewise selection or motion, move the end of the 64 | # previous line so that the cursor is left on an open line once the lines 65 | # are deleted. 66 | jim.adaptor.moveToEndOfPreviousLine() if @linewise 67 | 68 | jim.deleteSelection @motion?.exclusive, @linewise 69 | 70 | switchToMode: 'insert' 71 | 72 | # Delete the selection or the text that `@motion` moves over. 73 | map ['d'], class Delete extends Operation 74 | operate: (jim) -> 75 | jim.deleteSelection @motion?.exclusive, @linewise 76 | new MoveToFirstNonBlank().exec jim if @linewise 77 | 78 | # Yank into a register the selection or the text that `@motion` moves over. 79 | map ['y'], class Yank extends Operation 80 | operate: (jim) -> 81 | jim.yankSelection @motion?.exclusive, @linewise 82 | jim.adaptor.moveTo @startingPosition... if @startingPosition 83 | 84 | # Indent the lines in the selection or the text that `@motion` moves over. 85 | map ['>'], class Indent extends Operation 86 | operate: (jim) -> 87 | [minRow, maxRow] = jim.adaptor.selectionRowRange() 88 | jim.adaptor.indentSelection() 89 | new GoToLine(minRow + 1).exec jim 90 | 91 | # Outdent the lines in the selection or the text that `@motion` moves over. 92 | map ['<'], class Outdent extends Operation 93 | operate: (jim) -> 94 | [minRow, maxRow] = jim.adaptor.selectionRowRange() 95 | jim.adaptor.outdentSelection() 96 | new GoToLine(minRow + 1).exec jim 97 | 98 | module.exports = {Change, Delete} 99 | -------------------------------------------------------------------------------- /test/ace/commands.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: commands', 2 | setup: setupAceTests 3 | 4 | test 'A', -> 5 | @press 'Aend' 6 | eq @adaptor.lineText()[-3..-1], 'end' 7 | 8 | test 'C', -> 9 | @adaptor.moveTo 0, 11 10 | 11 | @press 'Cawesomes' 12 | eq @adaptor.lineText(), "_.sortBy = awesomes" 13 | 14 | test 'D', -> 15 | @adaptor.moveTo 0, 11 16 | 17 | @press 'D' 18 | eq @adaptor.lineText(), "_.sortBy = " 19 | 20 | test 'I', -> 21 | @press 'Istart', @esc 22 | eq @adaptor.lineText()[0..5], 'start_' 23 | 24 | @press 'jIstart', @esc 25 | eq @adaptor.lineText()[0..7], ' startr' 26 | 27 | test 'p', -> 28 | @press '3p' 29 | eq @adaptor.lineText(), "_.sortBy = function(obj, iterator, context) {" 30 | 31 | # linewise 32 | @press 'Wyyp' 33 | deepEqual @adaptor.position(), [1, 0] 34 | eq @adaptor.lineText(), "_.sortBy = function(obj, iterator, context) {" 35 | eq @adaptor.lineText(0), "_.sortBy = function(obj, iterator, context) {" 36 | 37 | @jim.registers['"'] = '!?' 38 | @press '2p' 39 | eq @adaptor.lineText(), "_!?!?.sortBy = function(obj, iterator, context) {" 40 | 41 | @jim.registers['"'] = 'last line\n' 42 | @press 'Gp' 43 | # this fails, but it is actually doing the right thing 44 | #deepEqual @adaptor.position(), [@adaptor.lastRow(), 0] 45 | eq @adaptor.lineText(), 'last line' 46 | 47 | 48 | test 'P', -> 49 | @press '3p' 50 | eq @adaptor.lineText(), "_.sortBy = function(obj, iterator, context) {" 51 | 52 | @press 'yyWP' 53 | deepEqual @adaptor.position(), [0, 0] 54 | eq @adaptor.lineText(), "_.sortBy = function(obj, iterator, context) {" 55 | eq @adaptor.lineText(1), "_.sortBy = function(obj, iterator, context) {" 56 | 57 | @jim.registers['"'] = '!?' 58 | @press '2P' 59 | eq @adaptor.lineText(), "!?!?_.sortBy = function(obj, iterator, context) {" 60 | 61 | test 's', -> 62 | @press 'sunderscore', @esc 63 | eq @adaptor.lineText(), "underscore.sortBy = function(obj, iterator, context) {" 64 | @press '3sy', @esc 65 | eq @adaptor.lineText(), "underscoryortBy = function(obj, iterator, context) {" 66 | @press '$sdo', @esc 67 | eq @adaptor.lineText(), "underscoryortBy = function(obj, iterator, context) do" 68 | 69 | test 'x', -> 70 | @press 'x' 71 | eq @adaptor.lineText(), ".sortBy = function(obj, iterator, context) {" 72 | @press '3x' 73 | eq @adaptor.lineText(), "rtBy = function(obj, iterator, context) {" 74 | @press '$x' 75 | eq @adaptor.lineText(), "rtBy = function(obj, iterator, context) " 76 | @press '$3x' 77 | eq @adaptor.lineText(), "rtBy = function(obj, iterator, context)" 78 | 79 | test 'X', -> 80 | @adaptor.moveTo 2, 11 81 | 82 | @press 'X' 83 | eq @adaptor.lineText(), " return{" 84 | @press 'X' 85 | eq @adaptor.lineText(), " retur{" 86 | @press '9X' 87 | eq @adaptor.lineText(), "{" 88 | @press 'X' 89 | eq @adaptor.lineText(), "{" 90 | 91 | test 'o,O', -> 92 | @press 'Onew line', @esc 93 | eq @adaptor.lineText(), "new line" 94 | eq @adaptor.row(), 0 95 | eq @adaptor.lastRow(), 16 96 | 97 | @press 'oanother line', @esc 98 | eq @adaptor.lineText(), 'another line' 99 | eq @adaptor.row(), 1 100 | eq @adaptor.lastRow(), 17 101 | 102 | test 'cc', -> 103 | @press 'ccdifferent line', @esc 104 | eq @adaptor.lineText(), 'different line' 105 | deepEqual @adaptor.position(), [0, 13], 'cc should leave the cursor at the end of the insert' 106 | eq @adaptor.lastRow(), 15 107 | 108 | @press '2ccbetter', @esc 109 | eq @adaptor.lineText(), 'better' 110 | deepEqual @adaptor.position(), [0, 5], 'cc should leave the cursor at the end of the insert' 111 | eq @adaptor.lineText(1), ' return {' 112 | eq @adaptor.lastRow(), 14 113 | 114 | test 'dd', -> 115 | @press '2Wdd' 116 | deepEqual @adaptor.position(), [0, 2], 'dd should leave the cursor on the first non-blank after the deletion' 117 | eq @adaptor.lastRow(), 14 118 | 119 | @press '3dd' 120 | deepEqual @adaptor.position(), [0, 6], 'dd should leave the cursor on the first non-blank after the deletion' 121 | eq @adaptor.lastRow(), 11 122 | 123 | test 'yy', -> 124 | @press 'Wyy' 125 | deepEqual @adaptor.position(), [0, 9], "yy should leave the cursor where it started" 126 | eq endings(@jim.registers['"']), "_.sortBy = function(obj, iterator, context) {\n" 127 | 128 | @press '2yy' 129 | deepEqual @adaptor.position(), [0, 9], "yy should leave the cursor where it started" 130 | eq endings(@jim.registers['"']), """ 131 | _.sortBy = function(obj, iterator, context) { 132 | return _.pluck(_.map(obj, function(value, index, list) { 133 | 134 | """ 135 | 136 | test '>>', -> 137 | @press '>>' 138 | eq @adaptor.lineText(0), ' _.sortBy = function(obj, iterator, context) {' 139 | deepEqual @adaptor.position(), [0, 2] 140 | 141 | @press '2>>' 142 | eq @adaptor.lineText(0), ' _.sortBy = function(obj, iterator, context) {' 143 | eq @adaptor.lineText(1), ' return _.pluck(_.map(obj, function(value, index, list) {' 144 | deepEqual @adaptor.position(), [0, 4] 145 | 146 | test '<<', -> 147 | @press '<<' # should do nothing 148 | eq @adaptor.lineText(0), '_.sortBy = function(obj, iterator, context) {' 149 | deepEqual @adaptor.position(), [0, 0] 150 | 151 | @press 'j<<' 152 | eq @adaptor.lineText(1), 'return _.pluck(_.map(obj, function(value, index, list) {' 153 | deepEqual @adaptor.position(), [1, 0] 154 | 155 | @press 'j4<<' 156 | eq @adaptor.lineText(2), ' return {' 157 | eq @adaptor.lineText(3), ' value : value,' 158 | eq @adaptor.lineText(4), ' criteria : iterator.call(context, value, index, list)' 159 | eq @adaptor.lineText(5), ' };' 160 | deepEqual @adaptor.position(), [2, 2] 161 | 162 | test 'r', -> 163 | @press 'r$' 164 | eq @adaptor.lineText(), '$.sortBy = function(obj, iterator, context) {' 165 | deepEqual @adaptor.position(), [0, 0] 166 | 167 | @press '3rz' 168 | eq @adaptor.lineText(), 'zzzortBy = function(obj, iterator, context) {' 169 | deepEqual @adaptor.position(), [0, 2] 170 | 171 | @press 'r\n' 172 | eq @adaptor.lineText(0), 'zz' 173 | eq @adaptor.lineText(), 'ortBy = function(obj, iterator, context) {' 174 | deepEqual @adaptor.position(), [1, 0] 175 | 176 | @press '3r\n' 177 | # three chars should be replaced with only one newline 178 | eq @adaptor.lineText(1), '' 179 | eq @adaptor.lineText(), 'By = function(obj, iterator, context) {' 180 | deepEqual @adaptor.position(), [2, 0] 181 | 182 | ok not @jim.registers['"'] 183 | 184 | test 'J', -> 185 | @press 'J' 186 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) {' 187 | deepEqual @adaptor.position(), [0, 45] 188 | 189 | @press 'j4J' 190 | eq @adaptor.lineText(), ' return { value : value, criteria : iterator.call(context, value, index, list) };' 191 | deepEqual @adaptor.position(), [1, 81] 192 | 193 | #TODO special case for lines starting with ")"?!?!?! 194 | 195 | test 'gJ', -> 196 | @press 'gJ' 197 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) {' 198 | deepEqual @adaptor.position(), [0, 45] 199 | 200 | @press 'j4gJ' 201 | eq @adaptor.lineText(), ' return { value : value, criteria : iterator.call(context, value, index, list) };' 202 | deepEqual @adaptor.position(), [1, 91] 203 | 204 | test 'ctrl-[ escapes', -> 205 | @press 'i', 'ctrl-[' 206 | eq @jim.mode.name, 'normal' 207 | 208 | test 'delete', -> 209 | @press @delete 210 | eq @jim.registers['"'], '_' 211 | 212 | @press @delete, @delete 213 | eq @jim.registers['"'], 's' 214 | -------------------------------------------------------------------------------- /test/ace/dot_command.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: dot command', 2 | setup: setupAceTests 3 | 4 | test 'repeating basic commands', -> 5 | @press 'x..' 6 | eq @adaptor.lineText(), 'ortBy = function(obj, iterator, context) {' 7 | 8 | test 'repeating commands with arbitrary chars', -> 9 | @press 'r 2W.W.' 10 | eq @adaptor.lineText(), ' .sortBy unction(obj, iterator, context) {' 11 | 12 | test 'repeating operations', -> 13 | @press 'dw..' 14 | eq @adaptor.lineText(), '= function(obj, iterator, context) {' 15 | 16 | test 'repeating commands that are Change operations', -> 17 | @press '4WCwham!', @esc, 'j.' 18 | eq @adaptor.lineText(), ' return _.pluck(_.map(obj, function(vawham!' # vawham!!!11 19 | 20 | test 'repeating inserts', -> 21 | @press 'AtheEnd', @esc, 'j.' 22 | eq @adaptor.lineText(), ' return _.pluck(_.map(obj, function(value, index, list) {theEnd' 23 | 24 | @press 'j^cwwhoa', @esc, 'j.' 25 | eq @adaptor.lineText(), ' vwhoa : value,' 26 | 27 | test 'repeating inserts in which the users arrows around', -> 28 | @press 'Cfirstline', @down, 'secondLine', @esc, 'j^.' 29 | eq @adaptor.lineText(), ' secondLinereturn {' 30 | 31 | test 'repeating inserts that have been undone', -> 32 | @press 'isomething', @esc, 'uW.' 33 | eq @adaptor.lineText(), '_.sortBy something= function(obj, iterator, context) {' 34 | 35 | test 'repeating linewise changes', -> 36 | @press 'cjone line now', @esc, 'j.' 37 | eq @adaptor.lineText(), 'one line now' 38 | eq @adaptor.lastRow(), 13 39 | 40 | test 'repeating characterwise visual commands', -> 41 | @press 'WvjdW.' 42 | eq @adaptor.lineText(), '_.sortBy .pluck(_.map(obj, {' 43 | 44 | test 'repeating single line characterwise visual commands', -> 45 | @press 'v10ld10l.' 46 | eq @adaptor.lineText(), 'function(or, context) {' 47 | 48 | test 'repeating linewise visual commands', -> 49 | @press 'VjjJj.' 50 | eq @adaptor.lineText(), ' value : value, criteria : iterator.call(context, value, index, list) };' 51 | 52 | test 'repeating linewise visual operators', -> 53 | @press 'Vj>j.' 54 | eq @adaptor.lineText(0), ' _.sortBy = function(obj, iterator, context) {' 55 | eq @adaptor.lineText(1), ' return _.pluck(_.map(obj, function(value, index, list) {' 56 | eq @adaptor.lineText(2), ' return {' 57 | eq @adaptor.lineText(3), ' value : value,' 58 | 59 | test 'repeating words that have some deletion', -> 60 | @press 'iExtra punctuation.!', @backspace, @esc 61 | eq @adaptor.lastInsert().string, 'Extra punctuation.' 62 | 63 | @press 'iI am misspelleed', @backspace, @backspace, 'd', @esc 64 | eq @adaptor.lastInsert().string, 'I am misspelled' 65 | 66 | @press 'iWill be. deleted' 67 | @press @backspace, @backspace, @backspace, @backspace, @backspace, @backspace, @backspace, @backspace 68 | @press @esc 69 | eq @adaptor.lastInsert().string, 'Will be.' 70 | 71 | @press 'i bc', @esc, 'O', @esc, 'iabc', @down, @backspace, @backspace, 'bc', @esc 72 | eq @adaptor.lastInsert().string, 'bc' 73 | 74 | @press 'iabc\nbc', @backspace, @backspace, 'de', @esc 75 | eq endings(@adaptor.lastInsert().string), 'abc\nde' 76 | -------------------------------------------------------------------------------- /test/ace/insert_mode.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: insert mode', 2 | setup: setupAceTests 3 | 4 | test 'leaves the cursor in the right spot', -> 5 | @press 'Wi', @esc 6 | deepEqual @adaptor.position(), [0, 8] 7 | @press 'i', @esc 8 | deepEqual @adaptor.position(), [0, 7] 9 | @press 'a', @esc 10 | deepEqual @adaptor.position(), [0, 7] 11 | -------------------------------------------------------------------------------- /test/ace/motions.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: motions', 2 | setup: setupAceTests 3 | 4 | test 'h', -> 5 | @adaptor.moveTo 1, 14 6 | 7 | @press 'h' 8 | deepEqual @adaptor.position(), [1, 13] 9 | @press 'h' 10 | deepEqual @adaptor.position(), [1, 12] 11 | @press '12h' 12 | deepEqual @adaptor.position(), [1, 0] 13 | @press 'h' 14 | deepEqual @adaptor.position(), [1, 0] 15 | 16 | test 'left', -> 17 | @adaptor.moveTo 1, 14 18 | 19 | @press @left 20 | deepEqual @adaptor.position(), [1, 13] 21 | @press @left 22 | deepEqual @adaptor.position(), [1, 12] 23 | @press '12', @left 24 | deepEqual @adaptor.position(), [1, 0] 25 | @press @left 26 | deepEqual @adaptor.position(), [1, 0] 27 | 28 | test 'j', -> 29 | @press 'j' 30 | deepEqual @adaptor.position(), [1, 0] 31 | @press 'j' 32 | deepEqual @adaptor.position(), [2, 0] 33 | @press '12j' 34 | deepEqual @adaptor.position(), [14, 0] 35 | 36 | # this will fail, but it seems to be an issue with Ace thinking there's another line 37 | # where there shouldn't be... 38 | #@press 'j' 39 | #deepEqual @adaptor.position(), [14, 0] 40 | 41 | test 'down', -> 42 | @press @down 43 | deepEqual @adaptor.position(), [1, 0] 44 | @press @down 45 | deepEqual @adaptor.position(), [2, 0] 46 | @press '12', @down 47 | deepEqual @adaptor.position(), [14, 0] 48 | 49 | test 'k', -> 50 | @adaptor.moveTo 5, 0 51 | 52 | @press 'k' 53 | deepEqual @adaptor.position(), [4, 0] 54 | @press 'k' 55 | deepEqual @adaptor.position(), [3, 0] 56 | @press '3k' 57 | deepEqual @adaptor.position(), [0, 0] 58 | @press 'k' 59 | deepEqual @adaptor.position(), [0, 0] 60 | 61 | test 'up', -> 62 | @adaptor.moveTo 5, 0 63 | 64 | @press @up 65 | deepEqual @adaptor.position(), [4, 0] 66 | @press @up 67 | deepEqual @adaptor.position(), [3, 0] 68 | @press '3', @up 69 | deepEqual @adaptor.position(), [0, 0] 70 | @press @up 71 | deepEqual @adaptor.position(), [0, 0] 72 | 73 | test 'l', -> 74 | @press 'l' 75 | deepEqual @adaptor.position(), [0, 1] 76 | @press 'l' 77 | deepEqual @adaptor.position(), [0, 2] 78 | @press '42l' 79 | deepEqual @adaptor.position(), [0, 44] 80 | @press 'l' 81 | deepEqual @adaptor.position(), [0, 44] 82 | 83 | test 'right', -> 84 | @press @right 85 | deepEqual @adaptor.position(), [0, 1] 86 | @press @right 87 | deepEqual @adaptor.position(), [0, 2] 88 | @press '42', @right 89 | deepEqual @adaptor.position(), [0, 44] 90 | @press @right 91 | deepEqual @adaptor.position(), [0, 44] 92 | 93 | test 'E', -> 94 | @press 'E' 95 | deepEqual @adaptor.position(), [0, 7] 96 | @press 'E' 97 | deepEqual @adaptor.position(), [0, 9] 98 | @press 'E' 99 | deepEqual @adaptor.position(), [0, 23] 100 | @press '3E' 101 | deepEqual @adaptor.position(), [0, 44] 102 | @press 'E' 103 | deepEqual @adaptor.position(), [1, 7] 104 | @press '21E' 105 | deepEqual @adaptor.position(), [7, 6] 106 | 107 | test 'W', -> 108 | @press 'W' 109 | deepEqual @adaptor.position(), [0, 9] 110 | @press 'W' 111 | deepEqual @adaptor.position(), [0, 11] 112 | @press 'W' 113 | deepEqual @adaptor.position(), [0, 25] 114 | @press '2W' 115 | deepEqual @adaptor.position(), [0, 44] 116 | @press 'W' 117 | deepEqual @adaptor.position(), [1, 2] 118 | @press '18W' 119 | deepEqual @adaptor.position(), [6, 2] 120 | 121 | test 'B', -> 122 | @adaptor.moveTo 4, 15 123 | 124 | @press 'B' 125 | deepEqual @adaptor.position(), [4, 6] 126 | @press 'B' 127 | deepEqual @adaptor.position(), [3, 14] 128 | @press '12B' 129 | deepEqual @adaptor.position(), [0, 35] 130 | 131 | test 'e', -> 132 | @press 'e' 133 | deepEqual @adaptor.position(), [0, 1] 134 | @press 'e' 135 | deepEqual @adaptor.position(), [0, 7] 136 | @press 'e' 137 | deepEqual @adaptor.position(), [0, 9] 138 | @press 'e' 139 | deepEqual @adaptor.position(), [0, 18] 140 | @press 'e' 141 | deepEqual @adaptor.position(), [0, 19] 142 | @press 'e' 143 | deepEqual @adaptor.position(), [0, 22] 144 | @press '6e' 145 | deepEqual @adaptor.position(), [0, 44] 146 | @press 'e' 147 | deepEqual @adaptor.position(), [1, 7] 148 | @press '28e' 149 | deepEqual @adaptor.position(), [4, 24] 150 | 151 | test 'w', -> 152 | @press 'w' 153 | deepEqual @adaptor.position(), [0, 1] 154 | @press 'w' 155 | deepEqual @adaptor.position(), [0, 2] 156 | @press 'w' 157 | deepEqual @adaptor.position(), [0, 9] 158 | @press 'w' 159 | deepEqual @adaptor.position(), [0, 11] 160 | @press '8w' 161 | deepEqual @adaptor.position(), [0, 44] 162 | @press 'w' 163 | deepEqual @adaptor.position(), [1, 2] 164 | @press '3w' 165 | deepEqual @adaptor.position(), [1, 11] 166 | @press 'w' 167 | deepEqual @adaptor.position(), [1, 16] 168 | @press 'w' 169 | deepEqual @adaptor.position(), [1, 17] 170 | 171 | test 'b', -> 172 | @adaptor.moveTo 4, 15 173 | 174 | @press 'b' 175 | deepEqual @adaptor.position(), [4, 6] 176 | @press 'b' 177 | deepEqual @adaptor.position(), [3, 19] 178 | @press 'b' 179 | deepEqual @adaptor.position(), [3, 14] 180 | @press 'b' 181 | deepEqual @adaptor.position(), [3, 12] 182 | @press 'b' 183 | deepEqual @adaptor.position(), [3, 6] 184 | @press 'b' 185 | deepEqual @adaptor.position(), [2, 11] 186 | @press '17b' 187 | deepEqual @adaptor.position(), [1, 16] 188 | 189 | test 'w, W EOL behavior in operations', -> 190 | @press '$cWdo', @esc 191 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) do' 192 | 193 | test '^', -> 194 | @press '^' 195 | deepEqual @adaptor.position(), [0, 0] 196 | 197 | @press 'j^' 198 | deepEqual @adaptor.position(), [1, 2] 199 | 200 | @press '2W^' 201 | deepEqual @adaptor.position(), [1, 2] 202 | 203 | test '$', -> 204 | @press '$' 205 | deepEqual @adaptor.position(), [0, 44] 206 | 207 | @press '3$' 208 | deepEqual @adaptor.position(), [2, 11] 209 | 210 | test '0', -> 211 | @adaptor.moveTo 0, 7 212 | @press '0' 213 | deepEqual @adaptor.position(), [0, 0] 214 | 215 | test 'G', -> 216 | @press '3G' 217 | deepEqual @adaptor.position(), [2, 4] 218 | @press 'G' 219 | deepEqual @adaptor.position(), [15, 0] 220 | @press '1G' 221 | deepEqual @adaptor.position(), [0, 0] 222 | 223 | test 'gg', -> 224 | @press '4gg' 225 | deepEqual @adaptor.position(), [3, 6] 226 | @press 'gg' 227 | deepEqual @adaptor.position(), [0, 0] 228 | 229 | test 'f', -> 230 | @press 'fz' 231 | deepEqual @adaptor.position(), [0, 0] 232 | 233 | @press '2f(' 234 | deepEqual @adaptor.position(), [0, 0] 235 | 236 | @press 'fu' # hahaha 237 | deepEqual @adaptor.position(), [0, 12] 238 | 239 | @press '2f,' 240 | deepEqual @adaptor.position(), [0, 33] 241 | 242 | test 't', -> 243 | @press 'tz' 244 | deepEqual @adaptor.position(), [0, 0] 245 | 246 | @press '2t(' 247 | deepEqual @adaptor.position(), [0, 0] 248 | 249 | @press 'tu' 250 | deepEqual @adaptor.position(), [0, 11] 251 | 252 | @press '2t,' 253 | deepEqual @adaptor.position(), [0, 32] 254 | 255 | test 'F', -> 256 | @press '$Fz' 257 | deepEqual @adaptor.position(), [0, 44] 258 | 259 | @press '2F)' 260 | deepEqual @adaptor.position(), [0, 44] 261 | 262 | @press 'F)' 263 | deepEqual @adaptor.position(), [0, 42] 264 | 265 | @press '2F,' 266 | deepEqual @adaptor.position(), [0, 23] 267 | 268 | test 'T', -> 269 | @press '$Tz' 270 | deepEqual @adaptor.position(), [0, 44] 271 | 272 | @press '2T)' 273 | deepEqual @adaptor.position(), [0, 44] 274 | 275 | @press 'T)' 276 | deepEqual @adaptor.position(), [0, 43] 277 | 278 | @press '2T,' 279 | deepEqual @adaptor.position(), [0, 24] 280 | 281 | test 'H', -> 282 | @renderer.getFirstFullyVisibleRow = -> 0 283 | 284 | @press '3H' 285 | deepEqual @adaptor.position(), [2, 4] 286 | 287 | @press 'H' 288 | deepEqual @adaptor.position(), [0, 0] 289 | 290 | test 'M', -> 291 | @renderer.getFirstFullyVisibleRow = -> 0 292 | @renderer.getLastFullyVisibleRow = -> 14 293 | 294 | @press '4M' # the 4 should have no effect 295 | deepEqual @adaptor.position(), [7, 4] 296 | 297 | @press 'M' 298 | deepEqual @adaptor.position(), [7, 4] 299 | 300 | # odd number of rows 301 | @renderer.getLastFullyVisibleRow = -> 13 302 | @press 'ddM' 303 | deepEqual @adaptor.position(), [6, 2] 304 | 305 | test 'L', -> 306 | @renderer.getLastFullyVisibleRow = -> 14 307 | 308 | @press '6L' 309 | deepEqual @adaptor.position(), [9, 2] 310 | 311 | @press 'L' 312 | deepEqual @adaptor.position(), [14, 0] 313 | 314 | test 'backspace', -> 315 | @adaptor.moveTo 1, 0 316 | 317 | @press '16', @backspace 318 | deepEqual @adaptor.position(), [0, 30] 319 | 320 | @press @backspace 321 | deepEqual @adaptor.position(), [0, 29] 322 | 323 | @press @backspace 324 | deepEqual @adaptor.position(), [0, 28] 325 | 326 | @press @backspace 327 | deepEqual @adaptor.position(), [0, 27] 328 | 329 | test 'space', -> 330 | @press '$', @space 331 | deepEqual @adaptor.position(), [1, 0] 332 | 333 | @press '5', @space 334 | deepEqual @adaptor.position(), [1, 6] 335 | 336 | @press @space 337 | deepEqual @adaptor.position(), [1, 7] 338 | 339 | @press @space 340 | deepEqual @adaptor.position(), [1, 8] 341 | -------------------------------------------------------------------------------- /test/ace/operators.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: operators', 2 | setup: setupAceTests 3 | 4 | test 'c', -> 5 | @press 'cWsorta', @esc 6 | eq @adaptor.lineText(), "sorta = function(obj, iterator, context) {" 7 | 8 | @adaptor.moveTo 0, 21 9 | @press '2c2bfunky(', @esc 10 | eq @adaptor.lineText(), "sorta = funky( iterator, context) {" 11 | 12 | eq @adaptor.lastRow(), 15 13 | @adaptor.moveTo 6, 0 14 | @press 'ckkablammo!' 15 | eq @adaptor.lineText(), "kablammo!" 16 | eq @adaptor.lastRow(), 14 17 | 18 | test 'd', -> 19 | @adaptor.moveTo 0, 11 20 | @press 'd11W' 21 | eq @adaptor.lineText(), "_.sortBy = {" 22 | 23 | eq @adaptor.lastRow(), 13 24 | @press 'dj' 25 | eq @adaptor.lastRow(), 11 26 | 27 | test 'y', -> 28 | @press 'y3W' 29 | eq @jim.registers['"'], "_.sortBy = function(obj, " 30 | 31 | @press 'yj' 32 | eq endings(@jim.registers['"']), """ 33 | _.sortBy = function(obj, iterator, context) { 34 | return _.pluck(_.map(obj, function(value, index, list) { 35 | 36 | """ 37 | 38 | @press '$hy4l' 39 | eq @jim.registers['"'], ' {' 40 | 41 | test '>', -> 42 | @press '>3G' 43 | eq @adaptor.lineText(0), ' _.sortBy = function(obj, iterator, context) {' 44 | eq @adaptor.lineText(1), ' return _.pluck(_.map(obj, function(value, index, list) {' 45 | eq @adaptor.lineText(2), ' return {' 46 | deepEqual @adaptor.position(), [0, 2] 47 | 48 | test '<', -> 49 | @press 'j 55 | @press 'cql' 56 | 57 | # makes no changes and moves one char right 58 | deepEqual @adaptor.position(), [0, 1] 59 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) {' 60 | 61 | test 'work with motions that are followed by something', -> 62 | @press 'ct(thwap!', @esc 63 | eq @adaptor.lineText(), 'thwap!(obj, iterator, context) {' 64 | -------------------------------------------------------------------------------- /test/ace/search.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: search', 2 | setup: -> 3 | setupAceTests.call this 4 | @windowPrompt = window.prompt 5 | 6 | teardown: -> 7 | window.prompt = @windowPrompt 8 | 9 | test '/', -> 10 | window.prompt = -> "or" 11 | @press '/' 12 | deepEqual @adaptor.position(), [0, 3] 13 | 14 | @press 'n' 15 | deepEqual @adaptor.position(), [0, 31] 16 | 17 | test '?', -> 18 | window.prompt = -> "value" 19 | @press '?' 20 | deepEqual @adaptor.position(), [9, 7] 21 | 22 | @press 'n' 23 | deepEqual @adaptor.position(), [4, 40] 24 | 25 | test '*', -> 26 | @adaptor.moveTo 0, 14 27 | @press '*' 28 | deepEqual @adaptor.position(), [1, 28] 29 | 30 | @press 'n' 31 | deepEqual @adaptor.position(), [6, 10] 32 | 33 | @press 'N' 34 | deepEqual @adaptor.position(), [1, 28] 35 | 36 | @press '2*' 37 | deepEqual @adaptor.position(), [0, 11] 38 | 39 | @adaptor.moveTo 7, 8 40 | @press '*' 41 | # asserting that only whole word are matched and not just any instance of "a" 42 | deepEqual @adaptor.position(), [8, 11] 43 | 44 | @press '2n' 45 | deepEqual @adaptor.position(), [7, 8] 46 | 47 | test '#', -> 48 | @adaptor.moveTo 1, 4 49 | @press '#' 50 | deepEqual @adaptor.position(), [8, 4] 51 | 52 | @press 'n' 53 | deepEqual @adaptor.position(), [2, 4] 54 | 55 | @press 'N' 56 | deepEqual @adaptor.position(), [8, 4] 57 | 58 | @press '2#' 59 | deepEqual @adaptor.position(), [1, 2] 60 | 61 | @adaptor.moveTo 7, 8 62 | @press '#' 63 | # asserting that only whole word are matched and not just any instance of "a" 64 | deepEqual @adaptor.position(), [8, 24] 65 | 66 | @press 'N' 67 | deepEqual @adaptor.position(), [7, 8] 68 | 69 | 70 | # there are four different rules for what search Vim uses for * and # 71 | # http://vimdoc.sourceforge.net/htmldoc/pattern.html#star 72 | 73 | # rule #1 74 | test '* or # will match the keyword under the cursor', -> 75 | # match the underscore 76 | @press '#' 77 | deepEqual @adaptor.position(), [1, 17] 78 | 79 | # rule #2 80 | test '* or # will match the keyword after the cursor', -> 81 | @adaptor.moveTo 0, 8 82 | # match "function" since the equals sign and spaces aren't keywords 83 | @press '*' 84 | deepEqual @adaptor.position(), [1, 28] 85 | 86 | # rule #3 87 | test '* or # will match the non-blank word under the cursor', -> 88 | @adaptor.moveTo 5, 4 89 | # match "};", since there aren't any keywords on the line after the cursor 90 | @press '#' 91 | deepEqual @adaptor.position(), [10, 0] 92 | 93 | # rule #4 94 | test '* or # will match the non-blank word after the cursor', -> 95 | @adaptor.moveTo 0, 43 96 | # match the curly brace, since there aren't any keywords on the line after the cursor 97 | # and the curly brace is a non-blank 98 | @press '*' 99 | deepEqual @adaptor.position(), [1, 57] 100 | -------------------------------------------------------------------------------- /test/ace/undo.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: undo', 2 | setup: setupAceTests 3 | 4 | test 'undoing commands', -> 5 | @press 'xxu' 6 | eq @adaptor.lineText(), '.sortBy = function(obj, iterator, context) {' 7 | 8 | test 'undoing multiple commands', -> 9 | @press 'xx2u' 10 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) {' 11 | 12 | test 'undoing inserts', -> 13 | @press 'Aend', @esc, 'Ibegin', @esc 14 | eq @adaptor.lineText(), 'begin_.sortBy = function(obj, iterator, context) {end' 15 | @press 'u' 16 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) {end' 17 | @press 'u' 18 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) {' 19 | 20 | test 'undoing replaces', -> 21 | @press 'Rundo', @esc, '2WRthing', @esc 22 | eq @adaptor.lineText(), 'undortBy = thingion(obj, iterator, context) {' 23 | 24 | @press 'u' 25 | eq @adaptor.lineText(), 'undortBy = function(obj, iterator, context) {' 26 | 27 | @press 'u' 28 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) {' 29 | 30 | test 'undoing visual operators', -> 31 | @press 'Wv>u' 32 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) {' 33 | -------------------------------------------------------------------------------- /test/ace/unhandled_keys.coffee: -------------------------------------------------------------------------------- 1 | module 'unhandled keys', 2 | setup: setupAceTests 3 | 4 | test 'arrow keys', -> 5 | @press @down, @down 6 | eq @adaptor.editor.session.getValue(), @sort_by_js 7 | deepEqual @adaptor.position(), [2, 0] 8 | -------------------------------------------------------------------------------- /test/ace/visual_mode.coffee: -------------------------------------------------------------------------------- 1 | module 'Ace: visual mode', 2 | setup: setupAceTests 3 | 4 | test 'motions', -> 5 | @press 'v3Gd' 6 | deepEqual @adaptor.position(), [0, 0] 7 | eq @adaptor.lineText(), 'eturn {' 8 | 9 | @press 'v2ly' 10 | eq @jim.registers['"'], 'etu' 11 | deepEqual @adaptor.position(), [0, 0] 12 | 13 | test 'making a backwards selection', -> 14 | @press 'Wvhhy' 15 | eq @jim.registers['"'], 'y =' 16 | 17 | test 'transition from a backwards selection to forwards', -> 18 | @press 'Wvhllly' 19 | eq @jim.registers['"'], '= f' 20 | 21 | test 'linewise selections', -> 22 | @press 'lllVjd' 23 | eq @adaptor.lastRow(), 13 24 | eq @adaptor.lineText(), " return {" 25 | eq @jim.mode.name, 'normal' 26 | 27 | @press 'jjjV2kd' 28 | eq @adaptor.lastRow(), 10 29 | eq @adaptor.row(), 1 30 | eq @jim.mode.name, 'normal' 31 | 32 | test 'characterwise changes', -> 33 | @press '2WvEchi!', @esc 34 | eq @adaptor.lineText(0), '_.sortBy = hi! iterator, context) {' 35 | eq @jim.mode.name, 'normal' 36 | 37 | test 'linewise changes', -> 38 | eq @adaptor.lastRow(), 15 39 | @press 'Vjcnew line!', @esc 40 | eq @adaptor.lastRow(), 14 41 | deepEqual @adaptor.position(), [0, 8] 42 | eq @adaptor.lineText(0), 'new line!' 43 | 44 | test 'p, P', -> 45 | # p and P do the same thing in visual mode 46 | @press 'xlvep' 47 | eq @adaptor.lineText(), '._ = function(obj, iterator, context) {' 48 | eq @jim.mode.name, 'normal' 49 | 50 | @press 'wv3P' 51 | eq @adaptor.lineText(), '._ sortBysortBysortBy function(obj, iterator, context) {' 52 | eq @jim.mode.name, 'normal' 53 | 54 | test 'J', -> 55 | @press 'vjJ' 56 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) {' 57 | deepEqual @adaptor.position(), [0, 45] 58 | eq @jim.mode.name, 'normal' 59 | 60 | @press 'jVjjjJ' 61 | eq @adaptor.lineText(), ' return { value : value, criteria : iterator.call(context, value, index, list) };' 62 | deepEqual @adaptor.position(), [1, 81] 63 | eq @jim.mode.name, 'normal' 64 | 65 | #TODO special case for lines starting with ")"?!?!?! 66 | 67 | test 'gJ', -> 68 | @press 'vlgJ' 69 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) {' 70 | deepEqual @adaptor.position(), [0, 45] 71 | eq @jim.mode.name, 'normal' 72 | 73 | @press 'jv3jgJ' 74 | eq @adaptor.lineText(), ' return { value : value, criteria : iterator.call(context, value, index, list) };' 75 | deepEqual @adaptor.position(), [1, 91] 76 | eq @jim.mode.name, 'normal' 77 | 78 | test 'linewise paste over a linewise selection', -> 79 | firstLine = @adaptor.lineText 0 80 | fourthLine = @adaptor.lineText 3 81 | lastRow = @adaptor.lastRow() 82 | 83 | # replace lines 2 & 3 with line 1 (threw the `l`'s in there fo fun) 84 | @press 'yyjlllVjp' 85 | eq @adaptor.lineText(1), firstLine 86 | eq @adaptor.lineText(2), fourthLine 87 | eq @adaptor.lastRow(), lastRow - 1 88 | 89 | test 'linewise gJ', -> 90 | lastRow = @adaptor.lastRow() 91 | 92 | @press 'lllVjgJ' 93 | eq @adaptor.lineText(), '_.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) {' 94 | eq @adaptor.lastRow(), lastRow - 1 95 | 96 | test 'toggle visual mode', -> 97 | @press 'vW' 98 | eq @jim.mode.name, 'visual' 99 | ok not @jim.mode.linewise 100 | 101 | @press 'Vj' 102 | eq @jim.mode.name, 'visual' 103 | ok @jim.mode.linewise 104 | 105 | @press 'vy' 106 | eq endings(@jim.registers['"']), '_.sortBy = function(obj, iterator, context) {\n return _' 107 | 108 | @press 'vv' 109 | eq @jim.mode.name, 'normal' 110 | 111 | @press 'VV' 112 | eq @jim.mode.name, 'normal' 113 | 114 | @press 'WVvy' 115 | eq @jim.registers['"'], '=' 116 | 117 | @press 'vjVy' 118 | eq endings(@jim.registers['"']), """ 119 | _.sortBy = function(obj, iterator, context) { 120 | return _.pluck(_.map(obj, function(value, index, list) { 121 | 122 | """ 123 | 124 | test 'selection with arrow keys', -> 125 | @press 'v', @down, @right, @up, @left, 'y' 126 | eq @jim.registers['"'], '_.' 127 | 128 | @adaptor.moveTo 4, 6 129 | 130 | @press 'vw', @right, @right, 'Wy' 131 | eq @jim.registers['"'], 'criteria : iterator.call(context, v' 132 | 133 | test 'x, X, and delete with selection', -> 134 | @press 'vWx' 135 | eq @jim.registers['"'], '_.sortBy =' 136 | 137 | @press 'vWX' 138 | eq endings(@jim.registers['"']), ' function(obj, iterator, context) {\n' 139 | 140 | @press 'jvl', @delete 141 | eq @jim.registers['"'], ' r' 142 | 143 | test 'visual mode o', -> 144 | @adaptor.moveTo 3, 6 145 | @press 'vjo' 146 | deepEqual @adaptor.position(), [3, 6] 147 | 148 | @press 'o' 149 | deepEqual @adaptor.position(), [4, 6] 150 | 151 | @press 'o', @esc 152 | deepEqual @adaptor.position(), [3, 6] 153 | 154 | @press 'veo' 155 | deepEqual @adaptor.position(), [3, 6] 156 | 157 | @press 'o' 158 | deepEqual @adaptor.position(), [3, 10] 159 | 160 | @press 'o', @esc 161 | deepEqual @adaptor.position(), [3, 6] 162 | -------------------------------------------------------------------------------- /test/fixtures/sort_by.js: -------------------------------------------------------------------------------- 1 | _.sortBy = function(obj, iterator, context) { 2 | return _.pluck(_.map(obj, function(value, index, list) { 3 | return { 4 | value : value, 5 | criteria : iterator.call(context, value, index, list) 6 | }; 7 | }).sort(function(left, right) { 8 | var a = left.criteria, b = right.criteria; 9 | return a < b ? -1 : a > b ? 1 : 0; 10 | }), 'value'); 11 | }; 12 | 13 | // borrowed from: 14 | // Underscore.js 1.1.6 15 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. 16 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 19 | 20 | 21 | 98 | 99 | 100 |

Jim's tests

101 |

102 |
103 |

104 |
    105 |
    test markup, will be hidden
    106 | 107 | 108 | -------------------------------------------------------------------------------- /vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Fri Jun 17 01:45:01 UTC 2011 10 | * Last Commit: d4f23f8a882d13b71768503e2db9fa33ef169ba0 11 | */ 12 | 13 | /** Font Family and Sizes */ 14 | 15 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 16 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 20 | #qunit-tests { font-size: smaller; } 21 | 22 | 23 | /** Resets */ 24 | 25 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | 31 | /** Header */ 32 | 33 | #qunit-header { 34 | padding: 0.5em 0 0.5em 1em; 35 | 36 | color: #8699a4; 37 | background-color: #0d3349; 38 | 39 | font-size: 1.5em; 40 | line-height: 1em; 41 | font-weight: normal; 42 | 43 | border-radius: 15px 15px 0 0; 44 | -moz-border-radius: 15px 15px 0 0; 45 | -webkit-border-top-right-radius: 15px; 46 | -webkit-border-top-left-radius: 15px; 47 | } 48 | 49 | #qunit-header a { 50 | text-decoration: none; 51 | color: #c2ccd1; 52 | } 53 | 54 | #qunit-header a:hover, 55 | #qunit-header a:focus { 56 | color: #fff; 57 | } 58 | 59 | #qunit-banner { 60 | height: 5px; 61 | } 62 | 63 | #qunit-testrunner-toolbar { 64 | padding: 0.5em 0 0.5em 2em; 65 | color: #5E740B; 66 | background-color: #eee; 67 | } 68 | 69 | #qunit-userAgent { 70 | padding: 0.5em 0 0.5em 2.5em; 71 | background-color: #2b81af; 72 | color: #fff; 73 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 74 | } 75 | 76 | 77 | /** Tests: Pass/Fail */ 78 | 79 | #qunit-tests { 80 | list-style-position: inside; 81 | } 82 | 83 | #qunit-tests li { 84 | padding: 0.4em 0.5em 0.4em 2.5em; 85 | border-bottom: 1px solid #fff; 86 | list-style-position: inside; 87 | } 88 | 89 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 90 | display: none; 91 | } 92 | 93 | #qunit-tests li strong { 94 | cursor: pointer; 95 | } 96 | 97 | #qunit-tests li a { 98 | padding: 0.5em; 99 | color: #c2ccd1; 100 | text-decoration: none; 101 | } 102 | #qunit-tests li a:hover, 103 | #qunit-tests li a:focus { 104 | color: #000; 105 | } 106 | 107 | #qunit-tests ol { 108 | margin-top: 0.5em; 109 | padding: 0.5em; 110 | 111 | background-color: #fff; 112 | 113 | border-radius: 15px; 114 | -moz-border-radius: 15px; 115 | -webkit-border-radius: 15px; 116 | 117 | box-shadow: inset 0px 2px 13px #999; 118 | -moz-box-shadow: inset 0px 2px 13px #999; 119 | -webkit-box-shadow: inset 0px 2px 13px #999; 120 | } 121 | 122 | #qunit-tests table { 123 | border-collapse: collapse; 124 | margin-top: .2em; 125 | } 126 | 127 | #qunit-tests th { 128 | text-align: right; 129 | vertical-align: top; 130 | padding: 0 .5em 0 0; 131 | } 132 | 133 | #qunit-tests td { 134 | vertical-align: top; 135 | } 136 | 137 | #qunit-tests pre { 138 | margin: 0; 139 | white-space: pre-wrap; 140 | word-wrap: break-word; 141 | } 142 | 143 | #qunit-tests del { 144 | background-color: #e0f2be; 145 | color: #374e0c; 146 | text-decoration: none; 147 | } 148 | 149 | #qunit-tests ins { 150 | background-color: #ffcaca; 151 | color: #500; 152 | text-decoration: none; 153 | } 154 | 155 | /*** Test Counts */ 156 | 157 | #qunit-tests b.counts { color: black; } 158 | #qunit-tests b.passed { color: #5E740B; } 159 | #qunit-tests b.failed { color: #710909; } 160 | 161 | #qunit-tests li li { 162 | margin: 0.5em; 163 | padding: 0.4em 0.5em 0.4em 0.5em; 164 | background-color: #fff; 165 | border-bottom: none; 166 | list-style-position: inside; 167 | } 168 | 169 | /*** Passing Styles */ 170 | 171 | #qunit-tests li li.pass { 172 | color: #5E740B; 173 | background-color: #fff; 174 | border-left: 26px solid #C6E746; 175 | } 176 | 177 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 178 | #qunit-tests .pass .test-name { color: #366097; } 179 | 180 | #qunit-tests .pass .test-actual, 181 | #qunit-tests .pass .test-expected { color: #999999; } 182 | 183 | #qunit-banner.qunit-pass { background-color: #C6E746; } 184 | 185 | /*** Failing Styles */ 186 | 187 | #qunit-tests li li.fail { 188 | color: #710909; 189 | background-color: #fff; 190 | border-left: 26px solid #EE5757; 191 | } 192 | 193 | #qunit-tests > li:last-child { 194 | border-radius: 0 0 15px 15px; 195 | -moz-border-radius: 0 0 15px 15px; 196 | -webkit-border-bottom-right-radius: 15px; 197 | -webkit-border-bottom-left-radius: 15px; 198 | } 199 | 200 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 201 | #qunit-tests .fail .test-name, 202 | #qunit-tests .fail .module-name { color: #000000; } 203 | 204 | #qunit-tests .fail .test-actual { color: #EE5757; } 205 | #qunit-tests .fail .test-expected { color: green; } 206 | 207 | #qunit-banner.qunit-fail { background-color: #EE5757; } 208 | 209 | 210 | /** Result */ 211 | 212 | #qunit-testresult { 213 | padding: 0.5em 0.5em 0.5em 2.5em; 214 | 215 | color: #2b81af; 216 | background-color: #D2E0E6; 217 | 218 | border-bottom: 1px solid white; 219 | } 220 | 221 | /** Fixture */ 222 | 223 | #qunit-fixture { 224 | position: absolute; 225 | top: -10000px; 226 | left: -10000px; 227 | } 228 | -------------------------------------------------------------------------------- /vendor/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 0.24.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/jrburke/requirejs for details 5 | */ 6 | /*jslint regexp: false, nomen: false, plusplus: false, strict: false */ 7 | /*global require: false, XMLHttpRequest: false, ActiveXObject: false, 8 | define: false, window: false, process: false, Packages: false, 9 | java: false */ 10 | 11 | (function () { 12 | var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 13 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 14 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 15 | buildMap = []; 16 | 17 | define(function () { 18 | var text, get, fs; 19 | 20 | if (typeof window !== "undefined" && window.navigator && window.document) { 21 | get = function (url, callback) { 22 | var xhr = text.createXhr(); 23 | xhr.open('GET', url, true); 24 | xhr.onreadystatechange = function (evt) { 25 | //Do not explicitly handle errors, those should be 26 | //visible via console output in the browser. 27 | if (xhr.readyState === 4) { 28 | callback(xhr.responseText); 29 | } 30 | }; 31 | xhr.send(null); 32 | }; 33 | } else if (typeof process !== "undefined" && 34 | process.versions && 35 | !!process.versions.node) { 36 | //Using special require.nodeRequire, something added by r.js. 37 | fs = require.nodeRequire('fs'); 38 | 39 | get = function (url, callback) { 40 | callback(fs.readFileSync(url, 'utf8')); 41 | }; 42 | } else if (typeof Packages !== 'undefined') { 43 | //Why Java, why is this so awkward? 44 | get = function (url, callback) { 45 | var encoding = "utf-8", 46 | file = new java.io.File(url), 47 | lineSeparator = java.lang.System.getProperty("line.separator"), 48 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 49 | stringBuffer, line, 50 | content = ''; 51 | try { 52 | stringBuffer = new java.lang.StringBuffer(); 53 | line = input.readLine(); 54 | 55 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 56 | // http://www.unicode.org/faq/utf_bom.html 57 | 58 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 59 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 60 | if (line && line.length() && line.charAt(0) === 0xfeff) { 61 | // Eat the BOM, since we've already found the encoding on this file, 62 | // and we plan to concatenating this buffer with others; the BOM should 63 | // only appear at the top of a file. 64 | line = line.substring(1); 65 | } 66 | 67 | stringBuffer.append(line); 68 | 69 | while ((line = input.readLine()) !== null) { 70 | stringBuffer.append(lineSeparator); 71 | stringBuffer.append(line); 72 | } 73 | //Make sure we return a JavaScript string and not a Java string. 74 | content = String(stringBuffer.toString()); //String 75 | } finally { 76 | input.close(); 77 | } 78 | callback(content); 79 | }; 80 | } 81 | 82 | text = { 83 | version: '0.24.0', 84 | 85 | strip: function (content) { 86 | //Strips declarations so that external SVG and XML 87 | //documents can be added to a document without worry. Also, if the string 88 | //is an HTML document, only the part inside the body tag is returned. 89 | if (content) { 90 | content = content.replace(xmlRegExp, ""); 91 | var matches = content.match(bodyRegExp); 92 | if (matches) { 93 | content = matches[1]; 94 | } 95 | } else { 96 | content = ""; 97 | } 98 | return content; 99 | }, 100 | 101 | jsEscape: function (content) { 102 | return content.replace(/(['\\])/g, '\\$1') 103 | .replace(/[\f]/g, "\\f") 104 | .replace(/[\b]/g, "\\b") 105 | .replace(/[\n]/g, "\\n") 106 | .replace(/[\t]/g, "\\t") 107 | .replace(/[\r]/g, "\\r"); 108 | }, 109 | 110 | createXhr: function () { 111 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 112 | var xhr, i, progId; 113 | if (typeof XMLHttpRequest !== "undefined") { 114 | return new XMLHttpRequest(); 115 | } else { 116 | for (i = 0; i < 3; i++) { 117 | progId = progIds[i]; 118 | try { 119 | xhr = new ActiveXObject(progId); 120 | } catch (e) {} 121 | 122 | if (xhr) { 123 | progIds = [progId]; // so faster next time 124 | break; 125 | } 126 | } 127 | } 128 | 129 | if (!xhr) { 130 | throw new Error("require.getXhr(): XMLHttpRequest not available"); 131 | } 132 | 133 | return xhr; 134 | }, 135 | 136 | get: get, 137 | 138 | load: function (name, req, onLoad, config) { 139 | //Name has format: some.module.filext!strip 140 | //The strip part is optional. 141 | //if strip is present, then that means only get the string contents 142 | //inside a body tag in an HTML string. For XML/SVG content it means 143 | //removing the declarations so the content can be inserted 144 | //into the current doc without problems. 145 | 146 | var strip = false, url, index = name.indexOf("."), 147 | modName = name.substring(0, index), 148 | ext = name.substring(index + 1, name.length); 149 | 150 | index = ext.indexOf("!"); 151 | if (index !== -1) { 152 | //Pull off the strip arg. 153 | strip = ext.substring(index + 1, ext.length); 154 | strip = strip === "strip"; 155 | ext = ext.substring(0, index); 156 | } 157 | 158 | //Load the text. 159 | url = req.nameToUrl(modName, "." + ext); 160 | text.get(url, function (content) { 161 | content = strip ? text.strip(content) : content; 162 | if (config.isBuild && config.inlineText) { 163 | buildMap[name] = content; 164 | } 165 | onLoad(content); 166 | }); 167 | }, 168 | 169 | write: function (pluginName, moduleName, write) { 170 | if (moduleName in buildMap) { 171 | var content = text.jsEscape(buildMap[moduleName]); 172 | write("define('" + pluginName + "!" + moduleName + 173 | "', function () { return '" + content + "';});\n"); 174 | } 175 | } 176 | }; 177 | 178 | return text; 179 | }); 180 | }()); 181 | --------------------------------------------------------------------------------