Editor: use Function.bind instead of hitch, add test
diff --git a/bundles/org.eclipse.orion.client.core/web/js-tests/jsTestSuite.js b/bundles/org.eclipse.orion.client.core/web/js-tests/jsTestSuite.js
index d726d3d..4393cd7 100644
--- a/bundles/org.eclipse.orion.client.core/web/js-tests/jsTestSuite.js
+++ b/bundles/org.eclipse.orion.client.core/web/js-tests/jsTestSuite.js
@@ -41,7 +41,8 @@
 				"/js-tests/serviceRegistry/test.html",
 				"/js-tests/preferences/test.html",
 				"/js-tests/pluginRegistry/test.html",
-				"/js-tests/testRunAsynch/test.html"
+				"/js-tests/testRunAsynch/test.html",
+				"/js-tests/editor/test-editor.html"
 			]).then(noop, errback);
 		});
 	});
diff --git a/bundles/org.eclipse.orion.client.editor/web/js-tests/editor/test-editor.html b/bundles/org.eclipse.orion.client.editor/web/js-tests/editor/test-editor.html
new file mode 100644
index 0000000..e285495
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.editor/web/js-tests/editor/test-editor.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+<!-- standard scripts -->
+	<script type="text/javascript" src="/requirejs/require.js"></script>
+	<script type="text/javascript">
+	require({
+		baseUrl: '',
+	
+		// set the paths to our library packages
+		packages: [
+			{
+				name: 'dojo',
+				location: '/org.dojotoolkit/dojo',
+				main: 'lib/main-browser',
+				lib: '.'
+			},
+			{
+				name: 'dijit',
+				location: '/org.dojotoolkit/dijit',
+				main: 'lib/main',
+				lib: '.'
+			}
+		],
+	
+		paths: {
+			orion: '/orion',
+			text: '/requirejs/text',
+			i18n: '/requirejs/i18n'
+		}
+	});
+	
+	window.onload = function() {
+		require(["orion/test","testcase-editor"], function(test, testcase) {
+			test.run(testcase);
+		});
+	};
+	</script>
+	<title>TEST: Editor</title>
+</head>
+<body>
+	<div style="padding-bottom:1em;">Editor tests</div>
+	
+	<div id="editorDiv"></div>
+</body>
+</html>
diff --git a/bundles/org.eclipse.orion.client.editor/web/js-tests/editor/testcase-editor.js b/bundles/org.eclipse.orion.client.editor/web/js-tests/editor/testcase-editor.js
new file mode 100644
index 0000000..05a6e84
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.editor/web/js-tests/editor/testcase-editor.js
@@ -0,0 +1,66 @@
+/******************************************************************************* 
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation 
+ ******************************************************************************/
+
+/*jslint */
+/*global define*/
+
+define(["orion/assert", "orion/editor/editor"],
+		function(assert, mEditor) {
+	var tests = {};
+	
+	// ************************************************************************************************
+	// Test supporting util methods
+	
+	// Test our implementation of "bind"
+	tests["test Editor - bind"] = function() {
+		var binder = mEditor.util.bind;
+		
+		// Test: bound function gets proper context
+		var context1 = {},
+		    bound1 = binder.call(
+				function () {
+					assert.strictEqual(this, context1);
+				}, context1);
+		bound1();
+		
+		// Test: argument is passed to bound function
+		var context2 = {},
+		    bound2 = binder.call(
+				function(arg1) {
+					assert.strictEqual(this, context2);
+					assert.strictEqual(arg1, "foo");
+				}, context2);
+		bound2("foo");
+		
+		// Test: fixed arguments are passed to bound function
+		var context3 = {},
+		    bound3 = binder.call(
+				function(arg1, arg2) {
+					assert.strictEqual(context3, this);
+					assert.strictEqual(arg1, "a");
+					assert.strictEqual(arg2, "b");
+				}, context3, "a", "b");
+		bound3();
+		
+		// Test: fixed arguments prepend arguments passed to bound function
+		var context4 = {},
+		    bound4 = binder.call(
+				function(arg1, arg2, arg3, arg4) {
+					assert.strictEqual(context4, this);
+					assert.strictEqual(arg1, "a");
+					assert.strictEqual(arg2, "b");
+					assert.strictEqual(arg3, "c");
+					assert.strictEqual(arg4, "d");
+				}, context4, "a", "b");
+		bound4("c", "d");
+	};
+	
+	return tests;
+});
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/contentAssist.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/contentAssist.js
index 4883714..8a9abb1 100644
--- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/contentAssist.js
+++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/contentAssist.js
@@ -64,10 +64,10 @@
 		init: function() {

 			var isMac = navigator.platform.indexOf("Mac") !== -1;

 			this.textView.setKeyBinding(isMac ? new orion.textview.KeyBinding(' ', false, false, false, true) : new orion.textview.KeyBinding(' ', true), "Content Assist");

-			this.textView.setAction("Content Assist", orion.editor.util.hitch(this, function() {

+			this.textView.setAction("Content Assist", function() {

 				this.showContentAssist(true);

 				return true;

-			}));

+			}.bind(this));

 		},

 		/** Registers a listener with this <code>ContentAssist</code>. */

 		addEventListener: function(/**String*/ type, /**Function*/ listener) {

@@ -259,7 +259,7 @@
 				 * can trigger linked mode behavior in the editor.

 				 */

 				this.getKeywords(this.prefix, buffer, selection).then(

-					orion.editor.util.hitch(this, function(keywords) {

+					function(keywords) {

 						this.proposals = [];

 						for (var i = 0; i < keywords.length; i++) {

 							var proposal = keywords[i];

@@ -303,10 +303,10 @@
 							this.textView.addEventListener("Scroll", this, this.contentAssistListener.onScroll);

 						}

 						this.listenerAdded = true;

-						this.contentAssistPanel.onclick = orion.editor.util.hitch(this, this.click);

+						this.contentAssistPanel.onclick = this.click.bind(this);

 						this.active = true;

 						this.finishing = false;

-					}));

+					}.bind(this));

 			}

 		},

 		/** @private */

diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/editor.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/editor.js
index e5315d7..25abaf0 100644
--- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/editor.js
+++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/editor.js
@@ -9,7 +9,7 @@
  ******************************************************************************/
  
  /*global define window orion:true eclipse:true handleGetAuthenticationError*/
- /*jslint maxerr:150 browser:true devel:true regexp:false*/
+ /*jslint maxerr:150 browser:true devel:true laxbreak:true regexp:false*/
 
 var orion = orion || {};
 orion.editor = orion.editor || {};	
@@ -262,40 +262,40 @@
 			
 			// Set keybindings for keys that apply to different modes
 			textView.setKeyBinding(new orion.textview.KeyBinding(27), "Cancel Current Mode");
-			textView.setAction("Cancel Current Mode", orion.editor.util.hitch(this, function() {
+			textView.setAction("Cancel Current Mode", function() {
 				for (var i=0; i<this._keyModes.length; i++) {
 					if (this._keyModes[i].isActive()) {
 						return this._keyModes[i].cancel();
 					}
 				}
 				return false;
-			}));
+			}.bind(this));
 
-			textView.setAction("lineUp", orion.editor.util.hitch(this, function() {
+			textView.setAction("lineUp", function() {
 				for (var i=0; i<this._keyModes.length; i++) {
 					if (this._keyModes[i].isActive()) {
 						return this._keyModes[i].lineUp();
 					}
 				}
 				return false;
-			}));
-			textView.setAction("lineDown", orion.editor.util.hitch(this, function() {
+			}.bind(this));
+			textView.setAction("lineDown", function() {
 				for (var i=0; i<this._keyModes.length; i++) {
 					if (this._keyModes[i].isActive()) {
 						return this._keyModes[i].lineDown();
 					}
 				}
 				return false;
-			}));
+			}.bind(this));
 
