From 755ec7eeb2bbf9b682781b006dff5d58fb98b034 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Wed, 16 Nov 2011 11:07:20 +0100 Subject: [PATCH 1/4] First iteration of placeholders in ACE. --- lib/ace/placeholder.js | 196 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 lib/ace/placeholder.js diff --git a/lib/ace/placeholder.js b/lib/ace/placeholder.js new file mode 100644 index 00000000..ed5e7da9 --- /dev/null +++ b/lib/ace/placeholder.js @@ -0,0 +1,196 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Zef Hemel + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +define(function(require, exports, module) { + +var Range = require('ace/range').Range; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var oop = require("./lib/oop"); + +var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) { + var _self = this; + this.length = length; + this.session = session; + this.doc = session.getDocument(); + this.mainClass = mainClass || "ace_placeholder_main"; + this.othersClass = othersClass || "ace_placeholder"; + this.$onUpdate = this.onUpdate.bind(this); + this.doc.on("change", this.$onUpdate); + this.$others = others; + + this.$onCursorChange = function() { + setTimeout(function() { + _self.onCursorChange(); + }); + }; + + this.$pos = pos; + this.setup(); + + session.selection.on("changeCursor", this.$onCursorChange); +}; + +(function() { + + oop.implement(this, EventEmitter); + + this.setup = function() { + var _self = this; + var doc = this.doc; + var session = this.session; + var pos = this.$pos; + + this.pos = doc.createAnchor(pos.row, pos.column); + this.markerId = session.addMarker(new Range(pos.row, pos.column, pos.row, pos.column + this.length), this.mainClass, null, false); + this.pos.on("change", function(event) { + session.removeMarker(_self.markerId); + _self.markerId = session.addMarker(new Range(event.value.row, event.value.column, event.value.row, event.value.column+_self.length), _self.mainClass, null, false); + }); + this.others = []; + this.$others.forEach(function(other) { + var anchor = doc.createAnchor(other.row, other.column); + _self.others.push(anchor); + }); + }; + + this.showOtherMarkers = function() { + if(this.othersActive) return; + var session = this.session; + var _self = this; + this.othersActive = true; + this.others.forEach(function(anchor) { + anchor.markerId = session.addMarker(new Range(anchor.row, anchor.column, anchor.row, anchor.column+_self.length), _self.othersClass, null, false); + anchor.on("change", function(event) { + session.removeMarker(anchor.markerId); + anchor.markerId = session.addMarker(new Range(event.value.row, event.value.column, event.value.row, event.value.column+_self.length), _self.othersClass, null, false); + }); + }); + console.log("Showing"); + }; + + this.hideOtherMarkers = function() { + if(!this.othersActive) return; + this.othersActive = false; + for (var i = 0; i < this.others.length; i++) { + this.session.removeMarker(this.others[i].markerId); + } + console.log("Hiding"); + }; + + this.onUpdate = function(event) { + var delta = event.data; + var range = delta.range; + if(range.start.row !== range.end.row) return; + if(range.start.row !== this.pos.row) return; + var lengthDiff = delta.action === "insertText" ? range.end.column - range.start.column : range.start.column - range.end.column; + + if(range.start.column >= this.pos.column && range.end.column <= this.pos.column + this.length + 1) { + var distanceFromStart = range.start.column - this.pos.column; + this.length += lengthDiff; + if(!this.session.$fromUndo) { + if(delta.action === "insertText") { + for (var i = this.others.length - 1; i >= 0; i--) { + var otherPos = this.others[i]; + var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; + if(otherPos.row === range.start.row && range.start.column < otherPos.column) + newPos.column += lengthDiff; + this.doc.insert(newPos, delta.text); + } + } else if(delta.action === "removeText") { + for (var i = this.others.length - 1; i >= 0; i--) { + var otherPos = this.others[i]; + var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; + if(otherPos.row === range.start.row && range.start.column < otherPos.column) + newPos.column += lengthDiff; + this.doc.remove(new Range(newPos.row, newPos.column, newPos.row, newPos.column - lengthDiff)); + } + } + // Special case: insert in beginning + if(range.start.column === this.pos.column && delta.action === "insertText") { + setTimeout(function() { + this.pos.setPosition(this.pos.row, this.pos.column - lengthDiff); + for (var i = 0; i < this.others.length; i++) { + var other = this.others[i]; + var newPos = {row: other.row, column: other.column - lengthDiff}; + if(other.row === range.start.row && range.start.column < other.column) + newPos.column += lengthDiff; + other.setPosition(newPos.row, newPos.column); + } + }.bind(this)); + } + else if(range.start.column === this.pos.column && delta.action === "removeText") { + setTimeout(function() { + for (var i = 0; i < this.others.length; i++) { + var other = this.others[i]; + if(other.row === range.start.row && range.start.column < other.column) { + other.setPosition(other.row, other.column - lengthDiff); + } + } + }.bind(this)); + } + } + this.pos._dispatchEvent("change", {value: this.pos}); + for (var i = 0; i < this.others.length; i++) { + this.others[i]._dispatchEvent("change", {value: this.others[i]}); + } + } + }; + + this.onCursorChange = function(event) { + var pos = this.session.selection.getCursor(); + if(pos.row === this.pos.row && pos.column >= this.pos.column && pos.column <= this.pos.column + this.length) { + this.showOtherMarkers(); + this._dispatchEvent("cursorEnter", event); + } else { + this.hideOtherMarkers(); + this._dispatchEvent("cursorLeave", event); + } + }; + + this.detach = function() { + this.session.removeMarker(this.markerId); + this.hideOtherMarkers(); + this.doc.removeEventListener("change", this.$onUpdate); + this.pos.detach(); + for (var i = 0; i < this.others.length; i++) { + this.others[i].detach(); + } + }; +}).call(PlaceHolder.prototype); + + +exports.PlaceHolder = PlaceHolder; +}); From 42d6cedf4dd8abeba7bb89c96253ebd28ffa1b23 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Wed, 16 Nov 2011 16:46:28 +0100 Subject: [PATCH 2/4] Minor tweaks. --- lib/ace/placeholder.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ace/placeholder.js b/lib/ace/placeholder.js index ed5e7da9..11eaf767 100644 --- a/lib/ace/placeholder.js +++ b/lib/ace/placeholder.js @@ -184,6 +184,7 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) this.session.removeMarker(this.markerId); this.hideOtherMarkers(); this.doc.removeEventListener("change", this.$onUpdate); + this.session.selection.removeEventListener("changeCursor", this.$onCursorChange); this.pos.detach(); for (var i = 0; i < this.others.length; i++) { this.others[i].detach(); From 90928a61ae8be78fdfa1d1bbb686445627ad8871 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Wed, 16 Nov 2011 16:48:58 +0100 Subject: [PATCH 3/4] Removed default classes. --- lib/ace/placeholder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ace/placeholder.js b/lib/ace/placeholder.js index 11eaf767..2b9ec30d 100644 --- a/lib/ace/placeholder.js +++ b/lib/ace/placeholder.js @@ -45,8 +45,8 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) this.length = length; this.session = session; this.doc = session.getDocument(); - this.mainClass = mainClass || "ace_placeholder_main"; - this.othersClass = othersClass || "ace_placeholder"; + this.mainClass = mainClass; + this.othersClass = othersClass; this.$onUpdate = this.onUpdate.bind(this); this.doc.on("change", this.$onUpdate); this.$others = others; From 7d50280a49fb5bbf89bf21260408906aa5382925 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 17 Nov 2011 10:39:42 +0100 Subject: [PATCH 4/4] Unit tests for placeholder and bugfixes resulting from the test. --- lib/ace/placeholder.js | 10 +-- lib/ace/placeholder_test.js | 143 ++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 lib/ace/placeholder_test.js diff --git a/lib/ace/placeholder.js b/lib/ace/placeholder.js index 2b9ec30d..df6a5f98 100644 --- a/lib/ace/placeholder.js +++ b/lib/ace/placeholder.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { -var Range = require('ace/range').Range; +var Range = require('./range').Range; var EventEmitter = require("./lib/event_emitter").EventEmitter; var oop = require("./lib/oop"); @@ -98,7 +98,6 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) anchor.markerId = session.addMarker(new Range(event.value.row, event.value.column, event.value.row, event.value.column+_self.length), _self.othersClass, null, false); }); }); - console.log("Showing"); }; this.hideOtherMarkers = function() { @@ -107,7 +106,6 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) for (var i = 0; i < this.others.length; i++) { this.session.removeMarker(this.others[i].markerId); } - console.log("Hiding"); }; this.onUpdate = function(event) { @@ -117,7 +115,7 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) if(range.start.row !== this.pos.row) return; var lengthDiff = delta.action === "insertText" ? range.end.column - range.start.column : range.start.column - range.end.column; - if(range.start.column >= this.pos.column && range.end.column <= this.pos.column + this.length + 1) { + if(range.start.column >= this.pos.column && range.start.column <= this.pos.column + this.length + 1) { var distanceFromStart = range.start.column - this.pos.column; this.length += lengthDiff; if(!this.session.$fromUndo) { @@ -149,7 +147,7 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) newPos.column += lengthDiff; other.setPosition(newPos.row, newPos.column); } - }.bind(this)); + }.bind(this), 0); } else if(range.start.column === this.pos.column && delta.action === "removeText") { setTimeout(function() { @@ -159,7 +157,7 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) other.setPosition(other.row, other.column - lengthDiff); } } - }.bind(this)); + }.bind(this), 0); } } this.pos._dispatchEvent("change", {value: this.pos}); diff --git a/lib/ace/placeholder_test.js b/lib/ace/placeholder_test.js new file mode 100644 index 00000000..6130aa3f --- /dev/null +++ b/lib/ace/placeholder_test.js @@ -0,0 +1,143 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +if (typeof process !== "undefined") { + require("amd-loader"); + require("./test/mockdom"); +} + +define(function(require, exports, module) { + +var EditSession = require("./edit_session").EditSession; +var Editor = require("./editor").Editor; +var MockRenderer = require("./test/mockrenderer").MockRenderer; +var assert = require("./test/assertions"); +var JavaScriptMode = require("./mode/javascript").Mode; +var PlaceHolder = require('./placeholder').PlaceHolder; + +module.exports = { + + "test: simple at the end appending of text" : function() { + var session = new EditSession("var a = 10;\nconsole.log(a, a);", new JavaScriptMode()); + var editor = new Editor(new MockRenderer(), session); + + new PlaceHolder(session, 1, {row: 0, column: 4}, [{row: 1, column: 12}, {row: 1, column: 15}]); + + editor.moveCursorTo(0, 5); + editor.insert('b'); + assert.equal(session.doc.getValue(), "var ab = 10;\nconsole.log(ab, ab);"); + editor.insert('cd'); + assert.equal(session.doc.getValue(), "var abcd = 10;\nconsole.log(abcd, abcd);"); + editor.remove('left'); + editor.remove('left'); + editor.remove('left'); + assert.equal(session.doc.getValue(), "var a = 10;\nconsole.log(a, a);"); + }, + + "test: inserting text outside placeholder" : function() { + var session = new EditSession("var a = 10;\nconsole.log(a, a);\n", new JavaScriptMode()); + var editor = new Editor(new MockRenderer(), session); + + new PlaceHolder(session, 1, {row: 0, column: 4}, [{row: 1, column: 12}, {row: 1, column: 15}]); + + editor.moveCursorTo(2, 0); + editor.insert('b'); + assert.equal(session.doc.getValue(), "var a = 10;\nconsole.log(a, a);\nb"); + }, + + "test: insertion at the beginning" : function(next) { + var session = new EditSession("var a = 10;\nconsole.log(a, a);", new JavaScriptMode()); + var editor = new Editor(new MockRenderer(), session); + + var p = new PlaceHolder(session, 1, {row: 0, column: 4}, [{row: 1, column: 12}, {row: 1, column: 15}]); + + editor.moveCursorTo(0, 4); + editor.insert('$'); + assert.equal(session.doc.getValue(), "var $a = 10;\nconsole.log($a, $a);"); + editor.moveCursorTo(0, 4); + // Have to put this in a setTimeout because the anchor is only fixed later. + setTimeout(function() { + editor.insert('v'); + assert.equal(session.doc.getValue(), "var v$a = 10;\nconsole.log(v$a, v$a);"); + next(); + }, 10); + }, + + "test: detaching placeholder" : function() { + var session = new EditSession("var a = 10;\nconsole.log(a, a);", new JavaScriptMode()); + var editor = new Editor(new MockRenderer(), session); + + var p = new PlaceHolder(session, 1, {row: 0, column: 4}, [{row: 1, column: 12}, {row: 1, column: 15}]); + + editor.moveCursorTo(0, 5); + editor.insert('b'); + assert.equal(session.doc.getValue(), "var ab = 10;\nconsole.log(ab, ab);"); + p.detach(); + editor.insert('cd'); + assert.equal(session.doc.getValue(), "var abcd = 10;\nconsole.log(ab, ab);"); + }, + + "test: events" : function() { + var session = new EditSession("var a = 10;\nconsole.log(a, a);", new JavaScriptMode()); + var editor = new Editor(new MockRenderer(), session); + + var p = new PlaceHolder(session, 1, {row: 0, column: 4}, [{row: 1, column: 12}, {row: 1, column: 15}]); + var entered = false; + var left = false; + p.on("cursorEnter", function() { + entered = true; + }); + p.on("cursorLeave", function() { + left = true; + }); + + editor.moveCursorTo(0, 0); + editor.moveCursorTo(0, 4); + p.onCursorChange(); // Have to do this by hand because moveCursorTo doesn't trigger the event + assert.ok(entered); + editor.moveCursorTo(1, 0); + p.onCursorChange(); // Have to do this by hand because moveCursorTo doesn't trigger the event + assert.ok(left); + } +}; + +}); + +if (typeof module !== "undefined" && module === require.main) { + require("asyncjs").test.testcase(module.exports).exec() +} \ No newline at end of file