Comparing version 0.3.1 to 0.3.2
155
canop.js
@@ -14,5 +14,6 @@ (function (root, factory) { | ||
var actions = { | ||
set: 0, | ||
stringAdd: 7, | ||
stringRemove: 8 | ||
pass: 0, | ||
set: 1, | ||
stringAdd: 8, | ||
stringRemove: 9 | ||
}; | ||
@@ -59,4 +60,12 @@ var PROTOCOL_VERSION = 0; | ||
}, | ||
// Is this operation's type rebaseable (ie, its indices get reshifted)? | ||
// If it is, then key = index in a list. | ||
rebaseable: function rebaseable() { | ||
return this.action === actions.stringAdd || | ||
this.action === actions.stringRemove; | ||
}, | ||
// Get this operation modified by a list of PosChanges. | ||
getModifiedBy: function getModifiedBy(changes) { | ||
if (!this.rebaseable()) { return; } | ||
var oldEnd = this.key + this.value.length; | ||
@@ -79,4 +88,3 @@ this.original = { | ||
if (key === undefined || end === undefined) { | ||
this.key = 0; // Nulled to avoid adding spaces at the end in toString(). | ||
this.value = ""; // Null operation. | ||
this.action = actions.pass; // Nulled to avoid adding spaces at the end in toString(). | ||
} else { | ||
@@ -97,8 +105,11 @@ this.key = key; | ||
inverse: function inverse() { | ||
var op = this.dup(); | ||
if (this.action === actions.stringAdd) { | ||
var op = this.dup(); | ||
op.action = actions.stringRemove; | ||
} else if (this.action === actions.stringRemove) { | ||
var op = this.dup(); | ||
op.action = actions.stringAdd; | ||
} else if (this.action === actions.set) { | ||
var newValue = op.key; | ||
op.key = op.value; // Old value | ||
op.value = newValue;// New value | ||
} | ||
@@ -170,2 +181,6 @@ return op; | ||
var change = changes[i]; | ||
if (change === undefined) { | ||
if (bestGuess) { return key; } | ||
else { return; } | ||
} | ||
var newKey = change.update(key, originalKey); | ||
@@ -271,7 +286,4 @@ var contextFound = false; | ||
var change = this.list[i].change(); | ||
if (change !== undefined) { | ||
posChanges.push(change); | ||
} else { | ||
posChanges = []; | ||
} | ||
if (change === undefined) { return posChanges; } | ||
posChanges.push(change); | ||
} | ||
@@ -284,11 +296,16 @@ return posChanges; | ||
for (var i = this.list.length - 1; i >= 0; i--) { | ||
var change = this.list[i].change().inverse(); | ||
if (change !== undefined) { | ||
posChanges.push(change); | ||
} else { | ||
return posChanges; | ||
} | ||
var change = this.list[i].change(); | ||
if (change === undefined) { return posChanges; } | ||
change = change.inverse(); | ||
if (change === undefined) { return posChanges; } | ||
posChanges.push(change); | ||
} | ||
return posChanges; | ||
}, | ||
// actionType: actions.* | ||
applyAtomicOperation: function applyAtomicOperation(actionType, key, value, base, local) { | ||
var aop = new AtomicOperation(actionType, key, value, base, local); | ||
this.list.push(aop); | ||
return aop; | ||
}, | ||
// Insert a value to the operation. Mutates this. | ||
@@ -306,2 +323,8 @@ add: function addOp(path, offset, value, base, local) { | ||
}, | ||
// Change the whole value. Mutates this. | ||
set: function setOp(path, newVal, oldVal, base, local) { | ||
var aop = new AtomicOperation(actions.set, newVal, oldVal, base, local); | ||
this.list.push(aop); | ||
return aop; | ||
}, | ||
// Assume we start with the empty value. | ||
@@ -392,2 +415,3 @@ toString: function toString() { | ||
var self = this; | ||
params = params || {}; | ||
this.base = params.base || 0; // Most recent known canon operation index. | ||
@@ -400,2 +424,4 @@ this.id = 0; // Identifier of the current machine. | ||
this.listeners = {}; | ||
this.undoStack = []; | ||
this.redoStack = []; | ||
@@ -405,5 +431,3 @@ this.on('change', function(event) { self.updateData(event); }); | ||
// Note: servers should never disable data. | ||
if (params.disableData !== undefined) { | ||
this.disableData = params.disableData; | ||
} | ||
this.disableData = !!params.disableData; | ||
this.data = undefined; // Holds the current data including local operations. | ||
@@ -557,4 +581,6 @@ // Also, clients should never have params.data. | ||
} | ||
if (delta[1][0] === 0) { // set | ||
} else if ((delta[1][0] === 7) || (delta[1][0] === 8)) { | ||
if (delta[1][0] === actions.pass) { // pass | ||
} else if (delta[1][0] === actions.set) { // set | ||
} else if ((delta[1][0] === actions.stringAdd) || | ||
(delta[1][0] === actions.stringRemove)) { | ||
// string add / remove | ||
@@ -667,7 +693,8 @@ if (typeof delta[1][1] !== "number") { // offset | ||
emitChanges: function(eventName, changes, posChanges) { | ||
var change = changes.map(function(change) { | ||
return [[], change.action, change.key, change.value]; | ||
}); | ||
var change = changes.map(this.aopArrayFromAop); | ||
this.emit(eventName, {changes: change, posChanges: posChanges}); | ||
}, | ||
aopArrayFromAop: function(change) { | ||
return [[], change.action, change.key, change.value]; | ||
}, | ||
// Receive an external update conforming to the protocol, as a String. | ||
@@ -842,9 +869,6 @@ // Emit the corresponding change, update and synced events. | ||
act: function(action) { | ||
var aops = this.commitAction(action); | ||
// TODO: localUpdate event. | ||
this.emitChanges('localChange', aops); | ||
this.sendToServer(); | ||
this.registerAtomicOperations(this.commitAction(action)); | ||
}, | ||
// actions: list of [actionType, path, …params]. | ||
actAtomically: function(actions) { | ||
actAtomically: function(actions, options) { | ||
var aops = []; | ||
@@ -854,4 +878,3 @@ for (var i = 0; i < actions.length; i++) { | ||
} | ||
this.emitChanges('localChange', aops); | ||
this.sendToServer(); | ||
this.registerAtomicOperations(aops, options); | ||
}, | ||
@@ -863,3 +886,6 @@ // action: [actions.*, path, …params] | ||
var aops = []; // AtomicOperations | ||
if (actionType === actions.stringAdd) { | ||
if (actionType === actions.pass) { | ||
} else if (actionType === actions.set) { | ||
aops.push(this.set(action)); | ||
} else if (actionType === actions.stringAdd) { | ||
aops.push(this.stringAdd(action)); | ||
@@ -873,2 +899,7 @@ } else if (actionType === actions.stringRemove) { | ||
}, | ||
// actionType: actions.* | ||
applyAtomicOperation: function(actionType, key, value) { | ||
return this.local.applyAtomicOperation( | ||
actionType, key, value, this.base, this.id); | ||
}, | ||
// action: [actions.stringAdd, path, …params] | ||
@@ -886,3 +917,52 @@ // Return an AtomicOperation. | ||
}, | ||
// action: [actions.set, path, new value, old value] | ||
// Return an AtomicOperation. | ||
set: function(action) { | ||
return this.local.set(action[1], action[2], action[3], | ||
this.base, this.localId); | ||
}, | ||
// History management | ||
registerAtomicOperations: function(aops, options) { | ||
options = options || {}; | ||
if (!options.skipHistory) { | ||
// Add to the local history. | ||
this.undoStack.push(new Operation(aops)); | ||
this.redoStack = []; | ||
} | ||
// TODO: localUpdate event. | ||
this.emitChanges('localChange', aops); | ||
this.sendToServer(); | ||
}, | ||
undo: function() { | ||
var lastOp = this.undoStack.pop(); | ||
var aops = []; | ||
if (lastOp !== undefined) { | ||
this.redoStack.push(lastOp.dup()); | ||
var op = lastOp.inverse(); | ||
for (var i = 0; i < op.list.length; i++) { | ||
var aop = op.list[i]; | ||
aops.push(this.applyAtomicOperation(aop.action, aop.key, aop.value)); | ||
} | ||
this.registerAtomicOperations(aops, {skipHistory: true}); | ||
} | ||
return aops.map(this.aopArrayFromAop); | ||
}, | ||
redo: function() { | ||
var op = this.redoStack.pop(); | ||
var aops = []; | ||
if (op !== undefined) { | ||
this.undoStack.push(op.dup()); | ||
for (var i = 0; i < op.list.length; i++) { | ||
var aop = op.list[i]; | ||
aops.push(this.applyAtomicOperation(aop.action, aop.key, aop.value)); | ||
} | ||
this.registerAtomicOperations(aops, {skipHistory: true}); | ||
} | ||
return aops.map(this.aopArrayFromAop); | ||
}, | ||
// Send a signal to all other nodes of the network. | ||
@@ -935,2 +1015,3 @@ // content: JSON-serializable value, sent to other nodes. | ||
self.removeClient(newClient); | ||
var oldClientId = newClient.id; | ||
newClient.id = protocol[1]; | ||
@@ -950,2 +1031,6 @@ newClient.base = base; | ||
} | ||
// The client may have had its id changed. | ||
delete self.signalFromClient[oldClientId]; | ||
self.signalFromClient[newClient.id] = self.signalFromClient[newClient.id] || | ||
Object.create(null); | ||
self.sendSignalsToClient(newClient); | ||
@@ -965,2 +1050,4 @@ } else if (messageType === PROTOCOL_DELTA) { | ||
for (var key in data) { | ||
self.signalFromClient[clientId] = self.signalFromClient[clientId] || | ||
{}; | ||
self.signalFromClient[clientId][key] = data[key]; | ||
@@ -967,0 +1054,0 @@ } |
@@ -33,11 +33,9 @@ # Canop Protocol | ||
- `action`: set = 0 | ||
- `action`: pass = 0, set = 1 | ||
- `key`: any JSON value, possibly of a different type. | ||
- `value`: old value, if any. | ||
(Note: `[0]` "set nothing to nothing" is the empty operation.) | ||
Delta for objects: | ||
- `action`: add = 1, remove = 2, move = 3. | ||
- `action`: add = 2, remove = 3, move = 4. | ||
- `key`: key (in the object) as a string. | ||
@@ -48,3 +46,3 @@ - `value`: value (for move, path of the new location). | ||
- `action`: add = 4, remove = 5, move = 6. | ||
- `action`: add = 5, remove = 6, move = 7. | ||
- `key`: index (in the list). | ||
@@ -55,3 +53,3 @@ - `value`: value (for move, path of the new location). | ||
- `action`: add = 7, remove = 8. | ||
- `action`: add = 8, remove = 9. | ||
- `key`: offset (in the string). | ||
@@ -58,0 +56,0 @@ - `value`: string. |
{ | ||
"name": "canop", | ||
"version": "0.3.1", | ||
"version": "0.3.2", | ||
"description": "Convergent algorithm for collaborative text.", | ||
@@ -9,2 +9,5 @@ "main": "canop.js", | ||
}, | ||
"devDependencies": { | ||
"camp": "~17.2.1" | ||
}, | ||
"repository": { | ||
@@ -11,0 +14,0 @@ "type": "git", |
@@ -9,2 +9,8 @@ `canop` | ||
A simple [client](./web/index.html)-[server](./app.js) example is available, | ||
using adapters for [WebSocket](./web/canop-websocket.js) and | ||
[CodeMirror](./web/canop-codemirror.js). | ||
Exploratory API description: | ||
```js | ||
@@ -47,2 +53,5 @@ var canop = require('canop'); | ||
client.removeListener('change', changeListener); | ||
// Returns an array of [path, action type, parameters…] | ||
client.undo() | ||
client.redo() | ||
@@ -142,6 +151,7 @@ // When all local operations are acknowledged by the server. | ||
- Textarea adapter | ||
- Customizable UI sync debouncing | ||
- JSON-compatible protocol | ||
- Array index rebasing | ||
- Autosave of operations to disk | ||
- Separate display hooks from transport hooks | ||
- Allow creating user-defined operations |
@@ -143,1 +143,55 @@ var canop = require('../canop.js'); | ||
assert.equal(star.clients[1].clientCount, 1, 'Client 1 clientCount after disconnection'); | ||
// Undo | ||
var star = new Star(''); | ||
star.clients[0].add([], 0, 'ab'); | ||
sendChange(star, 0); | ||
star.clients[0].add([], 2, 'cd'); | ||
sendChange(star, 0); | ||
sendChange(star, 1); | ||
// Undo wrong client | ||
star.clients[1].undo(); | ||
sendChange(star, 0); | ||
sendChange(star, 1); | ||
assert.equal(String(star.clients[0]), 'abcd', 'Client 0 wrong undo'); | ||
assert.equal(String(star.clients[1]), 'abcd', 'Client 1 wrong undo'); | ||
assert.equal(String(star.server), 'abcd', 'Client 1 wrong undo'); | ||
// Undo right client | ||
star.clients[0].undo(); | ||
assert.equal(String(star.clients[0]), 'ab', 'Client 0 undo'); | ||
sendChange(star, 0); | ||
sendChange(star, 1); | ||
assert.equal(String(star.server), 'ab', 'Server undo'); | ||
assert.equal(String(star.clients[1]), 'ab', 'Client 1 undo'); | ||
// Redo | ||
star.clients[0].redo(); | ||
assert.equal(String(star.clients[0]), 'abcd', 'Client 0 redo'); | ||
sendChange(star, 0); | ||
sendChange(star, 1); | ||
assert.equal(String(star.server), 'abcd', 'Server redo'); | ||
assert.equal(String(star.clients[1]), 'abcd', 'Client 1 redo'); | ||
// Partial undo, edition, and redo | ||
var star = new Star(''); | ||
star.clients[0].add([], 0, 'a'); | ||
star.clients[0].add([], 1, 'b'); | ||
star.clients[0].undo(); | ||
star.clients[0].add([], 1, 'c'); | ||
star.clients[0].redo(); | ||
sendChange(star, 0); | ||
sendChange(star, 1); | ||
assert.equal(String(star.server), 'ac', 'Server undo, edition, redo'); | ||
assert.equal(String(star.clients[0]), 'ac', 'Client 0 undo, edition, redo'); | ||
assert.equal(String(star.clients[1]), 'ac', 'Client 1 undo, edition, redo'); | ||
// undo, redo, undo | ||
var star = new Star(''); | ||
star.clients[0].add([], 0, 'a'); | ||
star.clients[0].undo(); | ||
star.clients[0].redo(); | ||
star.clients[0].undo(); | ||
sendChange(star, 0); | ||
sendChange(star, 1); | ||
assert.equal(String(star.server), '', 'Server undo, redo, undo'); | ||
assert.equal(String(star.clients[0]), '', 'Client 0 undo, redo, undo'); | ||
assert.equal(String(star.clients[1]), '', 'Client 1 undo, redo, undo'); |
(function(exports, undefined) { | ||
var canop = exports.canop; | ||
var RECONNECTION_INTERVAL = 256; // in ms. Increases exponentially to 10min. | ||
// client: a canop.Client instance. | ||
// editor: a CodeMirror instance. | ||
// options: | ||
// - url: location of websocket. Defaults to ws://<dns>/websocket. | ||
// - error: function triggered when the websocket errors. | ||
// - open: function triggered when the websocket opens. | ||
// - close: function triggered when the websocket closes. | ||
// (You can also rely on .canopClient.on('unsyncable', …) | ||
// - reconnect: if true, automatically reconnect when disconnected. | ||
function CanopCodemirrorHook(editor, options) { | ||
options = options || {}; | ||
options.url = options.url || | ||
// Trick: use the end of either http: or https:. | ||
'ws' + window.location.protocol.slice(4) + '//' + | ||
window.location.host + '/websocket'; | ||
// options: (planned) | ||
// - path: list determining the location of the corresponding string in the JSON | ||
// object. | ||
function CanopCodemirror(client, editor) { | ||
this.canopClient = client; | ||
this.editor = editor; | ||
var self = this; | ||
this.canopClient = new canop.Client({ | ||
send: function(msg) { | ||
if (self.socket === undefined || | ||
self.socket.readyState !== WebSocket.OPEN) { | ||
throw new Error("WebSocket is not open for business"); | ||
} | ||
self.socket.send(msg); | ||
} | ||
}); | ||
this.url = "" + options.url; | ||
this.socket = null; | ||
this.reconnect = (options.reconnect === undefined)? true: !!options.reconnect; | ||
this.reconnectionInterval = RECONNECTION_INTERVAL; | ||
this.socketReceive = this.socketReceive.bind(this); | ||
this.editorChange = this.editorChange.bind(this); | ||
@@ -42,2 +18,5 @@ this.remoteChange = this.remoteChange.bind(this); | ||
this.signalReceive = this.signalReceive.bind(this); | ||
this.editorUndo = this.editorUndo.bind(this); | ||
this.editorRedo = this.editorRedo.bind(this); | ||
this.commit = this.commit.bind(this); | ||
@@ -47,43 +26,43 @@ this.canopClient.on('change', this.remoteChange); | ||
this.clientSelectionWidgets = Object.create(null); | ||
this.connect(options); | ||
this.actionBuffer = []; | ||
this.actionBufferTimeout = null; | ||
this.commitCallbacks = []; | ||
this.editor.undo = this.editorUndo; | ||
this.editor.redo = this.editorRedo; | ||
} | ||
CanopCodemirrorHook.prototype = { | ||
connect: function CCHconnect(options) { | ||
CanopCodemirror.prototype = { | ||
remoteChange: function canopCodemirrorRemoteUpdate(event) { | ||
var self = this; | ||
this.socket = new WebSocket(this.url); | ||
this.socket.addEventListener('message', this.socketReceive); | ||
this.socket.addEventListener('close', function(e) { | ||
self.canopClient.emit('unsyncable'); | ||
if (self.reconnect) { | ||
setTimeout(function() { self.connect(options); }, | ||
self.reconnectionInterval); | ||
if (self.reconnectionInterval <= 1000 * 60 * 10) { | ||
self.reconnectionInterval *= 2; | ||
} | ||
} | ||
self.awaitCommit(function() { | ||
self.updateEditor(event.changes, event.posChanges); | ||
}); | ||
this.socket.addEventListener('open', function() { | ||
self.reconnectionInterval = RECONNECTION_INTERVAL; | ||
self.canopClient.emit('syncing'); | ||
}); | ||
if (options.error) { this.socket.addEventListener('error', options.error); } | ||
if (options.open) { this.socket.addEventListener('open', options.open); } | ||
if (options.close) { this.socket.addEventListener('close', options.close); } | ||
}, | ||
socketReceive: function CCHsocketReceive(event) { | ||
this.canopClient.receive('' + event.data); | ||
awaitCommit: function(cb) { | ||
if (this.actionBufferTimeout === null) { | ||
cb(); | ||
} else { | ||
this.commitCallbacks.push(cb); | ||
} | ||
}, | ||
remoteChange: function CCHremoteUpdate(event) { | ||
this.updateEditor(event.changes, event.posChanges); | ||
commit: function() { | ||
this.canopClient.actAtomically(this.actionBuffer); | ||
this.actionBuffer = []; | ||
this.actionBufferTimeout = null; | ||
for (var i = 0; i < this.commitCallbacks.length; i++) { | ||
this.commitCallbacks[i](); | ||
} | ||
this.commitCallbacks = []; | ||
}, | ||
editorChange: function CCHeditorChange(editor, change, actions) { | ||
var actions = actions || []; | ||
// Listen to UI changes and register them in canop. | ||
editorChange: function canopCodemirrorEditorChange(editor, change) { | ||
var actions = []; | ||
var from = change.from; | ||
var to = change.to; | ||
var added = change.text.join('\n'); | ||
var removed = change.removed.join('\n'); | ||
var removed = editor.getRange(from, to, '\n'); | ||
var fromIdx = editor.indexFromPos(from); | ||
@@ -96,10 +75,10 @@ if (removed.length > 0) { | ||
} | ||
if (change.next) { | ||
this.editorChange(editor, change.next, actions); | ||
} else { | ||
this.canopClient.actAtomically(actions); | ||
this.actionBuffer = this.actionBuffer.concat(actions); | ||
if (this.actionBufferTimeout === null) { | ||
this.actionBufferTimeout = setTimeout(this.commit, 100); | ||
} | ||
}, | ||
cursorActivity: function CCHcursorActivity(editor) { | ||
cursorActivity: function canopCodemirrorCursorActivity(editor) { | ||
var self = this; | ||
@@ -115,41 +94,86 @@ var selections = self.editor.listSelections().map(function(selection) { | ||
resetEditor: function CCHresetEditor() { | ||
this.editor.off('change', this.editorChange); | ||
// action: function | ||
withoutEditorListeners: function(action) { | ||
this.editor.off('beforeChange', this.editorChange); | ||
this.editor.off('cursorActivity', this.cursorActivity); | ||
var cursor = this.editor.getCursor(); | ||
this.editor.setValue('' + this.canopClient); | ||
this.editor.setCursor(cursor); | ||
action(); | ||
this.editor.on('cursorActivity', this.cursorActivity); | ||
this.editor.on('change', this.editorChange); | ||
this.editor.on('beforeChange', this.editorChange); | ||
}, | ||
resetEditor: function canopCodemirrorResetEditor() { | ||
var self = this; | ||
self.withoutEditorListeners(function() { | ||
var cursor = self.editor.getCursor(); | ||
self.editor.setValue('' + self.canopClient); | ||
self.editor.setCursor(cursor); | ||
}); | ||
}, | ||
// Takes a list of AtomicOperations and a list of PosChanges. | ||
updateEditor: function CCHupdateEditor(delta, posChanges) { | ||
this.editor.off('change', this.editorChange); | ||
this.editor.off('cursorActivity', this.cursorActivity); | ||
var cursor = this.editor.indexFromPos(this.editor.getCursor()); | ||
this.applyDelta(delta); | ||
this.updateCursor(posChanges, cursor); | ||
this.editor.on('cursorActivity', this.cursorActivity); | ||
this.editor.on('change', this.editorChange); | ||
updateEditor: function canopCodemirrorUpdateEditor(delta, posChanges) { | ||
var self = this; | ||
self.withoutEditorListeners(function() { | ||
var selections = self.getSelections(); | ||
self.applyDelta(delta); | ||
self.setSelections(posChanges, selections); | ||
}); | ||
}, | ||
applyDelta: function CCHapplyDelta(delta) { | ||
for (var i = 0; i < delta.length; i++) { | ||
var change = delta[i]; | ||
if (change[1] === canop.action.set) { | ||
this.editor.setValue(change[2]); | ||
} else if (change[1] === canop.action.stringAdd) { | ||
this.editor.replaceRange(change[3], this.editor.posFromIndex(change[2])); | ||
} else if (change[1] === canop.action.stringRemove) { | ||
var from = this.editor.posFromIndex(change[2]); | ||
var to = this.editor.posFromIndex(change[2] + change[3].length); | ||
this.editor.replaceRange('', from, to); | ||
applyDelta: function canopCodemirrorApplyDelta(delta) { | ||
var self = this; | ||
self.editor.changeGeneration(true); | ||
self.editor.operation(function() { | ||
for (var i = 0; i < delta.length; i++) { | ||
var change = delta[i]; | ||
if (change[1] === canop.action.set) { | ||
self.editor.setValue(change[2]); | ||
} else if (change[1] === canop.action.stringAdd) { | ||
var addPos = self.editor.posFromIndex(change[2]); | ||
self.editor.replaceRange(change[3], addPos, addPos, '*'); | ||
} else if (change[1] === canop.action.stringRemove) { | ||
var from = self.editor.posFromIndex(change[2]); | ||
var to = self.editor.posFromIndex(change[2] + change[3].length); | ||
self.editor.replaceRange('', from, to, '*'); | ||
} | ||
} | ||
}); | ||
}, | ||
editorUndo: function() { | ||
var self = this; | ||
self.withoutEditorListeners(function() { | ||
self.applyDelta(self.canopClient.undo()); | ||
}); | ||
}, | ||
editorRedo: function() { | ||
var self = this; | ||
self.withoutEditorListeners(function() { | ||
self.applyDelta(self.canopClient.redo()); | ||
}); | ||
}, | ||
getSelections: function canopCodemirrorGetSelections() { | ||
var cmSelections = this.editor.listSelections(); | ||
var selections = []; | ||
for (var i = 0; i < cmSelections.length; i++) { | ||
var cmSelection = cmSelections[i]; | ||
selections.push({ | ||
anchor: this.editor.indexFromPos(cmSelection.anchor), | ||
head: this.editor.indexFromPos(cmSelection.head), | ||
}); | ||
} | ||
return selections; | ||
}, | ||
updateCursor: function CCHupdateCursor(posChanges, oldCursor) { | ||
cursor = canop.changePosition(oldCursor, posChanges, true); | ||
this.editor.setCursor(this.editor.posFromIndex(cursor)); | ||
setSelections: function canopCodemirrorSetSelections(posChanges, oldSelections) { | ||
var cmSelections = []; | ||
for (var i = 0; i < oldSelections.length; i++) { | ||
var oldSelection = oldSelections[i]; | ||
cmSelections.push({ | ||
anchor: canop.changePosition(this.editor.posFromIndex(oldSelection.anchor), posChanges, true), | ||
head: canop.changePosition(this.editor.posFromIndex(oldSelection.head), posChanges, true), | ||
}); | ||
} | ||
this.editor.setSelections(cmSelections); | ||
}, | ||
@@ -159,3 +183,3 @@ | ||
signalReceive: function CCHsignalReceive(event) { | ||
signalReceive: function canopCodemirrorSignalReceive(event) { | ||
var clientId = event.clientId; | ||
@@ -187,3 +211,3 @@ var data = event.data; | ||
// Return a list of widgets that got added. | ||
addSelection: function CCHaddSelection(selection, name) { | ||
addSelection: function canopCodemirrorAddSelection(selection, name) { | ||
var widgets = [this.addUiCursor(selection[0], name)]; | ||
@@ -197,3 +221,3 @@ if (selection[0] !== selection[1]) { | ||
// Return the CodeMirror bookmark associated with the cursor. | ||
addUiCursor: function CCHaddUiCursor(offset, name) { | ||
addUiCursor: function canopCodemirrorAddUiCursor(offset, name) { | ||
var pos = this.editor.posFromIndex(offset); | ||
@@ -227,3 +251,3 @@ var coords = this.editor.cursorCoords(pos); | ||
// Returns a CodeMirror mark. | ||
addUiSelection: function CCHaddUiSelection(selection, name) { | ||
addUiSelection: function canopCodemirrorAddUiSelection(selection, name) { | ||
var color = this.colorFromName(name.toString(), 0.9); | ||
@@ -245,3 +269,3 @@ if (selection[0] < selection[1]) { | ||
// Return a CSS rgb(…) string. | ||
rgbFromLch: function CCHrgbFromLch(luma, chroma, hue) { | ||
rgbFromLch: function canopCodemirrorRgbFromLch(luma, chroma, hue) { | ||
var hue6 = hue / 60; | ||
@@ -272,3 +296,3 @@ var x = chroma * (1 - Math.abs((hue6 % 2) - 1)); | ||
// Small differences in the string yield very different colors. | ||
hueFromName: function CCHcolorFromName(name) { | ||
hueFromName: function canopCodemirrorColorFromName(name) { | ||
var hue = 0; | ||
@@ -284,3 +308,3 @@ for (var i = 0; i < name.length; i++) { | ||
// such that small differences in the string yield very different colors. | ||
colorFromName: function CCHcolorFromName(name, luma) { | ||
colorFromName: function canopCodemirrorColorFromName(name, luma) { | ||
if (luma === undefined) { luma = 0.7; } | ||
@@ -292,3 +316,7 @@ return this.rgbFromLch(luma, 0.6, this.hueFromName(name)); | ||
exports.CanopCodemirrorHook = CanopCodemirrorHook; | ||
canop.ui = canop.ui || {}; | ||
canop.ui.codemirror = function(client, editor) { | ||
return new CanopCodemirror(client, editor); | ||
}; | ||
}(this)); |
Sorry, the diff of this file is not supported yet
207639
17
1761
155
1