-			textView.setAction("enter", orion.editor.util.hitch(this, function() {
+			textView.setAction("enter", function() {
 				for (var i=0; i<this._keyModes.length; i++) {
 					if (this._keyModes[i].isActive()) {
 						return this._keyModes[i].enter();
 					}
 				}
 				return false;
-			}));
+			}.bind(this));
 						
 			/** @this {orion.editor.Editor} */
 			function updateCursorStatus() {
@@ -452,23 +452,6 @@
  */
 orion.editor.util = {
 	/**
-	 * Returns a function that always executes in the given scope. Similar to <code>dojo.hitch</code>.
-	 * Differences: a scope object must always be provided; the global object is never assumed.
-	 */
-	hitch: function(/**Object*/ scope, /**Function|String*/ method /*, ...*/) {
-		method = typeof method === "string" ? scope[method] : method;
-		if (arguments.length > 2) {
-			var boundArgs = Array.prototype.slice.call(arguments, 2);
-			return function() {
-				return method.apply(scope, boundArgs.concat(Array.slice.call(arguments, 0)));
-			};
-		}
-		return function() {
-			return method.apply(scope, arguments);
-		};
-	},
-	
-	/**
 	 * Event handling helper. Similar to <code>dojo.connect</code>.
 	 * Differences: doesn't return a handle, doesn't support the <code>dontFix</code> parameter.
 	 * @deprecated Once Bug 349957 is fixed, this function should be deleted.
@@ -544,9 +527,34 @@
 			return Math.sin(x * (Math.PI / 2));
 		};
 		return Animation;
-	}())
+	}()),
+	
+	/**
+	 * @private
+	 * @param context Value to be used as the returned function's <code>this</code> value.
+	 * @param [arg1, arg2, ...] Fixed argument values that will prepend any arguments passed to the returned function when it is invoked.
+	 * @returns {Function} A function that always executes this function in the given <code>context</code>.
+	 */
+	bind: function(context) {
+		var fn = this,
+		    fixed = Array.prototype.slice.call(arguments, 1);
+		if (fixed.length) {
+			return function() {
+				return arguments.length
+					? fn.apply(context, fixed.concat(Array.prototype.slice.call(arguments)))
+					: fn.apply(context, fixed);
+			};
+		}
+		return function() {
+			return arguments.length ? fn.apply(context, arguments) : fn.call(context);
+		};
+	}
 };
 
+if (!Function.prototype.bind) {
+	Function.prototype.bind = orion.editor.util.bind;
+}
+
 if (typeof window !== "undefined" && typeof window.define !== "undefined") {
 	define(['orion/textview/keyBinding'], function(){
 		return orion.editor;
diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/editorFeatures.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/editorFeatures.js
index 40c1831..a37085e 100644
--- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/editorFeatures.js
+++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/editorFeatures.js
@@ -126,7 +126,7 @@
 	TextActions.prototype = {
 		init: function() {
 			this._incrementalFindListener = {
-				onVerify: orion.editor.util.hitch(this, function(event){
+				onVerify: function(event){
 					/** @returns {String} with regex special characters escaped. */
 					function regexpEscape(/**String*/ str) {
 						return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&");
@@ -151,17 +151,17 @@
 						event.text = null;
 					} else {
 					}
-				}),
-				onSelection: orion.editor.util.hitch(this, function() {
+				}.bind(this),
+				onSelection: function() {
 					if (!this._incrementalFindIgnoreSelection) {
 						this.toggleIncrementalFind();
 					}
-				})
+				}.bind(this)
 			};
 			// Find actions
 			// These variables are used among the various find actions:
 			this.textView.setKeyBinding(new orion.textview.KeyBinding("f", true), "Find...");
-			this.textView.setAction("Find...", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Find...", function() {
 				if(!this._searcher)
 					return false;
 				var selection = this.textView.getSelection();
@@ -171,26 +171,26 @@
 				}
 				this._searcher.buildToolBar(searchString);
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding("k", true), "Find Next Occurrence");
-			this.textView.setAction("Find Next Occurrence", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Find Next Occurrence", function() {
 				if(this._searcher){
 					this._searcher.findNext(true);
 				}
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding("k", true, true), "Find Previous Occurrence");
-			this.textView.setAction("Find Previous Occurrence", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Find Previous Occurrence", function() {
 				if(this._searcher){
 					this._searcher.findNext(false);
 				}
 				return true;
-			}));
+			}.bind(this));
 
 			this.textView.setKeyBinding(new orion.textview.KeyBinding("j", true), "Incremental Find");
-			this.textView.setAction("Incremental Find", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Incremental Find", function() {
 				if(this._searcher && this._searcher.visible())
 					return true;
 				if (!this._incrementalFindActive) {
@@ -223,8 +223,8 @@
 					}
 				}
 				return true;
-			}));
-			this.textView.setAction("deletePrevious", orion.editor.util.hitch(this, function() {
+			}.bind(this));
+			this.textView.setAction("deletePrevious", function() {
 				if (this._incrementalFindActive) {
 					var p = this._incrementalFindPrefix;
 					p = this._incrementalFindPrefix = p.substring(0, p.length-1);
@@ -250,10 +250,10 @@
 				} else {
 					return false;
 				}
-			}));
+			}.bind(this));
 			
 			// Tab actions
