├── .gitignore ├── .travis.yml ├── AUTHORS ├── History.md ├── LICENSE ├── README.md ├── bower.json ├── examples ├── index.html └── server.js ├── package.json ├── share-codemirror.js └── test └── headless_tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *~ 3 | npm-debug.log 4 | coverage/ 5 | .DS_Store 6 | .idea/ 7 | *.iml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Aslak Hellesøy (http://aslakhellesoy.com) 2 | Thaddée Tyl (http://espadrine.github.io/) 3 | luto (http://luto.at/) 4 | J.D. Zamfirescu -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## [0.1.0](https://github.com/share/share-codemirror/compare/v0.0.5...v0.1.0) (26 Nov 2014) 2 | 3 | * Augment CodeMirror instance with `detachShareJsDoc()`. In a single app context, CodeMirror instances might be attached several times to the same shared doc. This can lead to multiple share channels attached to multiple ghost instances of CodeMirror for the same file, causing an infinite loop while the editors keep appending content to the file. This new method allows for safely disconnecting a shareJs doc from a CodeMirror instance. 4 | 5 | ## [0.0.5](https://github.com/share/share-codemirror/compare/v0.0.4...v0.0.5) (12 Nov 2013) 6 | 7 | * Handle empty documents. This is a workaround for a sharejs bug where `ctx.get()` returns `undefined`. 8 | 9 | ## [0.0.4](https://github.com/share/share-codemirror/compare/v0.0.3...v0.0.4) (3 Nov 2013) 10 | 11 | * Fix npm install problem. [#2](https://github.com/share/share-codemirror/issues/2). 12 | 13 | ## [0.0.3](https://github.com/share/share-codemirror/compare/v0.0.2...v0.0.3) (28 Sept 2013) 14 | 15 | * Use `ctx.get()` instead of deprecated `ctx.getText()`. 16 | * Added code coverage. 17 | * Wrote more tests. 18 | 19 | ## 0.0.2 (2013-09-04) 20 | 21 | First release! Based on the ShareJS 0.6 CodeMirror bindings by Thaddée Tyl, J.D. Zamfirescu 22 | and luto. Aslak Hellesøy ported the code from CoffeeScript to JavaScript, adapted to the 23 | ShareJS 0.7 API and added tests. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the standard MIT license: 2 | 3 | Copyright 2013 Joseph Gentle and ShareJS contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Share-CodeMirror [![Build Status](https://secure.travis-ci.org/share/share-codemirror.png)](http://travis-ci.org/share/share-codemirror) [![Dependencies](https://david-dm.org/share/share-codemirror.png)](https://david-dm.org/share/share-codemirror) [![devDependency Status](https://david-dm.org/share/share-codemirror/dev-status.png)](https://david-dm.org/share/share-codemirror#info=devDependencies) 2 | CodeMirror bindings for ShareJS >= 0.7.x. 3 | 4 | ## Usage 5 | 6 | ```javascript 7 | var cm = CodeMirror.fromTextArea(elem); 8 | shareDoc.attachCodeMirror(cm); 9 | ``` 10 | 11 | That's it. You now have 2-way sync between your ShareJS and CodeMirror. 12 | 13 | ## Install with Bower 14 | 15 | ``` 16 | bower install share-codemirror 17 | ``` 18 | 19 | ## Install with NPM 20 | 21 | ``` 22 | npm install share-codemirror 23 | ``` 24 | 25 | On Node.js you can mount the `scriptsDir` (where `share-codemirror.js` lives) as a static resource 26 | in your web server: 27 | 28 | ```javascript 29 | var shareCodeMirror = require('share-codemirror'); 30 | // This example uses express. 31 | app.use(express.static(shareCodeMirror.scriptsDir)); 32 | ``` 33 | 34 | In the HTML: 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | ## Try it out 41 | 42 | ``` 43 | npm install 44 | node examples/server.js 45 | # in a couple of browsers... 46 | open http://localhost:7007 47 | ``` 48 | 49 | Try clicking the infinite monkeys button. Do it in both browsers. 50 | Wait for poetry to appear. 51 | 52 | ## Run tests 53 | 54 | ``` 55 | npm install 56 | npm test 57 | ``` 58 | 59 | With test coverage: 60 | 61 | ``` 62 | node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u exports 63 | open coverage/lcov-report/index.html 64 | ``` 65 | 66 | ## Release process 67 | 68 | ``` 69 | npm outdated --depth 0 # See if you can upgrade something 70 | ``` 71 | 72 | * Modify version in `bower.json` (not in `package.json`) 73 | * Update `History.md` 74 | * Commit 75 | 76 | Then run: 77 | 78 | ``` 79 | npm version `jq -r < bower.json .version` 80 | npm publish 81 | git push --tags 82 | ``` 83 | 84 | There is no `bower publish` - the existance of a git tag is enough. 85 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "share-codemirror", 3 | "version": "0.1.0", 4 | "main": "share-codemirror.js", 5 | "ignore": [ 6 | "node_modules" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var Duplex = require('stream').Duplex; 3 | var browserChannel = require('browserchannel').server; 4 | var express = require('express'); 5 | var livedb = require('livedb'); 6 | var sharejs = require('share'); 7 | var shareCodeMirror = require('..'); 8 | 9 | var backend = livedb.client(livedb.memory()); 10 | var share = sharejs.server.createClient({backend: backend}); 11 | 12 | var app = express(); 13 | app.use(express.static(__dirname)); 14 | app.use(express.static(shareCodeMirror.scriptsDir)); 15 | app.use(express.static(__dirname + '/../node_modules/codemirror/lib')); 16 | app.use(express.static(sharejs.scriptsDir)); 17 | app.use(browserChannel(function (client) { 18 | var stream = new Duplex({objectMode: true}); 19 | stream._write = function (chunk, encoding, callback) { 20 | if (client.state !== 'closed') { 21 | client.send(chunk); 22 | } 23 | callback(); 24 | }; 25 | stream._read = function () { 26 | }; 27 | stream.headers = client.headers; 28 | stream.remoteAddress = stream.address; 29 | client.on('message', function (data) { 30 | stream.push(data); 31 | }); 32 | stream.on('error', function (msg) { 33 | client.stop(); 34 | }); 35 | client.on('close', function (reason) { 36 | stream.emit('close'); 37 | stream.emit('end'); 38 | stream.end(); 39 | }); 40 | return share.listen(stream); 41 | })); 42 | 43 | var server = http.createServer(app); 44 | server.listen(7007, function (err) { 45 | if (err) throw err; 46 | 47 | console.log('Listening on http://%s:%s', server.address().address, server.address().port); 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "share-codemirror", 3 | "version": "0.1.0", 4 | "description": "CodeMirror bindings for ShareJS", 5 | "main": "share-codemirror.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/share/share-codemirror.git" 12 | }, 13 | "keywords": [ 14 | "codemirror", 15 | "sharejs" 16 | ], 17 | "author": "Aslak Hellesøy", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/share/share-codemirror/issues" 21 | }, 22 | "devDependencies": { 23 | "browserchannel": "^2.0.0", 24 | "codemirror": "^4.8.0", 25 | "express": "^4.10.3", 26 | "istanbul": "^0.3.2", 27 | "jsdom": "0.8.8", 28 | "livedb": "^0.4.9", 29 | "mocha": "^2.0.1", 30 | "share": "^0.7.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /share-codemirror.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | /** 5 | * @param cm - CodeMirror instance 6 | * @param ctx - Share context 7 | */ 8 | function shareCodeMirror(cm, ctx) { 9 | if (!ctx.provides.text) throw new Error('Cannot attach to non-text document'); 10 | 11 | var suppress = false; 12 | var text = ctx.get() || ''; // Due to a bug in share - get() returns undefined for empty docs. 13 | cm.setValue(text); 14 | check(); 15 | 16 | // *** remote -> local changes 17 | 18 | ctx.onInsert = function (pos, text) { 19 | suppress = true; 20 | cm.replaceRange(text, cm.posFromIndex(pos)); 21 | suppress = false; 22 | check(); 23 | }; 24 | 25 | ctx.onRemove = function (pos, length) { 26 | suppress = true; 27 | var from = cm.posFromIndex(pos); 28 | var to = cm.posFromIndex(pos + length); 29 | cm.replaceRange('', from, to); 30 | suppress = false; 31 | check(); 32 | }; 33 | 34 | // *** local -> remote changes 35 | 36 | cm.on('change', onLocalChange); 37 | 38 | function onLocalChange(cm, change) { 39 | if (suppress) return; 40 | applyToShareJS(cm, change); 41 | check(); 42 | } 43 | 44 | cm.detachShareJsDoc = function () { 45 | ctx.onRemove = null; 46 | ctx.onInsert = null; 47 | cm.off('change', onLocalChange); 48 | } 49 | 50 | // Convert a CodeMirror change into an op understood by share.js 51 | function applyToShareJS(cm, change) { 52 | // CodeMirror changes give a text replacement. 53 | // I tuned this operation a little bit, for speed. 54 | var startPos = 0; // Get character position from # of chars in each line. 55 | var i = 0; // i goes through all lines. 56 | 57 | while (i < change.from.line) { 58 | startPos += cm.lineInfo(i).text.length + 1; // Add 1 for '\n' 59 | i++; 60 | } 61 | 62 | startPos += change.from.ch; 63 | 64 | if (change.to.line == change.from.line && change.to.ch == change.from.ch) { 65 | // nothing was removed. 66 | } else { 67 | // delete.removed contains an array of removed lines as strings, so this adds 68 | // all the lengths. Later change.removed.length - 1 is added for the \n-chars 69 | // (-1 because the linebreak on the last line won't get deleted) 70 | var delLen = 0; 71 | for (var rm = 0; rm < change.removed.length; rm++) { 72 | delLen += change.removed[rm].length; 73 | } 74 | delLen += change.removed.length - 1; 75 | ctx.remove(startPos, delLen); 76 | } 77 | if (change.text) { 78 | ctx.insert(startPos, change.text.join('\n')); 79 | } 80 | if (change.next) { 81 | applyToShareJS(cm, change.next); 82 | } 83 | } 84 | 85 | function check() { 86 | setTimeout(function () { 87 | var cmText = cm.getValue(); 88 | var otText = ctx.get() || ''; 89 | 90 | if (cmText != otText) { 91 | console.error("Text does not match!"); 92 | console.error("cm: " + cmText); 93 | console.error("ot: " + otText); 94 | // Replace the editor text with the ctx snapshot. 95 | cm.setValue(ctx.get() || ''); 96 | } 97 | }, 0); 98 | } 99 | 100 | return ctx; 101 | } 102 | 103 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 104 | // Node.js 105 | module.exports = shareCodeMirror; 106 | module.exports.scriptsDir = __dirname; 107 | } else { 108 | if (typeof define === 'function' && define.amd) { 109 | // AMD 110 | define([], function () { 111 | return shareCodeMirror; 112 | }); 113 | } else { 114 | // Browser, no AMD 115 | window.sharejs.Doc.prototype.attachCodeMirror = function (cm, ctx) { 116 | if (!ctx) ctx = this.createContext(); 117 | shareCodeMirror(cm, ctx); 118 | }; 119 | } 120 | } 121 | })(); 122 | -------------------------------------------------------------------------------- /test/headless_tests.js: -------------------------------------------------------------------------------- 1 | // Create a browser environment for CodeMirror 2 | var jsdom = require("jsdom"); 3 | document = jsdom.jsdom(''); 4 | window = document.parentWindow; 5 | navigator = {}; 6 | // Add some missing stuff in jsdom that CodeMirror wants 7 | jsdom.dom.level3.html.HTMLElement.prototype.createTextRange = function () { 8 | return { 9 | moveToElementText: function () { 10 | }, 11 | collapse: function () { 12 | }, 13 | moveEnd: function () { 14 | }, 15 | moveStart: function () { 16 | }, 17 | getBoundingClientRect: function () { 18 | return {}; 19 | } 20 | }; 21 | }; 22 | 23 | var CodeMirror = require('codemirror'); 24 | var share = require('share'); 25 | var shareCodeMirror = require('..'); 26 | var assert = require('assert'); 27 | 28 | function newCm(ctx) { 29 | var cm = CodeMirror.fromTextArea(document.getElementById('editor')); 30 | shareCodeMirror(cm, ctx); 31 | return cm; 32 | } 33 | 34 | describe('CodeMirror creation', function () { 35 | it('sets context text in editor', function () { 36 | var ctx = new Ctx('hi'); 37 | var cm = newCm(ctx); 38 | 39 | assert.equal('hi', cm.getValue()); 40 | }); 41 | }); 42 | 43 | describe('CodeMirror edits', function () { 44 | it('adds text', function () { 45 | var ctx = new Ctx(''); 46 | var cm = newCm(ctx); 47 | 48 | var text = "aaaa\nbbbb\ncccc\ndddd"; 49 | cm.setValue(text); 50 | assert.equal(text, ctx.get()); 51 | }); 52 | 53 | it('adds empty text', function () { 54 | var ctx = new Ctx(''); 55 | var cm = newCm(ctx); 56 | 57 | cm.setValue(''); 58 | assert.equal('', ctx.get() || ''); 59 | 60 | cm.setValue('a'); 61 | assert.equal('a', ctx.get() || ''); 62 | }); 63 | 64 | it('replaces a couple of lines', function () { 65 | var ctx = new Ctx('three\nblind\nmice\nsee\nhow\nthey\nrun\n'); 66 | var cm = newCm(ctx); 67 | 68 | cm.replaceRange('evil\nrats\n', {line: 1, ch: 0}, {line: 3, ch: 0}); 69 | assert.equal('three\nevil\nrats\nsee\nhow\nthey\nrun\n', ctx.get()); 70 | }); 71 | }); 72 | 73 | describe('ShareJS changes', function () { 74 | it('adds text', function () { 75 | var ctx = new Ctx('', true); 76 | var cm = newCm(ctx); 77 | 78 | var text = "aaaa\nbbbb\ncccc\ndddd"; 79 | ctx.insert(0, text); 80 | assert.equal(text, cm.getValue()); 81 | }); 82 | 83 | it('can edit a doc that has been empty', function () { 84 | var ctx = new Ctx('', true); 85 | var cm = newCm(ctx); 86 | 87 | ctx.insert(0, ''); 88 | assert.equal('', cm.getValue()); 89 | 90 | ctx.insert(0, 'a'); 91 | assert.equal('a', cm.getValue()); 92 | }); 93 | 94 | it('replaces a line', function () { 95 | var ctx = new Ctx('hi', true); 96 | var cm = newCm(ctx); 97 | 98 | ctx.remove(0, 2); 99 | ctx.insert(0, 'hello'); 100 | assert.equal('hello', cm.getValue()); 101 | }); 102 | 103 | it('replaces a couple of lines', function () { 104 | var ctx = new Ctx('three\nblind\nmice\nsee\nhow\nthey\nrun\n', true); 105 | var cm = newCm(ctx); 106 | 107 | ctx.remove(6, 11); 108 | ctx.insert(6, 'evil\nrats\n'); 109 | assert.equal('three\nevil\nrats\nsee\nhow\nthey\nrun\n', cm.getValue()); 110 | }); 111 | }); 112 | 113 | describe('Stub context', function () { 114 | it('can insert at the beginning', function () { 115 | var ctx = new Ctx('abcdefg'); 116 | ctx.insert(0, '123'); 117 | assert.equal('123abcdefg', ctx.get()); 118 | }); 119 | 120 | it('can insert in the middle', function () { 121 | var ctx = new Ctx('abcdefg'); 122 | ctx.insert(2, '123'); 123 | assert.equal('ab123cdefg', ctx.get()); 124 | }); 125 | 126 | it('can insert at the end', function () { 127 | var ctx = new Ctx('abcdefg'); 128 | ctx.insert(ctx.get().length, '123'); 129 | assert.equal('abcdefg123', ctx.get()); 130 | }); 131 | 132 | it('can remove from the beginning', function () { 133 | var ctx = new Ctx('abcdefg'); 134 | ctx.remove(0, 2); 135 | assert.equal('cdefg', ctx.get()); 136 | }); 137 | 138 | it('can remove from the middle', function () { 139 | var ctx = new Ctx('abcdefg'); 140 | ctx.remove(2, 3); 141 | assert.equal('abfg', ctx.get()); 142 | }); 143 | 144 | it('can remove from the end', function () { 145 | var ctx = new Ctx('abcdefg'); 146 | ctx.remove(5, 2); 147 | assert.equal('abcde', ctx.get()); 148 | }); 149 | }) 150 | 151 | function Ctx(text, fireEvents) { 152 | this.provides = { text: true }; 153 | 154 | this.get = function () { 155 | // Replicate a sharejs bug where empty docs return undefined. 156 | return text == '' ? undefined : text; 157 | }; 158 | 159 | this.insert = function (startPos, newText) { 160 | var before = text.substring(0, startPos); 161 | var after = text.substring(startPos); 162 | text = before + newText + after; 163 | fireEvents && this.onInsert && this.onInsert(startPos, newText); 164 | }; 165 | 166 | this.remove = function (startPos, length) { 167 | var before = text.substring(0, startPos); 168 | var after = text.substring(startPos + length); 169 | text = before + after; 170 | fireEvents && this.onRemove && this.onRemove(startPos, length); 171 | }; 172 | } 173 | --------------------------------------------------------------------------------