├── .gitignore ├── src ├── buffer │ ├── segments.test.js │ ├── indexer.js │ ├── syntax.bench.js │ ├── segments.bench.js │ ├── bench.js │ ├── tokens.bench.js │ ├── prefixtree.js │ ├── prefixtree.test.js │ ├── tokens.js │ ├── skipstring.bench.js │ ├── syntax.js │ ├── parts.js │ ├── buffer.old.js │ ├── segments.js │ ├── test.js │ ├── buffer.old.test.js │ ├── skipstring.js │ └── lines.js ├── views │ ├── view.js │ ├── ruler.js │ ├── caret.js │ ├── index.js │ ├── rows.js │ ├── mark.js │ ├── find.js │ ├── block.js │ └── code.js ├── file.js ├── input │ ├── index.js │ ├── mouse.js │ ├── text.js │ └── bindings.js ├── style.css ├── theme.js ├── history.js ├── history.test.js └── move.js ├── lib ├── merge.js ├── save.js ├── bind-raf.js ├── open.js ├── debounce.js ├── print.js ├── clone.js ├── throttle.js ├── diff.js ├── dialog │ ├── test │ │ ├── dialog.html.js │ │ └── dialog.html │ ├── style.css │ └── index.js ├── diff.test.js ├── parse.js ├── range-gate-and.js ├── range-gate-not.js ├── binary-search.js ├── atomic.js ├── memoize.js ├── set-immediate.js ├── trim.js ├── event.js ├── range.js ├── trim.test.js ├── box.js ├── regexp.js ├── xhr-require.js ├── point.js ├── area.js └── dom.js ├── test ├── index.html ├── test.js ├── bench.js ├── syntax.html ├── suite.js ├── index.css ├── broken.js ├── assert.js ├── index.js └── syntax.html.js ├── theme ├── theme.css ├── color-picker.css ├── theme.js ├── generator.js └── index.html ├── examples ├── default.html ├── default.js └── style.css ├── package.json ├── Makefile ├── README.md ├── dist └── jazz.css └── Issues /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/buffer/segments.test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/merge.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function merge(dest, src) { 3 | for (var key in src) { 4 | dest[key] = src[key]; 5 | } 6 | return dest; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/save.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = save; 3 | 4 | function save(url, src, cb) { 5 | return fetch(url, { 6 | method: 'POST', 7 | body: src, 8 | }) 9 | .then(cb.bind(null, null)) 10 | .catch(cb); 11 | } 12 | -------------------------------------------------------------------------------- /lib/bind-raf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(fn) { 2 | var request; 3 | return function rafWrap(a, b, c, d) { 4 | window.cancelAnimationFrame(request); 5 | request = window.requestAnimationFrame(fn.bind(this, a, b, c, d)); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/open.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = open; 3 | 4 | function open(url, cb) { 5 | return fetch(url) 6 | .then(getText) 7 | .then(cb.bind(null, null)) 8 | .catch(cb); 9 | } 10 | 11 | function getText(res) { 12 | return res.text(); 13 | } 14 | -------------------------------------------------------------------------------- /lib/debounce.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(fn, ms) { 3 | var timeout; 4 | 5 | return function debounceWrap(a, b, c, d) { 6 | clearTimeout(timeout); 7 | timeout = setTimeout(fn.bind(this, a, b, c, d), ms); 8 | return timeout; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lib/print.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = print; 3 | 4 | function print(o, depth) { 5 | var inspect = require('util').inspect; 6 | if ('object' === typeof o) { 7 | console.log(inspect(o, null, depth || null, true)); 8 | } else { 9 | console.log(o.length, o); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function clone(obj) { 3 | var o = {}; 4 | for (var key in obj) { 5 | var val = obj[key]; 6 | if ('object' === typeof val) { 7 | o[key] = clone(val); 8 | } else { 9 | o[key] = val; 10 | } 11 | } 12 | return o; 13 | }; 14 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/view.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = View; 3 | 4 | function View(editor) { 5 | this.editor = editor; 6 | } 7 | 8 | View.prototype.render = function() { 9 | throw new Error('render not implemented'); 10 | }; 11 | 12 | View.prototype.clear = function() { 13 | throw new Error('clear not implemented'); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/throttle.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(fn, ms) { 3 | var running, timeout; 4 | 5 | return function(a, b, c) { 6 | if (running) return; 7 | running = true; 8 | fn.call(this, a, b, c); 9 | setTimeout(reset, ms); 10 | }; 11 | 12 | function reset() { 13 | running = false; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/diff.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = diff; 3 | 4 | function diff(a, b) { 5 | if ('object' === typeof a) { 6 | var d = {}; 7 | var i = 0; 8 | for (var k in b) { 9 | if (a[k] !== b[k]) { 10 | d[k] = b[k]; 11 | i++; 12 | } 13 | } 14 | if (i) return d; 15 | } else { 16 | return a !== b; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/dialog/test/dialog.html.js: -------------------------------------------------------------------------------- 1 | 2 | var Dialog = require('../'); 3 | 4 | var dialog = new Dialog('Find'); 5 | 6 | dialog.on('submit', function(s) { 7 | console.log(s); 8 | }); 9 | 10 | dialog.on('value', function(s) { 11 | console.log(s); 12 | }); 13 | 14 | dialog.open(); 15 | 16 | setTimeout(function() { 17 | dialog.info('5/16') 18 | }, 1000) 19 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = test; 3 | 4 | var meta = { 5 | left: '', 6 | right: '' 7 | }; 8 | 9 | function test(fn) { 10 | var error = null; 11 | var pass; 12 | 13 | try { 14 | fn(); 15 | pass = true; 16 | } catch(e) { 17 | e.meta = e.meta || meta; 18 | error = e; 19 | pass = false; 20 | } 21 | 22 | return { 23 | pass: pass, 24 | error: error 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/buffer/indexer.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Indexer; 3 | 4 | function Indexer(buffer) { 5 | this.buffer = buffer; 6 | } 7 | 8 | Indexer.prototype.find = function(s) { 9 | if (!s) return []; 10 | var offsets = []; 11 | var text = this.buffer.raw; 12 | var len = s.length; 13 | var index; 14 | while (~(index = text.indexOf(s, index + len))) { 15 | offsets.push(index); 16 | } 17 | return offsets; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/dialog/test/dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Test 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/diff.test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | 4 | module.exports = function(t, diff) { 5 | t('diff', function() { 6 | assert.equal({b:3}, diff({a:1,b:2},{a:1,b:3})); 7 | assert.equal({a:1,b:3}, diff({b:2},{a:1,b:3})); 8 | assert.equal({a:1,b:3}, diff({},{a:1,b:3})); 9 | assert.equal(undefined, diff({a:1,b:3},{a:1,b:3})); 10 | assert.equal(undefined, diff({a:1,b:3,c:4},{a:1,b:3})); 11 | assert.equal({a:2,d:5}, diff({a:1,b:3,c:4},{a:2,b:3,d:5})); 12 | }) 13 | } -------------------------------------------------------------------------------- /test/bench.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = bench; 3 | 4 | function bench(times, fn, name, mul) { 5 | var time = Date.now(); 6 | 7 | for (var i = times; i--;) { 8 | fn(); 9 | } 10 | 11 | var diff = Date.now() - time; 12 | 13 | times *= mul || 1; 14 | 15 | return { 16 | name: name, 17 | times: times, 18 | total: diff, 19 | persec: (1000 / (diff / times)).toFixed(2), 20 | perframe: ((1000 / 60) / (diff / times)).toFixed(2), 21 | single: (diff / times).toFixed(4) 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | var TOKENS = /.+?\b|.\B|\b.+?/g; 2 | var WORD = /[./\\\(\)"'\-:,.;<>~!@#$%^&*\|\+=\[\]{}`~\? ]+/g; 3 | 4 | var parse = exports; 5 | 6 | parse.words = function(s) { 7 | var words = []; 8 | var word; 9 | 10 | while (word = WORD.exec(s)) { 11 | words.push(word); 12 | } 13 | 14 | return words; 15 | }; 16 | 17 | parse.tokens = function(s) { 18 | var words = []; 19 | var word; 20 | 21 | while (word = TOKENS.exec(s)) { 22 | words.push(word); 23 | } 24 | 25 | return words; 26 | }; 27 | -------------------------------------------------------------------------------- /test/syntax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Syntax 9 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/range-gate-and.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = AND; 3 | 4 | function AND(a, b) { 5 | var found = false; 6 | var range = null; 7 | var out = []; 8 | 9 | for (var i = a[0]; i <= a[1]; i++) { 10 | found = false; 11 | 12 | for (var j = 0; j < b.length; j++) { 13 | if (i >= b[j][0] && i <= b[j][1]) { 14 | found = true; 15 | break; 16 | } 17 | } 18 | 19 | if (found) { 20 | if (!range) { 21 | range = [i,i]; 22 | out.push(range); 23 | } 24 | range[1] = i; 25 | } else { 26 | range = null; 27 | } 28 | } 29 | 30 | return out; 31 | } 32 | -------------------------------------------------------------------------------- /lib/range-gate-not.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = NOT; 3 | 4 | function NOT(a, b) { 5 | var found = false; 6 | var range = null; 7 | var out = []; 8 | 9 | for (var i = a[0]; i <= a[1]; i++) { 10 | found = false; 11 | 12 | for (var j = 0; j < b.length; j++) { 13 | if (i >= b[j][0] && i <= b[j][1]) { 14 | found = true; 15 | break; 16 | } 17 | } 18 | 19 | if (!found) { 20 | if (!range) { 21 | range = [i,i]; 22 | out.push(range); 23 | } 24 | range[1] = i; 25 | } else { 26 | range = null; 27 | } 28 | } 29 | 30 | return out; 31 | } 32 | -------------------------------------------------------------------------------- /theme/theme.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; 6 | } 7 | 8 | #pickers { 9 | position: fixed; 10 | left: 0; 11 | width: 300px; 12 | top: 0; 13 | } 14 | 15 | .Scp { 16 | float: left; 17 | } 18 | 19 | #randomize { 20 | position: fixed; 21 | top: 450px; 22 | left: 160px; 23 | padding: 30px; 24 | } 25 | 26 | #colors { 27 | white-space: pre; 28 | position: fixed; 29 | left: 350px; 30 | } 31 | #code { 32 | font-size: 14px; 33 | white-space: pre; 34 | float: right; 35 | margin-right: 100px; 36 | } 37 | -------------------------------------------------------------------------------- /src/buffer/syntax.bench.js: -------------------------------------------------------------------------------- 1 | var Syntax = require('./syntax'); 2 | var bench = require('../../test/bench'); 3 | var print = require('../../lib/print'); 4 | var read = require('fs').readFileSync; 5 | 6 | var TIMES = 10e3; 7 | 8 | var code = 9 | read(__dirname + '/../../test/syntax.html.js', 'utf8'); 10 | // read(__dirname + '/../../babel.js', 'utf8'); 11 | 12 | var syntax = new Syntax; 13 | 14 | // var res = bench(TIMES, function() { 15 | // syntax.set(code); 16 | // }, 'syntax set'); 17 | 18 | // print(res); 19 | 20 | var res = bench(TIMES, function() { 21 | syntax.highlight(code); 22 | }, 'syntax highlight'); 23 | 24 | print(res); 25 | -------------------------------------------------------------------------------- /examples/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Jazz 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = suite; 3 | 4 | function suite(root, name, paths) { 5 | var setup = require(normalize(root, name)); 6 | var tests = paths 7 | .map(normalize.bind(null, root)) 8 | .map(require) 9 | .map(load.bind(null, setup)); 10 | 11 | return tests; 12 | } 13 | 14 | function load(setup, mod) { 15 | var t = {}; 16 | setup(add(t), mod); 17 | return t; 18 | } 19 | 20 | function add(t) { 21 | return function(desc, fn) { 22 | t[desc] = fn; 23 | }; 24 | } 25 | 26 | function normalize(root, path) { 27 | return root + '/' + path; 28 | } 29 | 30 | function merge(a, b) { 31 | for (var key in b) { 32 | a[key] = b[key]; 33 | } 34 | return a; 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jazz", 3 | "version": "0.0.1", 4 | "description": "Jazz is a code editor for the web", 5 | "author": "stagas", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/stagas/jazz.git" 9 | }, 10 | "keywords": [ 11 | "editor" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/stagas/jazz/issues" 15 | }, 16 | "homepage": "https://github.com/stagas/jazz#readme", 17 | "license": "MIT", 18 | "scripts": { 19 | "test": "make test" 20 | }, 21 | "main": "index.js", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "css-modulesify": "^0.25.1", 25 | "live-server": "^1.1.0", 26 | "watchify": "^3.7.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/binary-search.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = binarySearch; 3 | 4 | function binarySearch(array, compare) { 5 | var index = -1; 6 | var prev = -1; 7 | var low = 0; 8 | var high = array.length; 9 | if (!high) return { 10 | item: null, 11 | index: 0 12 | }; 13 | 14 | do { 15 | prev = index; 16 | index = low + (high - low >> 1); 17 | var item = array[index]; 18 | var result = compare(item); 19 | 20 | if (result) low = index; 21 | else high = index; 22 | } while (prev !== index); 23 | 24 | if (item != null) { 25 | return { 26 | item: item, 27 | index: index 28 | }; 29 | } 30 | 31 | return { 32 | item: null, 33 | index: ~low * -1 - 1 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | @make dev-build & \ 3 | ./node_modules/.bin/live-server \ 4 | --no-browser \ 5 | --port=3000 \ 6 | --wait=200 \ 7 | --watch=dist/jazz.js,examples 8 | 9 | dev-build: 10 | @./node_modules/.bin/watchify \ 11 | --plugin [ css-modulesify -o dist/jazz.css ] \ 12 | --verbose \ 13 | --detect-globals=false \ 14 | --standalone Jazz \ 15 | --node \ 16 | --debug \ 17 | --entry index.js \ 18 | --outfile dist/jazz.js 19 | 20 | install: package.json 21 | @npm install 22 | 23 | todo: 24 | @grep -A 1 --color=always -nd recurse TODO lib src index.js 25 | 26 | test: 27 | @./node_modules/.bin/live-server \ 28 | --open=test \ 29 | --wait=100 \ 30 | --watch=lib,src,test 31 | 32 | .PHONY: dev dev-build todo test 33 | -------------------------------------------------------------------------------- /lib/atomic.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = atomic; 3 | 4 | // function atomic(fn) { 5 | // var stage = false; 6 | // var n = 0; 7 | 8 | // function wrap() { 9 | // if (stage) return n++; 10 | // else fn.call(this); 11 | // } 12 | 13 | // wrap.hold = function() { 14 | // stage = true; 15 | // n = n || 0; 16 | // }; 17 | 18 | // wrap.release = function(context) { 19 | // if (stage && n) { 20 | // stage = false; 21 | // n = 0; 22 | // fn.call(context); 23 | // } 24 | // }; 25 | 26 | // return wrap; 27 | // } 28 | 29 | function atomic(fn) { 30 | var request; 31 | 32 | return function(a, b, c) { 33 | clearImmediate(request); 34 | request = setImmediate(fn.bind(this, a, b, c)); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /examples/default.js: -------------------------------------------------------------------------------- 1 | var options = { 2 | theme: 'redbliss', 3 | // debug_layers: ['code'], 4 | // scroll_speed: 95, 5 | // hide_rows: true, 6 | center_horizontal: true, 7 | // center_vertical: false, 8 | // margin_left: 15, 9 | // gutter_margin: 20, 10 | }; 11 | 12 | var jazz = new Jazz(options); 13 | 14 | // jazz.use(document.body).open('ember.js', './').focus(); 15 | // jazz.use(document.body).open('babel.js', './').focus(); 16 | jazz.use(document.body).open('jazz.js', '../dist/').focus(); 17 | // jazz.use(document.body).open('jquery.js', './').focus(); 18 | // jazz.use(document.body).open('codemirror.js', './').focus(); 19 | // jazz.use(document.body).open('broken.js', '../test/').focus(); 20 | // jazz.use(document.body).open('index.js', '../').focus(); 21 | -------------------------------------------------------------------------------- /src/views/ruler.js: -------------------------------------------------------------------------------- 1 | var dom = require('../../lib/dom'); 2 | var css = require('../style.css'); 3 | var View = require('./view'); 4 | 5 | module.exports = RulerView; 6 | 7 | function RulerView(editor) { 8 | View.call(this, editor); 9 | this.name = 'ruler'; 10 | this.dom = dom(css.ruler); 11 | } 12 | 13 | RulerView.prototype.__proto__ = View.prototype; 14 | 15 | RulerView.prototype.use = function(target) { 16 | dom.append(target, this); 17 | }; 18 | 19 | RulerView.prototype.render = function() { 20 | dom.style(this, { 21 | top: 0, 22 | height: (this.editor.rows + this.editor.page.height) 23 | * this.editor.char.height 24 | + this.editor.pageRemainder.height 25 | }); 26 | }; 27 | 28 | RulerView.prototype.clear = function() { 29 | dom.style(this, { 30 | height: 0 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jazz 2 | 3 | A code editor for the web 4 | 5 | 6 | ## Features 7 | - focus on performance 8 | - operate on large files no sweat 9 | - loads 100k loc file under a second 10 | - native scrolling (mobile also! woohoo) 11 | - fast text buffer (uses a skiplist) 12 | - fast syntax highlight & themes 13 | - a theme editor(!) 14 | - autocomplete (uses a prefixtree) 15 | - full text search 16 | - block highlight 17 | - keyboard bindings 18 | - undo/redo history 19 | - selection & cut/copy/paste 20 | 21 | 22 | It's an early release - there are bugs, edge cases, optimizations to be done, etc 23 | 24 | 25 | ### Contributions are welcome ! 26 | 27 | 28 | ### Development 29 | 30 | ```bash 31 | $ make install && make dev 32 | ``` 33 | 34 | ### Where to start? 35 | check out `./Issues` or `make todo` 36 | 37 | ------------------------- 38 | 39 | mit licensed 2016 by stagas 40 | -------------------------------------------------------------------------------- /src/views/caret.js: -------------------------------------------------------------------------------- 1 | var dom = require('../../lib/dom'); 2 | var css = require('../style.css'); 3 | var View = require('./view'); 4 | 5 | module.exports = CaretView; 6 | 7 | function CaretView(editor) { 8 | View.call(this, editor); 9 | this.name = 'caret'; 10 | this.dom = dom(css.caret); 11 | } 12 | 13 | CaretView.prototype.__proto__ = View.prototype; 14 | 15 | CaretView.prototype.use = function(target) { 16 | dom.append(target, this); 17 | }; 18 | 19 | CaretView.prototype.render = function() { 20 | dom.style(this, { 21 | opacity: +this.editor.hasFocus, 22 | left: this.editor.caretPx.x + this.editor.marginLeft, 23 | top: this.editor.caretPx.y - 1, 24 | height: this.editor.char.height + 1 25 | }); 26 | }; 27 | 28 | CaretView.prototype.clear = function() { 29 | dom.style(this, { 30 | opacity: 0, 31 | left: 0, 32 | top: 0, 33 | height: 0 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /examples/style.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | padding: 0; 6 | margin: 0; 7 | border: none; 8 | background: none; 9 | list-style: none; 10 | font-size: 100%; 11 | font-weight: normal; 12 | font-family: sans-serif; 13 | text-decoration: none; 14 | outline: none; 15 | -webkit-text-size-adjust: 100%; 16 | -moz-text-size-adjust: 100%; 17 | -ms-text-size-adjust: 100%; 18 | -o-text-size-adjust: 100%; 19 | } 20 | 21 | /* style */ 22 | 23 | html, body { 24 | padding: 0; 25 | margin: 0; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | #debug { 31 | //display: none; 32 | position: fixed; 33 | top: 0; 34 | left: 0; 35 | width: 250px; 36 | margin: 0 30px; 37 | padding: 15px; 38 | color: #a1f1a1; 39 | background: rgba(0,0,0, .3); 40 | z-index: 1000; 41 | } 42 | 43 | mark { 44 | color: #fff; 45 | background: #b41; 46 | padding-top: 1px; 47 | padding-bottom: 1px; 48 | } 49 | -------------------------------------------------------------------------------- /lib/memoize.js: -------------------------------------------------------------------------------- 1 | var clone = require('./clone'); 2 | 3 | module.exports = function memoize(fn, diff, merge, pre) { 4 | diff = diff || function(a, b) { return a !== b }; 5 | merge = merge || function(a, b) { return b }; 6 | pre = pre || function(node, param) { return param }; 7 | 8 | var nodes = []; 9 | var cache = []; 10 | var results = []; 11 | 12 | return function(node, param) { 13 | var args = pre(node, param); 14 | node = args[0]; 15 | param = args[1]; 16 | 17 | var index = nodes.indexOf(node); 18 | if (~index) { 19 | var d = diff(cache[index], param); 20 | if (!d) return results[index]; 21 | else { 22 | cache[index] = merge(cache[index], param); 23 | results[index] = fn(node, param, d); 24 | } 25 | } else { 26 | cache.push(clone(param)); 27 | nodes.push(node); 28 | index = results.push(fn(node, param, param)); 29 | } 30 | 31 | return results[index]; 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/views/index.js: -------------------------------------------------------------------------------- 1 | var RulerView = require('./ruler'); 2 | var MarkView = require('./mark'); 3 | var CodeView = require('./code'); 4 | var CaretView = require('./caret'); 5 | var BlockView = require('./block'); 6 | var FindView = require('./find'); 7 | var RowsView = require('./rows'); 8 | 9 | module.exports = Views; 10 | 11 | function Views(editor) { 12 | this.editor = editor; 13 | 14 | this.views = [ 15 | new RulerView(editor), 16 | new MarkView(editor), 17 | new CodeView(editor), 18 | new CaretView(editor), 19 | new BlockView(editor), 20 | new FindView(editor), 21 | new RowsView(editor), 22 | ]; 23 | 24 | this.views.forEach(view => this[view.name] = view); 25 | this.forEach = this.views.forEach.bind(this.views); 26 | } 27 | 28 | Views.prototype.use = function(el) { 29 | this.forEach(view => view.use(el)); 30 | }; 31 | 32 | Views.prototype.render = function() { 33 | this.forEach(view => view.render()); 34 | }; 35 | 36 | Views.prototype.clear = function() { 37 | this.forEach(view => view.clear()); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/dialog/style.css: -------------------------------------------------------------------------------- 1 | .dialog, .dialog * { 2 | transform: translateZ(0); 3 | } 4 | 5 | .dialog { 6 | display: flex; 7 | flex-flow: row nowrap; 8 | align-items: center; 9 | align-content: center; 10 | position: fixed; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | color: #777; 15 | padding: 10px 11px 11px 10px; 16 | font-size: 9.5pt; 17 | border-top: 1px solid #aaa; 18 | border-radius: 0px; 19 | background: #efefef; 20 | z-index: 100; 21 | } 22 | 23 | .dialog > .input { 24 | display: flex; 25 | color: inherit; 26 | flex-grow: 1; 27 | background: #f3f3f3; 28 | border: none; 29 | box-shadow: inset 0px 1px 5px -1px rgba(0,0,0,.4); 30 | border-radius: 3px; 31 | padding: 5px 6px; 32 | margin: 0px; 33 | } 34 | 35 | .dialog > .input > .text { 36 | flex-grow: 1; 37 | border: none; 38 | color: inherit; 39 | } 40 | 41 | .dialog > .label { 42 | margin-right: 10px; 43 | } 44 | 45 | .dialog > .input > .info { 46 | padding: 0 0 0 6px; 47 | margin-left: 6px; 48 | border-left: 1px solid #c9c9c9; 49 | color: #999; 50 | } 51 | -------------------------------------------------------------------------------- /src/buffer/segments.bench.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('./'); 2 | var Point = require('../../lib/point'); 3 | var bench = require('../../test/bench'); 4 | var print = require('../../lib/print'); 5 | var read = require('fs').readFileSync; 6 | 7 | var TIMES = 10e3; 8 | 9 | var code = 10 | // read(__dirname + '/../../test/syntax.html.js', 'utf8'); 11 | read(__dirname + '/../../examples/babel.js', 'utf8'); 12 | 13 | var buffer = new Buffer(); 14 | 15 | buffer.set(code); 16 | 17 | // var res = bench(TIMES, function() { 18 | // syntax.set(code); 19 | // }, 'syntax set'); 20 | 21 | // print(res); 22 | 23 | var res = bench(1, function() { 24 | buffer.segments.index(buffer.raw); 25 | }, 'segments index'); 26 | print(res); 27 | 28 | // console.log(buffer.segments.segments.length) 29 | 30 | var res = bench(TIMES, function() { 31 | var x = 0; 32 | for (var i = 0; i < 80000; i+=Math.random() * 100 | 0) { 33 | x++; 34 | buffer.segments.get(i); 35 | if (x === 1000) break; 36 | } 37 | }, 'segments get', 1000); 38 | print(res); 39 | 40 | // console.log(buffer.segments.cache.point) -------------------------------------------------------------------------------- /test/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | margin: 0; 4 | padding: 0 5px 10px; 5 | font-family: monospace; 6 | color: #656565; 7 | font-size: 8.5pt; 8 | } 9 | 10 | table { 11 | width: 100%; 12 | border-sizing: border-box; 13 | border-collapse: collapse; 14 | } 15 | 16 | table, td { 17 | padding: 1px 15px; 18 | text-align: center; 19 | vertical-align: middle; 20 | } 21 | 22 | td, th { 23 | border-right: 1px solid #ccc; 24 | border-bottom: 1px solid #ccc; 25 | } 26 | 27 | td:last-child, 28 | th:last-child { 29 | border-right: none; 30 | } 31 | 32 | tr:last-child td { 33 | border-bottom: none; 34 | } 35 | 36 | td:nth-child(1) { 37 | width: 45%; 38 | text-align: right; 39 | } 40 | 41 | button { 42 | padding: 2px 7px; 43 | margin: 1px 0 5px; 44 | font-size: 10pt; 45 | letter-spacing: 0.5px; 46 | color: white; 47 | background: #4ab; 48 | border: none; 49 | border-radius: 2px; 50 | } 51 | 52 | .item .result { 53 | position: relative; 54 | } 55 | 56 | .item .header { 57 | margin: 25px 0; 58 | font-size: 14pt; 59 | text-align: center; 60 | } 61 | -------------------------------------------------------------------------------- /lib/set-immediate.js: -------------------------------------------------------------------------------- 1 | // Note: You probably do not want to use this in production code, as Promise is 2 | // not supported by all browsers yet. 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | if (window.setImmediate) { 8 | return; 9 | } 10 | 11 | var pending = {}, 12 | nextHandle = 1; 13 | 14 | function onResolve(handle) { 15 | var callback = pending[handle]; 16 | if (callback) { 17 | delete pending[handle]; 18 | callback.fn.apply(null, callback.args); 19 | } 20 | } 21 | 22 | window.setImmediate = function(fn) { 23 | var args = Array.prototype.slice.call(arguments, 1), 24 | handle; 25 | 26 | if (typeof fn !== "function") { 27 | throw new TypeError("invalid function"); 28 | } 29 | 30 | handle = nextHandle++; 31 | pending[handle] = { fn: fn, args: args }; 32 | 33 | new Promise(function(resolve) { 34 | resolve(handle); 35 | }).then(onResolve); 36 | 37 | return handle; 38 | }; 39 | 40 | window.clearImmediate = function(handle) { 41 | delete pending[handle]; 42 | }; 43 | }()); -------------------------------------------------------------------------------- /lib/trim.js: -------------------------------------------------------------------------------- 1 | 2 | var trim = exports; 3 | 4 | trim.emptyLines = function(s) { 5 | var trailing = trim.trailingEmptyLines(s); 6 | var leading = trim.leadingEmptyLines(trailing.string); 7 | return { 8 | trailing: trailing.removed, 9 | leading: leading.removed, 10 | removed: trailing.removed + leading.removed, 11 | string: leading.string 12 | }; 13 | }; 14 | 15 | trim.trailingEmptyLines = function(s) { 16 | var index = s.length; 17 | var lastIndex = index; 18 | var n = 0; 19 | while ( 20 | ~(index = s.lastIndexOf('\n', lastIndex - 1)) 21 | && index - lastIndex === -1) { 22 | n++; 23 | lastIndex = index; 24 | } 25 | 26 | if (n) s = s.slice(0, lastIndex); 27 | 28 | return { 29 | removed: n, 30 | string: s 31 | }; 32 | }; 33 | 34 | trim.leadingEmptyLines = function(s) { 35 | var index = -1; 36 | var lastIndex = index; 37 | var n = 0; 38 | 39 | while ( 40 | ~(index = s.indexOf('\n', lastIndex + 1)) 41 | && index - lastIndex === 1) { 42 | n++; 43 | lastIndex = index; 44 | } 45 | 46 | if (n) s = s.slice(lastIndex + 1); 47 | 48 | return { 49 | removed: n, 50 | string: s 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /test/broken.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * { 4 | * ([]) 5 | * } 6 | */ 7 | 8 | hello = function() { 9 | while (true) { 10 | if (something) { 11 | return ['ok',[1]]; 12 | } 13 | } 14 | } 15 | 16 | 17 | 18 | 19 | /* 20 | * Need to make sure that buffer isn't trying to write out of bounds. 21 | */ 22 | function checkOffset (offset, ext, length) { 23 | if ((offset % 1) !== 0 || offset < 0) 24 | throw new RangeError('offset is not uint') 25 | if (offset + ext > length) 26 | throw new RangeError('Trying to access beyond buffer length') 27 | } 28 | 29 | /** 30 | * ` 31 | */ 32 | 33 | hello = function() { 34 | 35 | } 36 | 37 | /** 38 | * ` 39 | */ 40 | 41 | hello = function() { 42 | 43 | } 44 | 45 | /** 46 | * ` 47 | */ 48 | 49 | hello = function() { 50 | 51 | } 52 | 53 | /** 54 | * ` 55 | */ 56 | 57 | hello = function() { 58 | thing = '`'; 59 | } 60 | 61 | /** 62 | * ` 63 | */ 64 | 65 | hello = function() { 66 | 67 | } 68 | 69 | /** 70 | * ` 71 | */ 72 | 73 | hello = function() { 74 | 75 | } 76 | 77 | /** 78 | * ` 79 | */ 80 | 81 | hello = function() { 82 | 83 | } 84 | 85 | /** 86 | * ` 87 | */ 88 | 89 | hello = function() { 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/file.js: -------------------------------------------------------------------------------- 1 | var open = require('../lib/open'); 2 | var save = require('../lib/save'); 3 | var Event = require('../lib/event'); 4 | var Buffer = require('./buffer'); 5 | 6 | module.exports = File; 7 | 8 | function File(editor) { 9 | Event.call(this); 10 | 11 | this.root = ''; 12 | this.path = 'untitled'; 13 | this.buffer = new Buffer; 14 | this.bindEvent(); 15 | } 16 | 17 | File.prototype.__proto__ = Event.prototype; 18 | 19 | File.prototype.bindEvent = function() { 20 | this.buffer.on('raw', this.emit.bind(this, 'raw')); 21 | this.buffer.on('set', this.emit.bind(this, 'set')); 22 | this.buffer.on('update', this.emit.bind(this, 'change')); 23 | this.buffer.on('before update', this.emit.bind(this, 'before change')); 24 | }; 25 | 26 | File.prototype.open = function(path, root, fn) { 27 | this.path = path; 28 | this.root = root; 29 | open(root + path, (err, text) => { 30 | if (err) { 31 | this.emit('error', err); 32 | fn && fn(err); 33 | return; 34 | } 35 | this.buffer.setText(text); 36 | this.emit('open'); 37 | fn && fn(null, this); 38 | }); 39 | }; 40 | 41 | File.prototype.save = function(fn) { 42 | save(this.root + this.path, this.buffer.toString(), fn || noop); 43 | }; 44 | 45 | File.prototype.set = function(text) { 46 | this.buffer.setText(text); 47 | this.emit('set'); 48 | }; 49 | 50 | function noop() {/* noop */} 51 | -------------------------------------------------------------------------------- /src/buffer/bench.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('./'); 2 | var Point = require('../../lib/point'); 3 | var bench = require('../../test/bench'); 4 | var print = require('../../lib/print'); 5 | var read = require('fs').readFileSync; 6 | 7 | var TIMES = 1000; 8 | 9 | var code = 10 | // read(__dirname + '/../../test/syntax.html.js', 'utf8'); 11 | read(__dirname + '/../../examples/babel.js', 'utf8'); 12 | 13 | var length = code.length - 200; 14 | 15 | var buffer = new Buffer; 16 | 17 | var i; 18 | 19 | var res = bench(1, function() { 20 | buffer.setText(code); 21 | }, 'setText'); 22 | print(res); 23 | 24 | i = 50000; 25 | var res = bench(TIMES*100, function() { 26 | buffer.get([i, i]); 27 | }, 'get same line'); 28 | print(res); 29 | 30 | i = 60000; 31 | var res = bench(TIMES*10, function() { 32 | buffer.get([i, i+50]); 33 | }, 'get same 50 lines'); 34 | print(res); 35 | 36 | i = 80000 37 | var res = bench(TIMES, function() { 38 | buffer.get([i, i+50]); 39 | i = Math.random() * (buffer.loc() - 51) | 0; 40 | }, 'random gets of 50 lines'); 41 | print(res); 42 | 43 | i = 0; 44 | var res = bench(TIMES*10, function() { 45 | buffer.get([i, i+50]); 46 | i += 50; 47 | }, 'sequential gets of 50 lines'); 48 | print(res); 49 | 50 | var res = bench(TIMES*10, function() { 51 | i = Math.random() * buffer.loc() | 0; 52 | buffer.insert({ x:0, y:i }, '123\n{456}\n'); 53 | }, 'random inserts'); 54 | print(res); 55 | -------------------------------------------------------------------------------- /src/buffer/tokens.bench.js: -------------------------------------------------------------------------------- 1 | var ChunkArray = require('./chunk-array'); 2 | var Tokens = require('./tokens'); 3 | var Point = require('../../lib/point'); 4 | var bench = require('../../test/bench'); 5 | var print = require('../../lib/print'); 6 | var read = require('fs').readFileSync; 7 | 8 | var TIMES = 10; 9 | 10 | var code = 11 | // read(__dirname + '/../../test/syntax.html.js', 'utf8'); 12 | read(__dirname + '/../../examples/babel.js', 'utf8'); 13 | 14 | var length = code.length - 200; 15 | 16 | var tokens = new Tokens(() => new ChunkArray(5000)); 17 | 18 | var res = bench(1, function() { 19 | tokens.index(code); 20 | }, 'tokens index'); 21 | print(res); 22 | 23 | // var res = bench(TIMES * 1000, function() { 24 | // tokens.get('segments', Math.random() * length | 0); 25 | // }, 'tokens get'); 26 | // print(res); 27 | 28 | // var res = bench(TIMES * 1000, function() { 29 | // tokens.shift(Math.random() * length | 0, 5); 30 | // }, 'tokens shift'); 31 | // print(res); 32 | 33 | var text = '/* { [`foo`] } */\n'; 34 | // var text = 'a'; 35 | var i; 36 | var res = bench(TIMES * 1000, function() { 37 | i = Math.random() * length | 0; 38 | tokens.updateRange([i, i + text.length], text); 39 | }, 'tokens updateRange'); 40 | print(res); 41 | 42 | // tokens.shift(0, 2) 43 | 44 | // var token = tokens.get('segments', 23204); 45 | // console.log(token) 46 | // console.log(Tokens.Type[code[token.offset-2]]); 47 | -------------------------------------------------------------------------------- /src/views/rows.js: -------------------------------------------------------------------------------- 1 | var dom = require('../../lib/dom'); 2 | var css = require('../style.css'); 3 | var View = require('./view'); 4 | 5 | module.exports = RowsView; 6 | 7 | function RowsView(editor) { 8 | View.call(this, editor); 9 | this.name = 'rows'; 10 | this.dom = dom(css.rows); 11 | this.rows = -1; 12 | this.range = [-1,-1]; 13 | this.html = ''; 14 | } 15 | 16 | RowsView.prototype.__proto__ = View.prototype; 17 | 18 | RowsView.prototype.use = function(target) { 19 | dom.append(target, this); 20 | }; 21 | 22 | RowsView.prototype.render = function() { 23 | var range = this.editor.getPageRange([-1,+1]); 24 | 25 | if ( range[0] >= this.range[0] 26 | && range[1] <= this.range[1] 27 | && ( this.range[1] !== this.rows 28 | || this.editor.rows === this.rows 29 | )) return; 30 | 31 | range = this.editor.getPageRange([-3,+3]); 32 | this.rows = this.editor.rows; 33 | this.range = range; 34 | 35 | var html = ''; 36 | for (var i = range[0]; i <= range[1]; i++) { 37 | html += (i + 1) + '\n'; 38 | } 39 | 40 | if (html !== this.html) { 41 | this.html = html; 42 | 43 | dom.html(this, html); 44 | 45 | dom.style(this, { 46 | top: range[0] * this.editor.char.height, 47 | height: (range[1] - range[0] + 1) * this.editor.char.height 48 | }); 49 | } 50 | }; 51 | 52 | RowsView.prototype.clear = function() { 53 | dom.style(this, { 54 | height: 0 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /theme/color-picker.css: -------------------------------------------------------------------------------- 1 | .Scp { 2 | width: 175px; 3 | height: 150px; 4 | -webkit-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | position: relative; 9 | } 10 | .Scp-saturation { 11 | position: relative; 12 | width: calc(100% - 25px); 13 | height: 100%; 14 | background: -webkit-linear-gradient(left, #fff, #f00); 15 | background: linear-gradient(to right, #fff, #f00); 16 | float: left; 17 | margin-right: 5px; 18 | } 19 | .Scp-brightness { 20 | width: 100%; 21 | height: 100%; 22 | background: -webkit-linear-gradient(rgba(255,255,255,0), #000); 23 | background: linear-gradient(rgba(255,255,255,0), #000); 24 | } 25 | .Scp-sbSelector { 26 | border: 2px solid #fff; 27 | position: absolute; 28 | width: 14px; 29 | height: 14px; 30 | background: #fff; 31 | border-radius: 10px; 32 | top: -7px; 33 | left: -7px; 34 | box-sizing: border-box; 35 | z-index: 10; 36 | } 37 | .Scp-hue { 38 | width: 20px; 39 | height: 100%; 40 | position: relative; 41 | float: left; 42 | background: -webkit-linear-gradient(#f00 0%, #f0f 17%, #00f 34%, #0ff 50%, #0f0 67%, #ff0 84%, #f00 100%); 43 | background: linear-gradient(#f00 0%, #f0f 17%, #00f 34%, #0ff 50%, #0f0 67%, #ff0 84%, #f00 100%); 44 | } 45 | .Scp-hSelector { 46 | position: absolute; 47 | background: #fff; 48 | border-bottom: 1px solid #000; 49 | right: -3px; 50 | width: 10px; 51 | height: 2px; 52 | } -------------------------------------------------------------------------------- /lib/event.js: -------------------------------------------------------------------------------- 1 | 2 | var push = [].push; 3 | var slice = [].slice; 4 | 5 | module.exports = Event; 6 | 7 | function Event() { 8 | if (!(this instanceof Event)) return new Event; 9 | 10 | this._handlers = {}; 11 | } 12 | 13 | Event.prototype._getHandlers = function(name) { 14 | this._handlers = this._handlers || {}; 15 | return this._handlers[name] = this._handlers[name] || []; 16 | }; 17 | 18 | Event.prototype.emit = function(name, a, b, c, d) { 19 | var handlers = this._getHandlers(name); 20 | for (var i = 0; i < handlers.length; i++) { 21 | handlers[i](a, b, c, d); 22 | }; 23 | }; 24 | 25 | Event.prototype.on = function(name) { 26 | var handlers; 27 | var newHandlers = slice.call(arguments, 1); 28 | if (Array.isArray(name)) { 29 | name.forEach(function(name) { 30 | handlers = this._getHandlers(name); 31 | push.apply(handlers, newHandlers[name]); 32 | }, this); 33 | } else { 34 | handlers = this._getHandlers(name); 35 | push.apply(handlers, newHandlers); 36 | } 37 | }; 38 | 39 | Event.prototype.off = function(name, handler) { 40 | var handlers = this._getHandlers(name); 41 | var index = handlers.indexOf(handler); 42 | if (~index) handlers.splice(index, 1); 43 | }; 44 | 45 | Event.prototype.once = function(name, fn) { 46 | var handlers = this._getHandlers(name); 47 | var handler = function(a, b, c, d) { 48 | fn(a, b, c, d); 49 | handlers.splice(handlers.indexOf(handler), 1); 50 | }; 51 | handlers.push(handler); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/range.js: -------------------------------------------------------------------------------- 1 | var AND = require('./range-gate-and'); 2 | var NOT = require('./range-gate-not'); 3 | 4 | module.exports = Range; 5 | 6 | function Range(r) { 7 | if (r) { 8 | this[0] = r[0]; 9 | this[1] = r[1]; 10 | } else { 11 | this[0] = 0; 12 | this[1] = 1; 13 | } 14 | }; 15 | 16 | Range.AND = AND; 17 | Range.NOT = NOT; 18 | 19 | Range.sort = function(a, b) { 20 | return a.y === b.y 21 | ? a.x - b.x 22 | : a.y - b.y; 23 | }; 24 | 25 | Range.equal = function(a, b) { 26 | return a[0] === b[0] && a[1] === b[1]; 27 | }; 28 | 29 | Range.clamp = function(a, b) { 30 | return new Range([ 31 | Math.min(b[1], Math.max(a[0], b[0])), 32 | Math.min(a[1], b[1]) 33 | ]); 34 | }; 35 | 36 | Range.prototype.slice = function() { 37 | return new Range(this); 38 | }; 39 | 40 | Range.ranges = function(items) { 41 | return items.map(function(item) { return item.range }); 42 | }; 43 | 44 | Range.prototype.inside = function(items) { 45 | var range = this; 46 | return items.filter(function(item) { 47 | return item.range[0] >= range[0] && item.range[1] <= range[1]; 48 | }); 49 | }; 50 | 51 | Range.prototype.overlap = function(items) { 52 | var range = this; 53 | return items.filter(function(item) { 54 | return item.range[0] <= range[0] && item.range[1] >= range[1]; 55 | }); 56 | }; 57 | 58 | Range.prototype.outside = function(items) { 59 | var range = this; 60 | return items.filter(function(item) { 61 | return item.range[1] < range[0] || item.range[0] > range[1]; 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /src/input/index.js: -------------------------------------------------------------------------------- 1 | var Event = require('../../lib/event'); 2 | var Mouse = require('./mouse'); 3 | var Text = require('./text'); 4 | 5 | module.exports = Input; 6 | 7 | function Input(editor) { 8 | this.editor = editor; 9 | this.mouse = new Mouse(this); 10 | this.text = new Text; 11 | this.bindEvent(); 12 | } 13 | 14 | Input.prototype.__proto__ = Event.prototype; 15 | 16 | Input.prototype.bindEvent = function() { 17 | this.blur = this.blur.bind(this); 18 | this.focus = this.focus.bind(this); 19 | this.text.on(['key', 'text'], this.emit.bind(this, 'input')); 20 | this.text.on('focus', this.emit.bind(this, 'focus')); 21 | this.text.on('blur', this.emit.bind(this, 'blur')); 22 | this.text.on('text', this.emit.bind(this, 'text')); 23 | this.text.on('keys', this.emit.bind(this, 'keys')); 24 | this.text.on('key', this.emit.bind(this, 'key')); 25 | this.text.on('cut', this.emit.bind(this, 'cut')); 26 | this.text.on('copy', this.emit.bind(this, 'copy')); 27 | this.text.on('paste', this.emit.bind(this, 'paste')); 28 | this.mouse.on('up', this.emit.bind(this, 'mouseup')); 29 | this.mouse.on('click', this.emit.bind(this, 'mouseclick')); 30 | this.mouse.on('down', this.emit.bind(this, 'mousedown')); 31 | this.mouse.on('drag', this.emit.bind(this, 'mousedrag')); 32 | this.mouse.on('drag begin', this.emit.bind(this, 'mousedragbegin')); 33 | }; 34 | 35 | Input.prototype.use = function(node) { 36 | this.mouse.use(node); 37 | this.text.reset(); 38 | }; 39 | 40 | Input.prototype.blur = function() { 41 | this.text.blur(); 42 | }; 43 | 44 | Input.prototype.focus = function() { 45 | this.text.focus(); 46 | }; 47 | -------------------------------------------------------------------------------- /src/views/mark.js: -------------------------------------------------------------------------------- 1 | var dom = require('../../lib/dom'); 2 | var css = require('../style.css'); 3 | var View = require('./view'); 4 | 5 | module.exports = MarkView; 6 | 7 | function MarkView(editor) { 8 | View.call(this, editor); 9 | this.name = 'mark'; 10 | this.dom = dom(css.mark); 11 | } 12 | 13 | MarkView.prototype.__proto__ = View.prototype; 14 | 15 | MarkView.prototype.use = function(target) { 16 | dom.append(target, this); 17 | }; 18 | 19 | MarkView.prototype.get = function(range, e) { 20 | var mark = e.mark.get(); 21 | if (range[0] > mark.end.y) return false; 22 | if (range[1] < mark.begin.y) return false; 23 | 24 | var offsets = e.buffer.getLineRangeOffsets(range); 25 | var area = e.buffer.getAreaOffsetRange(mark); 26 | var code = e.buffer.text.getRange(offsets); 27 | 28 | area[0] -= offsets[0]; 29 | area[1] -= offsets[0]; 30 | 31 | var above = code.substring(0, area[0]); 32 | var middle = code.substring(area[0], area[1]); 33 | var html = e.syntax.entities(above) 34 | + '' + e.syntax.entities(middle) + ''; 35 | 36 | html = html.replace(/\n/g, ' \n'); 37 | 38 | return html; 39 | }; 40 | 41 | MarkView.prototype.render = function() { 42 | if (!this.editor.mark.active) return this.clear(); 43 | 44 | var page = this.editor.getPageRange([-.5,+.5]); 45 | var html = this.get(page, this.editor); 46 | 47 | dom.html(this, html); 48 | 49 | dom.style(this, { 50 | top: page[0] * this.editor.char.height, 51 | height: 'auto' 52 | }); 53 | }; 54 | 55 | MarkView.prototype.clear = function() { 56 | dom.style(this, { 57 | top: 0, 58 | height: 0 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/trim.test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | 4 | module.exports = function(t, trim) { 5 | 6 | var text = '\n\n\n\nfoo bar\n\n\n\n'; 7 | var edge1 = '\nfoo bar\n'; 8 | var edge2 = '\n foo bar \n'; 9 | var edge3 = '\n \nfoo bar\n \n'; 10 | 11 | t('empty lines', function() { 12 | assert.equal('foo bar', trim.emptyLines(text).string); 13 | assert.equal(8, trim.emptyLines(text).removed); 14 | assert.equal(4, trim.emptyLines(text).trailing); 15 | assert.equal(4, trim.emptyLines(text).leading); 16 | }) 17 | 18 | t('trailing empty lines', function() { 19 | assert.equal('\n\n\n\nfoo bar', trim.trailingEmptyLines(text).string); 20 | assert.equal(4, trim.trailingEmptyLines(text).removed); 21 | assert.equal('\nfoo bar', trim.trailingEmptyLines(edge1).string); 22 | assert.equal(1, trim.trailingEmptyLines(edge1).removed); 23 | assert.equal('\n foo bar ', trim.trailingEmptyLines(edge2).string); 24 | assert.equal(1, trim.trailingEmptyLines(edge2).removed); 25 | assert.equal('\n \nfoo bar\n ', trim.trailingEmptyLines(edge3).string); 26 | assert.equal(1, trim.trailingEmptyLines(edge3).removed); 27 | }) 28 | 29 | t('leading empty lines', function() { 30 | assert.equal('foo bar\n\n\n\n', trim.leadingEmptyLines(text).string); 31 | assert.equal(4, trim.leadingEmptyLines(text).removed); 32 | assert.equal('foo bar\n', trim.leadingEmptyLines(edge1).string); 33 | assert.equal(1, trim.leadingEmptyLines(edge1).removed); 34 | assert.equal(' foo bar \n', trim.leadingEmptyLines(edge2).string); 35 | assert.equal(1, trim.leadingEmptyLines(edge2).removed); 36 | assert.equal(' \nfoo bar\n \n', trim.leadingEmptyLines(edge3).string); 37 | assert.equal(1, trim.leadingEmptyLines(edge3).removed); 38 | }) 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /src/views/find.js: -------------------------------------------------------------------------------- 1 | var dom = require('../../lib/dom'); 2 | var css = require('../style.css'); 3 | var View = require('./view'); 4 | 5 | module.exports = FindView; 6 | 7 | function FindView(editor) { 8 | View.call(this, editor); 9 | this.name = 'find'; 10 | this.dom = dom(css.find); 11 | } 12 | 13 | FindView.prototype.__proto__ = View.prototype; 14 | 15 | FindView.prototype.use = function(target) { 16 | dom.append(target, this); 17 | }; 18 | 19 | FindView.prototype.get = function(range, e) { 20 | var results = e.findResults; 21 | 22 | var begin = 0; 23 | var end = results.length; 24 | var prev = -1; 25 | var i = -1; 26 | 27 | do { 28 | prev = i; 29 | i = begin + (end - begin) / 2 | 0; 30 | if (results[i].y < range[0] - 1) begin = i; 31 | else end = i; 32 | } while (prev !== i); 33 | 34 | var width = e.findValue.length * e.char.width + 'px'; 35 | 36 | var html = ''; 37 | var tabs; 38 | var r; 39 | while (results[i] && results[i].y < range[1]) { 40 | r = results[i++]; 41 | tabs = e.getPointTabs(r); 42 | html += ''; 48 | } 49 | 50 | return html; 51 | }; 52 | 53 | FindView.prototype.render = function() { 54 | if (!this.editor.find.isOpen || !this.editor.findResults.length) return; 55 | 56 | var page = this.editor.getPageRange([-.5,+.5]); 57 | var html = this.get(page, this.editor); 58 | 59 | dom.html(this, html); 60 | }; 61 | 62 | FindView.prototype.clear = function() { 63 | dom.html(this, ''); 64 | }; 65 | -------------------------------------------------------------------------------- /src/buffer/prefixtree.js: -------------------------------------------------------------------------------- 1 | // var WORD = /\w+/g; 2 | var WORD = /[a-zA-Z0-9]{1,}/g 3 | var rank = 0; 4 | 5 | module.exports = PrefixTreeNode; 6 | 7 | function PrefixTreeNode() { 8 | this.value = ''; 9 | this.rank = 0; 10 | this.children = {}; 11 | } 12 | 13 | PrefixTreeNode.prototype.getChildren = function() { 14 | var children = Object 15 | .keys(this.children) 16 | .map((key) => this.children[key]); 17 | 18 | return children.reduce((p, n) => p.concat(n.getChildren()), children); 19 | }; 20 | 21 | PrefixTreeNode.prototype.collect = function(key) { 22 | var collection = []; 23 | var node = this.find(key); 24 | if (node) { 25 | collection = node 26 | .getChildren() 27 | .filter((node) => node.value) 28 | .sort((a, b) => { 29 | var res = b.rank - a.rank; 30 | if (res === 0) res = b.value.length - a.value.length; 31 | if (res === 0) res = a.value > b.value; 32 | return res; 33 | }); 34 | 35 | if (node.value) collection.push(node); 36 | } 37 | return collection; 38 | }; 39 | 40 | PrefixTreeNode.prototype.find = function(key) { 41 | var node = this; 42 | for (var char in key) { 43 | if (key[char] in node.children) { 44 | node = node.children[key[char]]; 45 | } else { 46 | return; 47 | } 48 | } 49 | return node; 50 | }; 51 | 52 | PrefixTreeNode.prototype.insert = function(s, value) { 53 | var node = this; 54 | var i = 0; 55 | var n = s.length; 56 | 57 | while (i < n) { 58 | if (s[i] in node.children) { 59 | node = node.children[s[i]]; 60 | i++; 61 | } else { 62 | break; 63 | } 64 | } 65 | 66 | while (i < n) { 67 | node = 68 | node.children[s[i]] = 69 | node.children[s[i]] || new PrefixTreeNode; 70 | i++; 71 | } 72 | 73 | node.value = s; 74 | node.rank++; 75 | }; 76 | 77 | PrefixTreeNode.prototype.index = function(s) { 78 | var word; 79 | while (word = WORD.exec(s)) { 80 | this.insert(word[0]); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /lib/box.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Box; 3 | 4 | function Box(b) { 5 | if (b) { 6 | this.width = b.width; 7 | this.height = b.height; 8 | } else { 9 | this.width = 0; 10 | this.height = 0; 11 | } 12 | } 13 | 14 | Box.prototype.set = function(b) { 15 | this.width = b.width; 16 | this.height = b.height; 17 | }; 18 | 19 | Box.prototype['/'] = 20 | Box.prototype.div = function(p) { 21 | return new Box({ 22 | width: this.width / (p.x || p.width || 0), 23 | height: this.height / (p.y || p.height || 0) 24 | }); 25 | }; 26 | 27 | Box.prototype['_/'] = 28 | Box.prototype.floorDiv = function(p) { 29 | return new Box({ 30 | width: this.width / (p.x || p.width || 0) | 0, 31 | height: this.height / (p.y || p.height || 0) | 0 32 | }); 33 | }; 34 | 35 | Box.prototype['^/'] = 36 | Box.prototype.ceildiv = function(p) { 37 | return new Box({ 38 | width: Math.ceil(this.width / (p.x || p.width || 0)), 39 | height: Math.ceil(this.height / (p.y || p.height || 0)) 40 | }); 41 | }; 42 | 43 | Box.prototype['*'] = 44 | Box.prototype.mul = function(b) { 45 | return new Box({ 46 | width: this.width * (b.width || b.x || 0), 47 | height: this.height * (b.height || b.y || 0) 48 | }); 49 | }; 50 | 51 | Box.prototype['^*'] = 52 | Box.prototype.mul = function(b) { 53 | return new Box({ 54 | width: Math.ceil(this.width * (b.width || b.x || 0)), 55 | height: Math.ceil(this.height * (b.height || b.y || 0)) 56 | }); 57 | }; 58 | 59 | Box.prototype['o*'] = 60 | Box.prototype.mul = function(b) { 61 | return new Box({ 62 | width: Math.round(this.width * (b.width || b.x || 0)), 63 | height: Math.round(this.height * (b.height || b.y || 0)) 64 | }); 65 | }; 66 | 67 | Box.prototype['_*'] = 68 | Box.prototype.mul = function(b) { 69 | return new Box({ 70 | width: this.width * (b.width || b.x || 0) | 0, 71 | height: this.height * (b.height || b.y || 0) | 0 72 | }); 73 | }; 74 | 75 | Box.prototype['-'] = 76 | Box.prototype.sub = function(b) { 77 | return new Box({ 78 | width: this.width - (b.width || b.x || 0), 79 | height: this.height - (b.height || b.y || 0) 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = assert; 3 | 4 | assert.disabled = false; 5 | 6 | function assert(expr, msg) { 7 | if (assert.disabled) return; 8 | 9 | if (!expr) { 10 | throw new Error(msg || 'assertion failed'); 11 | } 12 | } 13 | 14 | assert.equal = function(l, r, msg) { 15 | if (assert.disabled) return; 16 | 17 | if (!equal(l, r)) fail(l, r, msg); 18 | }; 19 | 20 | assert.throws = function(contains, fn, msg) { 21 | if (assert.disabled) return; 22 | 23 | if (arguments.length <= 2) { 24 | msg = fn; 25 | fn = contains; 26 | contains = null; 27 | } 28 | 29 | var passed = false; 30 | var error; 31 | 32 | try { 33 | fn(); 34 | passed = false; 35 | } catch(e) { 36 | error = e; 37 | passed = true; 38 | } 39 | 40 | if (passed) return; 41 | else fail(null, fn.toString(), msg || 'to throw'); 42 | 43 | if (contains) { 44 | assert( 45 | e.message.indexOf(contains) > -1, 46 | msg || '"' + e.message + '" does not contain "' + contains + '"' 47 | ); 48 | } 49 | }; 50 | 51 | function equal(l, r, visited) { 52 | visited = visited || []; 53 | 54 | var lt = typeof l; 55 | if (lt !== 'object' || null === l) { 56 | return l === r; 57 | } 58 | 59 | if (~visited.indexOf(l)) return true; 60 | if (~visited.indexOf(r)) return true; 61 | 62 | visited.push(l, r); 63 | 64 | var lk = Object.keys(l); 65 | var rk = Object.keys(r); 66 | 67 | for (var i = lk.length; i--;) { 68 | var key = lk[i]; 69 | if (!equal(l[key], r[key])) return false; 70 | } 71 | 72 | for (var i = rk.length; i--;) { 73 | var key = rk[i]; 74 | if (!equal(l[key], r[key])) return false; 75 | } 76 | 77 | return true; 78 | } 79 | 80 | function fail(l, r, msg) { 81 | var ls = '', rs = ''; 82 | 83 | try { 84 | ls = l != null ? JSON.stringify(l, null, Array.isArray(l) ? null : ' ') : msg || ''; 85 | rs = JSON.stringify(r, null, Array.isArray(r) ? null : ' '); 86 | } catch(_) { 87 | ls = rs = ''; 88 | } 89 | 90 | var error = new Error(msg == null ? 'not equal' : msg); 91 | error.meta = { left: ls, right: rs }; 92 | 93 | throw error; 94 | } 95 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .editor { 2 | overflow: scroll; 3 | } 4 | 5 | .editor, 6 | .layer { 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | -webkit-touch-callout: none; 11 | -webkit-user-select: none; 12 | -khtml-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | user-select: none; 16 | } 17 | 18 | .rows, 19 | .mark, 20 | .code, 21 | mark, 22 | p, 23 | t, 24 | k, 25 | d, 26 | n, 27 | o, 28 | e, 29 | m, 30 | f, 31 | r, 32 | c, 33 | s, 34 | l, 35 | x { 36 | white-space: pre; 37 | } 38 | 39 | .mark, 40 | .code { 41 | position: absolute; 42 | cursor: text; 43 | padding-right: 8px; 44 | overflow: hidden; 45 | } 46 | 47 | .caret { 48 | position: absolute; 49 | cursor: text; 50 | top: 0; 51 | left: 0; 52 | width: 1px; 53 | background: #fff; 54 | z-index: 90; 55 | } 56 | 57 | @keyframes caret-blink-smooth { 58 | 0% { opacity: 1 } 59 | 15% { opacity: 0 } 60 | 30% { opacity: 0 } 61 | 70% { opacity: 1 } 62 | 100% { opacity: 1 } 63 | } 64 | 65 | .caret.blink-smooth { 66 | animation-name: caret-blink-smooth; 67 | animation-duration: 776.666ms; 68 | animation-timing-function: ease-in-out; 69 | animation-iteration-count: infinite; 70 | } 71 | 72 | .gutter { 73 | box-sizing: content-box; 74 | position: absolute; 75 | padding: 0 15px; 76 | left: 0; 77 | top: 0; 78 | text-align: right; 79 | cursor: default; 80 | color: #9e9e96; 81 | } 82 | 83 | .ruler { 84 | position: absolute; 85 | cursor: text; 86 | left: 0; 87 | top: 0; 88 | right: 0; 89 | opacity: 0; 90 | } 91 | 92 | .gutter.above { 93 | box-shadow: 0px 0px 8px 6px #222; 94 | } 95 | 96 | .rows { 97 | position: absolute; 98 | text-align: right; 99 | cursor: default; 100 | color: #8e8e86; 101 | z-index: 5; 102 | } 103 | 104 | .mark { 105 | color: transparent; 106 | background: transparent; 107 | z-index: 1; 108 | } 109 | 110 | .find > i { 111 | position: absolute; 112 | border: 1px solid #ff1; 113 | border-radius: 4px; 114 | z-index: 50; 115 | } 116 | 117 | .block > i { 118 | position: absolute; 119 | border-bottom: 1px solid #aaa; 120 | z-index: 60; 121 | } 122 | -------------------------------------------------------------------------------- /theme/theme.js: -------------------------------------------------------------------------------- 1 | 2 | var picker = []; 3 | 4 | for (var i = 0; i < 9; i++) { 5 | picker[i] = new simpleColorPicker({ 6 | color: '#566', 7 | background: '#000', 8 | el: pickers, 9 | width: 140, 10 | height: 100 11 | }); 12 | picker[i].on('update', updateStyle); 13 | } 14 | 15 | 16 | function updateStyle() { 17 | var s = ''; 18 | var t = ''; 19 | s += 'body { background: ' + picker[0].getHexString() + '; color: '+ picker[1].getHexString() +'}'; 20 | t += '\nbackground: ' + picker[0].getHexString(); 21 | t += '\ncolor: ' + picker[1].getHexString(); 22 | s += 'operator,operator2,keyword { color: ' + picker[2].getHexString() + '}'; 23 | t += '\nkeyword: ' + picker[2].getHexString(); 24 | s += 'function { color: ' + picker[3].getHexString() + '}'; 25 | t += '\nfunction: ' + picker[3].getHexString(); 26 | s += 'declare,builtin { color: ' + picker[4].getHexString() + '}'; 27 | t += '\ndeclare: ' + picker[4].getHexString(); 28 | s += 'boolean,number { color: ' + picker[5].getHexString() + '}'; 29 | t += '\nnumber: ' + picker[5].getHexString(); 30 | s += 'params,regexp { color: ' + picker[6].getHexString() + '}'; 31 | t += '\nparams: ' + picker[6].getHexString(); 32 | s += 'comment { color: ' + picker[7].getHexString() + '}'; 33 | t += '\ncomment: ' + picker[7].getHexString(); 34 | s += 'string { color: ' + picker[8].getHexString() + '}'; 35 | t += '\nstring: ' + picker[8].getHexString(); 36 | 37 | style.textContent = s; 38 | colors.textContent = t; 39 | } 40 | 41 | randomize.onclick = randomizeColors; 42 | 43 | function randomizeColors() { 44 | picker.forEach(function(p, i) { 45 | if (0 === i) { 46 | p.setColor('#' + 47 | (Math.random() * 0xf | 0).toString(16) + (Math.random() * 0xf | 0).toString(16) + 48 | (Math.random() * 0xf | 0).toString(16) + (Math.random() * 0xf | 0).toString(16) + 49 | (Math.random() * 0xf | 0).toString(16) + (Math.random() * 0xf | 0).toString(16) 50 | ) 51 | } else { 52 | p.setColor('#' + 53 | zeroPad((Math.random() * 0xff | 0).toString(16)) + 54 | zeroPad((Math.random() * 0xff | 0).toString(16)) + 55 | zeroPad((Math.random() * 0xff | 0).toString(16)) 56 | ); 57 | } 58 | }); 59 | updateStyle(); 60 | } 61 | 62 | 63 | var style = document.createElement('style'); 64 | document.body.appendChild(style); 65 | 66 | function zeroPad(s) { 67 | if (s.length === 1) return '0' + s; 68 | else return s; 69 | } -------------------------------------------------------------------------------- /src/buffer/prefixtree.test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | 4 | module.exports = function(t, PTNode) { 5 | var words = [ 6 | 'foo', 7 | 'fo', 8 | 'ba', 9 | 'bar' 10 | ]; 11 | 12 | t('insert', function() { 13 | var node = new PTNode; 14 | words.forEach((word) => node.insert(word)); 15 | assert.equal('', node.value); 16 | assert.equal('', node.children.f.value); 17 | assert.equal('fo', node.children.f.children.o.value); 18 | assert.equal('foo', node.children.f.children.o.children.o.value); 19 | assert.equal('ba', node.children.b.children.a.value); 20 | assert.equal('bar', node.children.b.children.a.children.r.value); 21 | }) 22 | 23 | t('find', function() { 24 | var node = new PTNode; 25 | words.forEach((word) => node.insert(word)); 26 | assert.equal(undefined, node.find('x')); 27 | assert.equal('', node.find('f').value); 28 | assert.equal('fo', node.find('fo').value); 29 | assert.equal('foo', node.find('foo').value); 30 | assert.equal('', node.find('b').value); 31 | assert.equal('ba', node.find('ba').value); 32 | assert.equal('bar', node.find('bar').value); 33 | }) 34 | 35 | // t('getChildren', function() { 36 | // var node = new PTNode; 37 | // words.forEach((word) => node.insert(word)); 38 | // assert.equal( 39 | // ['ba', 'fo', 'bar', 'foo'], 40 | // node.getChildren().map((node) => node.value) 41 | // ); 42 | // node.children.f.children.o.incrementRank(); 43 | // assert.equal( 44 | // ['fo', 'ba', 'bar', 'foo'], 45 | // node.getChildren().map((node) => node.value) 46 | // ); 47 | // }) 48 | 49 | t('collect', function() { 50 | var node = new PTNode; 51 | words.forEach((word) => node.insert(word)); 52 | assert.equal( 53 | ['bar', 'ba'], 54 | node.collect('b').map((node) => node.value) 55 | ); 56 | assert.equal( 57 | ['foo', 'fo'], 58 | node.collect('f').map((node) => node.value) 59 | ); 60 | assert.equal( 61 | ['foo'], 62 | node.collect('foo').map((node) => node.value) 63 | ); 64 | assert.equal( 65 | [], 66 | node.collect('x') 67 | ); 68 | }) 69 | 70 | t('index', function() { 71 | var node = new PTNode; 72 | var s = 'foo bar fo ba'; 73 | node.index(s); 74 | assert.equal( 75 | ['', '', 'fo', 'foo', 'ba', 'bar'], 76 | node.getChildren().map((node) => node.value) 77 | ); 78 | }) 79 | }; 80 | 81 | function repeat(times, fn) { 82 | while (times--) fn(); 83 | } 84 | -------------------------------------------------------------------------------- /lib/dialog/index.js: -------------------------------------------------------------------------------- 1 | var dom = require('../dom'); 2 | var Event = require('../event'); 3 | var css = require('./style.css'); 4 | 5 | module.exports = Dialog; 6 | 7 | function Dialog(label, keymap) { 8 | this.node = dom(css.dialog, [ 9 | `