-			this.textView.setAction("tab", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("tab", function() {
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
 				var firstLine = model.getLineAtOffset(selection.start);
@@ -272,9 +272,9 @@
 					return true;
 				}
 				return false;
-			}));
+			}.bind(this));
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(9, false, true), "Unindent Lines");
-			this.textView.setAction("Unindent Lines", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Unindent Lines", function() {
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
 				var firstLine = model.getLineAtOffset(selection.start);
@@ -292,10 +292,10 @@
 				this.textView.setSelection(firstLineStart===selection.start?selection.start:selection.start - 1, selection.end - (lastLine - firstLine + 1) + (selection.end===lastLineStart+1?1:0));
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(38, false, false, true), "Move Lines Up");
-			this.textView.setAction("Move Lines Up", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Move Lines Up", function() {
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
 				var firstLine = model.getLineAtOffset(selection.start);
@@ -323,10 +323,10 @@
 				this.textView.setSelection(insertPos, selectionEnd);
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(40, false, false, true), "Move Lines Down");
-			this.textView.setAction("Move Lines Down", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Move Lines Down", function() {
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
 				var firstLine = model.getLineAtOffset(selection.start);
@@ -354,10 +354,10 @@
 				this.textView.setSelection(selStart, selEnd);
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(38, true, false, true), "Copy Lines Up");
-			this.textView.setAction("Copy Lines Up", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Copy Lines Up", function() {
 				this.startUndo();
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
@@ -374,10 +374,10 @@
 				this.textView.setSelection(insertPos, insertPos+text.length-(isCopyFromLastLine?delimiter.length:0));
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(40, true, false, true), "Copy Lines Down");
-			this.textView.setAction("Copy Lines Down", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Copy Lines Down", function() {
 				this.startUndo();
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
@@ -395,10 +395,10 @@
 				this.textView.setSelection(insertPos+(isCopyFromLastLine?delimiter.length:0), insertPos+text.length);
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding('d', true, false, false), "Delete Selected Lines");
-			this.textView.setAction("Delete Selected Lines", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Delete Selected Lines", function() {
 				this.startUndo();
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
@@ -409,11 +409,11 @@
 				model.setText("", lineStart, lineEnd);
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			// Go To Line action
 			this.textView.setKeyBinding(new orion.textview.KeyBinding("l", true), "Goto Line...");
-			this.textView.setAction("Goto Line...", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Goto Line...", function() {
 				var line = this.textView.getModel().getLineAtOffset(this.textView.getCaretOffset());
 				line = prompt("Go to line:", line + 1);
 				if (line) {
@@ -421,7 +421,7 @@
 					this.editor.onGotoLine(line-1, 0);
 				}
 				return true;
-			}));
+			}.bind(this));
 			
 		},
 			
@@ -527,7 +527,7 @@
 		this.contentAssist = contentAssist;
 		this.linkedMode = linkedMode;
 		if (this.contentAssist) {
-			this.contentAssist.addEventListener("accept", orion.editor.util.hitch(this, this.contentAssistProposalAccepted));
+			this.contentAssist.addEventListener("accept", this.contentAssistProposalAccepted.bind(this));
 		}
 		
 		this.init();
@@ -548,7 +548,7 @@
 		
 			// Block comment operations
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(191, true), "Toggle Line Comment");
-			this.textView.setAction("Toggle Line Comment", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Toggle Line Comment", function() {
 				this.startUndo();
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
@@ -595,7 +595,7 @@
 				}
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			function findEnclosingComment(model, start, end) {
 				var open = "/*", close = "*/";
@@ -631,7 +631,7 @@
 			}
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(191, true, true), "Add Block Comment");
-			this.textView.setAction("Add Block Comment", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Add Block Comment", function() {
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
 				var open = "/*", close = "*/", commentTags = new RegExp("/\\*" + "|" + "\\*/", "g");
@@ -655,10 +655,10 @@
 				this.textView.setSelection(selection.start + open.length, selection.end + open.length + (newLength-oldLength));
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(220, true, true), "Remove Block Comment");
-			this.textView.setAction("Remove Block Comment", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("Remove Block Comment", function() {
 				var selection = this.textView.getSelection();
 				var model = this.textView.getModel();
 				var open = "/*", close = "*/";
@@ -699,7 +699,7 @@
 				}
 				this.endUndo();
 				return true;
-			}));
+			}.bind(this));
 		},
 		/**
 		 * Called when a content assist proposal has been accepted. Inserts the proposal into the
@@ -806,7 +806,7 @@
 		 * on user change. Also escapes the Linked Mode if the text buffer was modified outside of the Linked Mode positions.
 		 */
 		this.linkedModeListener = {
-			onVerify: orion.editor.util.hitch(this, function(event) {
+			onVerify: function(event) {
 				var changeInsideGroup = false;
 				var offsetDifference = 0;
 				for (var i = 0; i < this.linkedModePositions.length; ++i) {
@@ -830,7 +830,7 @@
 					// The change has been done outside of the positions, exit the Linked Mode
 					this.cancel();
 				}
-			})
+			}.bind(this)
 		};
 	}
 	LinkedMode.prototype = {
@@ -872,19 +872,19 @@
 			this.textView.addEventListener("Verify", this, this.linkedModeListener.onVerify);
 
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(9), "nextLinkedModePosition");
-			this.textView.setAction("nextLinkedModePosition", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("nextLinkedModePosition", function() {
 				// Switch to the next group on TAB key
 				this.linkedModeCurrentPositionIndex = ++this.linkedModeCurrentPositionIndex % this.linkedModePositions.length;
 				this.selectTextForLinkedModePosition(this.linkedModePositions[this.linkedModeCurrentPositionIndex]);
 				return true;
-			}));
+			}.bind(this));
 			
 			this.textView.setKeyBinding(new orion.textview.KeyBinding(9, false, true), "previousLinkedModePosition");
-			this.textView.setAction("previousLinkedModePosition", orion.editor.util.hitch(this, function() {
+			this.textView.setAction("previousLinkedModePosition", function() {
 				this.linkedModeCurrentPositionIndex = this.linkedModeCurrentPositionIndex > 0 ? this.linkedModeCurrentPositionIndex-1 : this.linkedModePositions.length-1;
 				this.selectTextForLinkedModePosition(this.linkedModePositions[this.linkedModeCurrentPositionIndex]);
 				return true;
-			}));
+			}.bind(this));
 
 			this.editor.reportStatus("Linked Mode entered");
 		},