/******************************************************************************* 
 * @license
 * Copyright (c) 2011, 2012 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 
 ******************************************************************************/
/*eslint-env amd, browser, mocha*/
define([
	"chai/chai",
	"js-tests/editor/mockTextView",
	"js-tests/editor/textMateStyler/testGrammars",
	"orion/editor/textMateStyler",
], function(chai, mMockTextView, mTestGrammars, mTextMateStyler) {
	var assert = chai.assert;

	var view;

	function setup() {
		view = createTextView();
	}
	
	function teardown() {
		view = null;
	}

	describe("TextMateStyler", function() {
		beforeEach(setup);
		afterEach(teardown);

		// ************************************************************************************************
		// Test supporting util methods
		describe("Util", function() {
			// "Extended" regex syntax with whitespace and comments.
			it("#toRegex() - (?x)X and (?x:X)",  function() {
				var regex1 = mTextMateStyler.RegexUtil.toRegExp("", false);
				assertEmptyRegex(regex1);
				
				var regex2 = mTextMateStyler.RegexUtil.toRegExp("(?x)(a+  #fizz  \r\n     b)?", false);
				assert.equal(regex2.source, "(a+b)?");
				
				var regex3 = mTextMateStyler.RegexUtil.toRegExp("(?x:(a+  #foo\n       b  #bar  \n  )?)", false);
				assert.equal(regex3.source, "(a+b)?");
				
				var regex5 = mTextMateStyler.RegexUtil.toRegExp("(?x)(aa[ bc]d)", false);
				assert.equal(regex5.source, "(aa[ bc]d)");
				
				var regex6 = mTextMateStyler.RegexUtil.toRegExp("(?x:aa\\[ bc)", false);
				assert.equal(regex6.source, "aa\\[bc");
				
				var regex7 = mTextMateStyler.RegexUtil.toRegExp("(?x: \n \\\\ b)", false); // (literal backslash)b
				assert.equal(regex7.source, "\\\\b");
				
				var str4 = '(?x:                # turn on extended mode\n                     \\\\                # a literal backslash\n                     (?:               # ...followed by...\n                       ["\\\/bfnrt]     # one of these characters\n                       |               # ...or...\n                       u               # a u\n                       [0-9a-fA-F]{4}  # and four hex digits\n                     )\n                   )';
				var regex4 = mTextMateStyler.RegexUtil.toRegExp(str4, false);
				assert.equal(regex4.source, '\\\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})');
				
				// Case-insensitive flag applied to entire regex pattern
				var str8 = "(?i)fIzz",
				    str9 = "(?i:BUzz)",
				    str10 = "(?i)(foo(bar(baz.+)?)*)+",
				    str11 = "(?i:(a(b|c)d(e(f))g))",
				    regex8 = mTextMateStyler.RegexUtil.toRegExp(str8, false),
				    regex9 = mTextMateStyler.RegexUtil.toRegExp(str9, false),
				    regex10 = mTextMateStyler.RegexUtil.toRegExp(str10, false),
				    regex11 = mTextMateStyler.RegexUtil.toRegExp(str11, false);
				assert.equal(regex8.source, "fIzz");
				assert.equal(regex8.ignoreCase, true);
				assert.equal(regex9.source, "BUzz");
				assert.equal(regex9.ignoreCase, true);
				assert.equal(regex10.source, "(foo(bar(baz.+)?)*)+");
				assert.equal(regex10.ignoreCase, true);
				assert.equal(regex11.source, "(a(b|c)d(e(f))g)");
				assert.equal(regex11.ignoreCase, true);
			});
			it("#groupify", function() {
				var result1 = mTextMateStyler.RegexUtil.groupify(new RegExp("")),
				    regex1 = result1[0];
				assertEmptyRegex(regex1);
				
				var result2 = mTextMateStyler.RegexUtil.groupify(/()/),
				    regex2 = result2[0], 
				    map2 = result2[1],
				    con2 = result2[2];
				assert.equal(regex2.source, "()");
				assert.equal(map2[1], 1);
				assertHasProps(con2, "1");
				
				var result3 = mTextMateStyler.RegexUtil.groupify(/a+/),
				    regex3 = result3[0],
				    map3 = result3[1],
				    con3 = result3[2];
				assert.equal(regex3.source, "(a+)");
				assertDoesntHaveProps(map3, "1");
				assertHasProps(con3, "1");
				
				var result4 = mTextMateStyler.RegexUtil.groupify(/x(a+)b?/),
				    regex4 = result4[0],
				    map4 = result4[1],
				    con4 = result4[2];
				assert.equal(regex4.source, "(x)(a+)(b?)");
				assert.equal(map4[1], 2);
				assertDoesntHaveProps(map4, "2");
				assertHasProps(con4, "1");
				
				var result5 = mTextMateStyler.RegexUtil.groupify(/a+(?=b)c+(?!d*x?y)e+/),
				    regex5 = result5[0],
				    map5 = result5[1],
				    con5 = result5[2];
				assert.equal(regex5.source, "(a+)(?=b)(c+)(?!d*x?y)(e+)");
				assertDoesntHaveProps(map5, "1");
				assertHasProps(con5, "1", "2", "3");
				
				// Non-capturing group
				var result6 = mTextMateStyler.RegexUtil.groupify(/(?:x+(a+)(b+))(c+)/),
				    regex6 = result6[0],
				    map6 = result6[1],
				    con6 = result6[2];
				assert.equal(regex6.source, "(?:(x+)(a+)(b+))(c+)");
				assert.equal(map6[1], 2);
				assert.equal(map6[2], 3);
				assert.equal(map6[3], 4);
				assertHasProps(con6, "1", "2", "3", "4");
				
				// Capturing group inside a lookahead
				var result7 = mTextMateStyler.RegexUtil.groupify(/x+(?=aa(b+))z{2,}/),
				    regex7 = result7[0],
				    map7 = result7[1],
				    con7= result7[2];
				assert.equal(regex7.source, "(x+)(?=aa(b+))(z{2,})");
				assert.equal(map7[1], "2"); // (b+)
				assertHasProps(con7, "1", "3"); // (b+) is group 2, and it's NOT consuming
				
				// Escaping \( and \)
				var result8 = mTextMateStyler.RegexUtil.groupify(new RegExp(/aa(\(x\))bb|[^cd]/)),
				    regex8 = result8[0],
				    map8 = result8[1],
				    con8 = result8[2];
				assert.equal(regex8.source, "(aa)(\\(x\\))(bb|[^cd])");
				assert.equal(map8[1], "2"); // (\(x\))
				assertHasProps(con8, "1", "2", "3");
				
				// Escaping \
				var result9 = mTextMateStyler.RegexUtil.groupify(/C:\\(\w+)\\/),
				    regex9 = result9[0],
				    map9 = result9[1],
				    con9 = result9[2];
				assert.equal(regex9.source, /(C:\\)(\w+)(\\)/.source);
				assert.equal(map9[1], "2"); // (\w+)
				assertHasProps(con9, "1", "2", "3");
				
				// Backrefs
				var result10 = mTextMateStyler.RegexUtil.groupify(/x?(a+)x\1x?/),
				    regex10 = result10[0],
				    map10 = result10[1],
				    con10 = result10[2];
				assert.equal(regex10.source, "(x?)(a+)(x\\2x?)");
				assert.equal(map10[1], "2");
				assertHasProps(con10, "1", "2", "3");
				
				// Backrefs with the false parameter (should not be touched)
				var result11 = mTextMateStyler.RegexUtil.groupify(/(x+)(y+)(z+)\2/),
				    regex11 = result11[0],
				    map11 = result11[1],
				    con11 = result11[2];
				assert.equal(regex11.source, "(x+)(y+)(z+)(\\2)");
				assert.equal(map11[1], "1");
				assert.equal(map11[2], "2");
				assert.equal(map11[3], "3");
				assertHasProps(con11, "1", "2", "3");
				
				// Operators between groups
				var result12 = mTextMateStyler.RegexUtil.groupify(/(ab)+|(cd)/),
				    regex12 = result12[0],
				    map12 = result12[1],
				    con12 = result12[2];
				assert.equal(regex12.source, "((ab)+)|(cd)"); 
				assert.equal(map12[1], 2); // (ab) is now 2
				assert.equal(map12[2], 3); // (cd) is now 3
				assertHasProps(con12, "1");
				
				var result13 = mTextMateStyler.RegexUtil.groupify(/(,)|(?=\})/),
				    regex13 = result13[0],
				    map13 = result13[1],
				    con13 = result13[2];
				assert.equal(regex13.source, /(,)|(?=\})/.source);
				assert.equal(map13[1], 1);
				assertHasProps(con13, "1"); // no consuming
			});
		}); // describe Util

		describe("styler", function() {
			it("create", function() {
				var styler = makeStyler(view, mTestGrammars.SampleGrammar);
			});
		}); // describe styler

		describe("initial styling", function() {
			// Test initial styling of buffer
			it("style one line", function() {
				var styler = makeStyler(view, mTestGrammars.SampleGrammar);
				view.setText("fizzer");
				
				// expect fi[zz]er
				var invalidScopeName = mTestGrammars.SampleGrammar.repository.badZ.name;
				assertLineScope(view, styler, 0, [
						[2, 4, invalidScopeName] // z
					]);
			});
			it("style multiple lines", function() {
				var styler = makeStyler(view, mTestGrammars.SampleGrammar);
				var line0Text = "no_important_stuff_here",
				    line1Text = "    this    var    &&";
				setLines(view, [line0Text, line1Text]);
				
				assertLineScope(view, styler, 0, []);
				assertLineScope(view, styler, 1, [
					[4, 8, "keyword.other.mylang"],				// this
					[12, 15, "keyword.other.mylang"],			// var
					[19, 21, "keyword.operator.logical.mylang"]	// &&
				]);
			});
			it("begin/end single line - subrule", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				
				// test subrule invalid.illegal.badcomment.mylang applied to "--"
				lines = [ "<!--a--a-->" ];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang"], // <!--
					[4, 5, "comment.block.mylang"], // a
					[5, 7, "invalid.illegal.badcomment.mylang"], // --
					[7, 8, "comment.block.mylang"], // a
					[8, 11, "punctuation.definition.comment.mylang"] // -->
				]);
			});
			it("begin/end 1 line - subrule exited", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				
				// Test that the rule assigning -- to "invalid.illegal.badcomment.mylang" only takes effect
				// inside the <!-- --> block and not outside it
				lines = [ "-- <!--a--b--> --" ];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[3, 7, "punctuation.definition.comment.mylang"], // <!--
					[7, 8, "comment.block.mylang"], // a
					[8, 10, "invalid.illegal.badcomment.mylang"], // --
					[10, 11, "comment.block.mylang"], // b
					[11, 14, "punctuation.definition.comment.mylang"] // -->
				]);
			});
			it("begin/end single line - name", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				
				// test that "name" of begin/end rule is applied to text between the delimiters
				lines = [ "<!--aaaaaa-->" ];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[0, 4,   "punctuation.definition.comment.mylang"], // <!--
					[4, 10,  "comment.block.mylang"], // aaaaaa
					[10, 13, "punctuation.definition.comment.mylang"] // -->
				]);
			});
			it("begin/end 2 lines - just delimiters", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"<!--",
					"-->"
				];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [ [0, 4, "punctuation.definition.comment.mylang"] ]); // <!--
				assertLineScope(view, styler, 1, [ [0, 3, "punctuation.definition.comment.mylang"] ]); // -->
			});
			it("begin/end 2 lines - with content", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"<!--a",
					"b-->"
				];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang"], // <!--
					[4, 5, "comment.block.mylang"]  // a
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "comment.block.mylang"], // b
					[1, 4, "punctuation.definition.comment.mylang"] // -->
				]);
			});
		
			it("begin/end 3 lines - with leading/trailing content", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"a<!--c",
					"commentc",
					"omment-->bb"
				];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[1, 5, "punctuation.definition.comment.mylang"], // <!--
					[5, 6, "comment.block.mylang"] // c
				]);
				assertLineScope(view, styler, 1, [
					[0, 8, "comment.block.mylang"] // commentc
				]);
				assertLineScope(view, styler, 2, [
					[0, 6, "comment.block.mylang"], // omment
					[6, 9, "punctuation.definition.comment.mylang"] // -->
				]);
			});
			
			it("b/e region inside b/e region", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"<!--[]-->",
					"<!--[  ]-->",
					"<!--[ a ]-->",
					"<!--[   ",
					"b b"
				];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "meta.brace.square.open.mylang", "["],
					[5, 6, "meta.brace.square.close.mylang", "]"],
					[6, 9, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "meta.brace.square.open.mylang", "["],
					[5, 7, "invalid.illegal.whitespace.mylang", "  "],
					[7, 8, "meta.brace.square.close.mylang", "]"],
					[8, 11, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "meta.brace.square.open.mylang", "["],
					[5, 6, "invalid.illegal.whitespace.mylang", " "],
					[6, 7, "meta.insquare.mylang", "a"],
					[7, 8, "invalid.illegal.whitespace.mylang", " "],
					[8, 9, "meta.brace.square.close.mylang", "]"],
					[9, 12, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 3, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "meta.brace.square.open.mylang", "["],
					[5, 8, "invalid.illegal.whitespace.mylang", "   "]
				]);
				assertLineScope(view, styler, 4, [
					[0, 1, "meta.insquare.mylang", "b"],
					[1, 2, "invalid.illegal.whitespace.mylang", " "],
					[2, 3, "meta.insquare.mylang", "b"]
				]);
			});
			
			// Test for Bug 347486, ensure we try all subrules on each line
			it("all subrules are tried", function() {
				var styler = makeStyler(view, mTestGrammars.SampleGrammar);
				var lines = [
					'break var "foo" null 123',
					"z if"
				];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [
					[0, 5, "keyword.control.mylang", "break"],
					[6, 9, "keyword.other.mylang", "var"],
					[10, 15, "constant.character.mylang", '"foo"'],
					[16, 20, "constant.language.mylang", "null"],
					[21, 24, "constant.numeric.mylang", "123"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "invalid.illegal.idontlikez.mylang", "z"],
					[2, 4, "keyword.control.mylang", "if"]
				]);
			});
			
			it("scope to non-0 capturing groups", function() {
				var styler = makeStyler(view, mTestGrammars.ComplexCaptures);
				setLines(view, [
					"function (arg1, arg2)",
					"aafunction () bb",
					"nothinghere"
				]);
				assertLineScope(view, styler, 0, [
					[0, 8, "keyword.function", "function"],
					[9, 21, "meta.arglist.function", "(arg1, arg2)"]
				]);
				assertLineScope(view, styler, 1, [
					[2, 10, "keyword.function", "function"],
					[11, 13, "meta.arglist.function", "()"]
				]);
				assertLineScope(view, styler, 2, [
				]);
			});
			
			it("scope to non-0 capturing groups with end-to-begin backrefs", function() {
				var styler = makeStyler(view, mTestGrammars.ComplexCaptures);
				setLines(view, [
					"[foo]bar[/foo]",
					"[a][b][/b][/a]"
				]);
				assertLineScope(view, styler, 0, [
					// [foo]
					[0, 1, "punctuation.definition.tag.opener", "["],
					[1, 4, "entity.tag.open.name", "foo"],
					[4, 5, "punctuation.definition.tag.closer", "]"],
					// [/foo]
					[8, 9, "punctuation.definition.tag.opener", "["],
					[10, 13, "entity.tag.close.name", "foo"],
					[13, 14, "punctuation.definition.tag.closer", "]"]
				]);
				assertLineScope(view, styler, 1, [
					// [a]
					[0, 1, "punctuation.definition.tag.opener", "["],
					[1, 2, "entity.tag.open.name", "a"],
					[2, 3, "punctuation.definition.tag.closer", "]"],
					// [b]
					[3, 4, "punctuation.definition.tag.opener", "["],
					[4, 5, "entity.tag.open.name", "b"],
					[5, 6, "punctuation.definition.tag.closer", "]"],
					[6, 7, "punctuation.definition.tag.opener", "["],
					// [/b]
					[8, 9, "entity.tag.close.name", "b"],
					[9, 10, "punctuation.definition.tag.closer", "]"],
					[10, 11, "punctuation.definition.tag.opener", "["],
					// [/a]
					[12, 13, "entity.tag.close.name", "a"],
					[13, 14, "punctuation.definition.tag.closer", "]"]
				]);
			});
			
			it("scope to non-0 capturing groups with gaps between them", function() {
				var styler = makeStyler(view, mTestGrammars.ComplexCaptures);
				setLines(view, [
					"xxxaaxxxbbb"
				]);
				assertLineScope(view, styler, 0, [
					[3, 5, "meta.a", "aa"],
					[8, 11, "keyword.b", "bbb"]
				]);
			});
		}); // describe(initial)
	
		// ************************************************************************************************
		// Test damage/repair styling
		describe("damage/repair styling", function() {
			it("inside region", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"<!--",
					"a",
					"-->"
				];
				setLines(view, lines);
				assertLineScope(view, styler, 0, [ [0, 4, "punctuation.definition.comment.mylang", "<!--"] ]);
				assertLineScope(view, styler, 1, [ [0, 1, "comment.block.mylang", "a"] ]);
				assertLineScope(view, styler, 2, [ [0, 3, "punctuation.definition.comment.mylang", "-->"] ]);
				
				/*
				<!--
				axxxx
				-->
				*/
				changeLine(view, "xxxx", 1, 1, 1); // insert xxxx after a on line 1
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang"] // <!--
				]);
				assertLineScope(view, styler, 1, [
					[0, 5, "comment.block.mylang"] // axxxx
				]);
				assertLineScope(view, styler, 2, [
					[0, 3, "punctuation.definition.comment.mylang"] // -->
				]);
			}, false);
			it("add non-region text that follows region", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"<!--",
					"a",
					"-->"
				];
				setLines(view, lines);
				
				changeLine(view, "char", 2, 3, 3);
				/*
				<!--
				a
				-->char
				*/
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang"] // <!--
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "comment.block.mylang"] // a
				]);
				assertLineScope(view, styler, 2, [
					[0, 3, "punctuation.definition.comment.mylang"], // -->
					[3, 7, "storage.type.mylang"] // char
				]);
			});
			it("add non-region text that precedes region", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				var lines;
				lines = [
					"<!--",
					"a",
					"-->int"
				];
				setLines(view, lines);
				
				changeLine(view, "char", 0, 0, 0);
				/*
				char<!--
				a
				-->
				*/
				assertLineScope(view, styler, 0, [
					[0, 4, "storage.type.mylang", "char"],
					[4, 8, "punctuation.definition.comment.mylang", "<!--"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"],
					[3, 6, "storage.type.mylang", "int"]
				]);
			});
			// add non-region text between regions
			it("add non-region text between regions", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--aaa-->",
					"<!--bbb-->"
				]);
				changeLine(view, "int xxx char", 0, 10, 10);
				/*
				<!--aaa-->int xxx char
				<!--bbb-->
				*/
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 7, "comment.block.mylang", "aaa"],
					[7, 10, "punctuation.definition.comment.mylang", "-->"],
					[10, 13, "storage.type.mylang", "int"],
					// xxx is ignored: doesn't match anything
					[18, 22, "storage.type.mylang", "char"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 7, "comment.block.mylang", "bbb"],
					[7, 10, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			// creates a new region by adding the start block
			it("add 'start' 1", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"a",
					"-->"
				]);
				
				/*
				char<!--a
				-->
				*/
				changeLine(view, "char<!--", 0, 0, 0);
				assertLineScope(view, styler, 0, [
					[0, 4, "storage.type.mylang", "char"],
					[4, 8, "punctuation.definition.comment.mylang", "<!--"],
					[8, 9, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			// creates a new region by adding the start block
			it("add 'start' 2", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"xxxx<!--a",
					"-->"
				]);
				assertLineScope(view, styler, 0, [
					[4, 8, "punctuation.definition.comment.mylang", "<!--"],
					[8, 9, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"]
				]);
				
				// Add another start that knocks out the earlier one
				/*
				<!--xxxx<!--a
				-->
				*/
				changeLine(view, "<!--", 0, 0, 0);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 10, "comment.block.mylang", "xxxx<!"],
					[10, 12, "invalid.illegal.badcomment.mylang", "--"],
					[12, 13, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"]
				]);
				
				// Add another line just to make sure
				/*
				<!--xxxx<!--a
				b
				-->
				*/
				changeLine(view, NL + "b", 0, 13, 13);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 10, "comment.block.mylang", "xxxx<!"],
					[10, 12, "invalid.illegal.badcomment.mylang", "--"],
					[12, 13, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "comment.block.mylang", "b"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			// Creates a new region at eof. New region never matches its end (ie. extends until eof)
			it("add 'start' at eof, no 'end'", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--a-->"
				]);
				
				/*
				<!--a--><!--
				*/
				changeLine(view, "<!--", 0, 8, 8);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 12, "punctuation.definition.comment.mylang", "--><!--"]
				]);
				
				/*
				<!--a--><!--b
				*/
				changeLine(view, "b", 0, 12, 12);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 12, "punctuation.definition.comment.mylang", "--><!--"],
					[12, 13, "comment.block.mylang", "b"]
				]);
				
				/*
				<!--a--><!--b-->x
				*/
				changeLine(view, "-->x", 0, 13, 13);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 12, "punctuation.definition.comment.mylang", "--><!--"],
					[12, 13, "comment.block.mylang", "b"],
					[13, 16, "punctuation.definition.comment.mylang", "-->"]
					// x is ignored
				]);
			});
			it("add 'start' at eof on new line incr", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--a-->"
				], NL);
				// Helper since line 0's scope doesn't change in this test
				function assertLine0Scope() {
					assertLineScope(view, styler, 0, [
						[0, 4, "punctuation.definition.comment.mylang", "<!--"],
						[4, 5, "comment.block.mylang", "a"],
						[5, 8, "punctuation.definition.comment.mylang", "-->"]
					]);
				}
				
				// Add the newline first
				/*
				<!--a-->
				
				*/
				changeLine(view, NL, 0, 8, 8);
				assertLine0Scope();
				assertLineScope(view, styler, 1, [
					// empty line
				]);
				
				// Now add the start INCREMENTALLY
				/*
				<!--a-->
				<
				*/
				changeLine(view, "<", 1, 0, 0);
				assertLine0Scope();
				assertLineScope(view, styler, 1, [ /* no scope on line 1 */ ]);
				
				/*
				<!--a-->
				<!
				*/
				changeLine(view, "!", 1, 1, 1);
				assertLine0Scope();
				assertLineScope(view, styler, 1, [ /* no scope on line 1 */ ]);
				
				/*
				<!--a-->
				<!-
				*/
				changeLine(view, "-", 1, 2, 2);
				assertLine0Scope();
				assertLineScope(view, styler, 1, [ /* no scope on line 1 */ ]);
				
				/*
				<!--a-->
				<!--
				*/
				changeLine(view, "-", 1, 3, 3);
				assertLine0Scope();
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"]
				]);
				
				// Add something inside the new start, make sure it gets the right style
				/*
				<!--a-->
				<!--b
				*/
				changeLine(view, "b", 1, 4, 4);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"]
				]);
			});
			it("add 'end' 1", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--has no end"
				]);
				
				/*
				<!--has an end-->
				*/
				changeLine(view, "an end-->", 0, 8, 14);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 14, "comment.block.mylang", "has an end"],
					[14, 17, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			// Add an end when there are multiple regions
			it("add 'end' 2", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--fizz-->",
					"<!--buzz"
				]);
				
				// complete buzz's end token incrementally
				/*
				<!--fizz-->
				<!--buzz-
				*/
				changeLine(view, "-", 1, 8, 8);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", "fizz"],
					[8, 11, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 9, "comment.block.mylang", "buzz-"]
				]);
				
				/*
				<!--fizz-->
				<!--buzz--
				*/
				changeLine(view, "-", 1, 9, 9);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", "fizz"],
					[8, 11, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", "buzz"],
					[8, 10, "invalid.illegal.badcomment.mylang", "--"]
				]);
				
				/*
				<!--fizz-->
				<!--buzz-->
				*/
				changeLine(view, ">", 1, 10, 10);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", "fizz"],
					[8, 11, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", "buzz"],
					[8, 11, "punctuation.definition.comment.mylang", "-->"]
				]);
			}, false);
			// Add "end" where a following region exists
			it("add 'end' 3", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--b",
					"<!--c-->" // here <!-- is <! (comment) and -- (invalid) not <!-- (punctuation)
				]);
				
				/*
				<!--b-->
				<!--c-->
				*/
				changeLine(view, "-->", 0, 5, 5);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "c"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			// Add and "end" when there exist preceding and following regions
			it("add 'end' 4", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--a-->",
					"<!--b",
					"<!--c-->" // here <!-- is <! (comment) and -- (invalid) not <!-- (punctuation)
				]);
				// check initial styles for sanity
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 2, "comment.block.mylang", "<!"],
					[2, 4, "invalid.illegal.badcomment.mylang", "--"],
					[4, 5, "comment.block.mylang", "c"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				
				// Add end on line 1. Should affect line2
				/*
				<!--a-->
				<!--b-->
				<!--c-->
				*/
				changeLine(view, "-->", 1, 5, 5);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "c"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			it("remove 'start'", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--xxx int-->"
				]);
				
				/*
				xxx int-->
				*/
				changeLine(view, "", 0, 0, 4);
				assertLineScope(view, styler, 0, [
					[4, 7, "storage.type.mylang", "int"]
				]);
			});
			it("remove 'end' 1", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--a-->",
					"<!--b-->",
					"<!--c-->"
				]);
				// Remove end on line1, affects line2 also
				/*
				<!--a-->
				<!--b
				<!--c-->x
				*/
				changeLine(view, "", 1, 5, 8);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 2, "comment.block.mylang", "<!"],
					[2, 4, "invalid.illegal.badcomment.mylang", "--"],
					[4, 5, "comment.block.mylang", "c"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			// Remove end of a nested region that has sibling regions before and after it
			it("remove 'end' 2", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--a",
					"[a1]",
					"[a2]", // We'll remove this one's end ]
					"[a3]",
					"-->",
					"<!--b-->"
				]);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "meta.brace.square.open.mylang", "["],
					[1, 3, "meta.insquare.mylang", "a1"],
					[3, 4, "meta.brace.square.close.mylang", "]"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 1, "meta.brace.square.open.mylang", "["],
					[1, 3, "meta.insquare.mylang", "a2"],
					[3, 4, "meta.brace.square.close.mylang", "]"]
				]);
				assertLineScope(view, styler, 3, [
					[0, 1, "meta.brace.square.open.mylang", "["],
					[1, 3, "meta.insquare.mylang", "a3"],
					[3, 4, "meta.brace.square.close.mylang", "]"]
				]);
				assertLineScope(view, styler, 4, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 5, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				
				// Remove end on line2, which makes a2 region extend onto next line
				/*
				<!--a
				[a1]
				[a2
				[a3]
				-->
				<!--b-->
				*/
				changeLine(view, "", 2, 3, 4);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "meta.brace.square.open.mylang", "["],
					[1, 3, "meta.insquare.mylang", "a1"],
					[3, 4, "meta.brace.square.close.mylang", "]"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 1, "meta.brace.square.open.mylang", "["],
					[1, 3, "meta.insquare.mylang", "a2"]
				]);
				assertLineScope(view, styler, 3, [
					[0, 3, "meta.insquare.mylang", "[a3"],
					[3, 4, "meta.brace.square.close.mylang", "]"]
				]);
				assertLineScope(view, styler, 4, [
					[0, 3, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 5, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
			});
			it("remove 'end' at eof", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!--a-->",
					"<!--b-->"
				]);
				/*
				<!--a-->
				<!--b
				*/
				changeLine(view, "", 1, 5, 8);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "a"],
					[5, 8, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 5, "comment.block.mylang", "b"]
				]);
			});
			// remove "end" token of last region incrementally -- Bug 349506
			it("remove final 'end' - Bug 349506", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!-- foo",
					"bar-->",
					"baz"
				]);
				// sanity check
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", " foo"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 3, "comment.block.mylang", "bar"],
					[3, 6, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 2, []);
				
				// incrementally remove end token
				/*
				<!-- foo
				bar
				baz
				*/
				changeLine(view, "", 1, 5, 6); // bar--
				changeLine(view, "", 1, 4, 5); // bar-
				changeLine(view, "", 1, 3, 4); // bar
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", " foo"]
				]);
				assertLineScope(view, styler, 1, [ [0, 3, "comment.block.mylang", "bar"] ]);
				assertLineScope(view, styler, 2, [ [0, 3, "comment.block.mylang", "baz"] ]);
			});
			it("add final 'end', check redraw - Bug 349642", function() {
				var styler = makeStyler(view, mTestGrammars.SampleBeginEndGrammar);
				setLines(view, [
					"<!-- foo",
					"bar--",
					"baz"
				]);
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", " foo"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 3, "comment.block.mylang", "bar"],
					[3, 5, "invalid.illegal.badcomment.mylang", "--"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 3, "comment.block.mylang", "baz"]
				]);
				
				// now complete the -->
				/*
				<!-- foo
				bar-->
				baz
				*/
				changeLine(view, ">", 1, 5, 5);
				// Note assertDisplayedLineScope() so we can verify redrawRange()
				assertLineScope(view, styler, 0, [
					[0, 4, "punctuation.definition.comment.mylang", "<!--"],
					[4, 8, "comment.block.mylang", " foo"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 3, "comment.block.mylang", "bar"],
					[3, 6, "punctuation.definition.comment.mylang", "-->"]
				]);
				assertLineScope(view, styler, 2, []);
			});
			//	// TODO: more damage/repair of nested regions
		}); // describe(damage/repair styling)

		describe("references", function() {
			it("end-to-begin backreferences", function() {
				var styler = makeStyler(view, mTestGrammars.BackrefTestGrammar);
				setLines(view, [
					"This is [b]ENTERPRISE[/b] quality",
					"[del]",
					"i'm line 2",
					"[/del]",
					"[one]aaa[two]bbb[/two]ccc[/one]", // make sure [/two] don't end [one]
					"[a.b]xx[/axb]xx[/a.b]" // make sure [/axb] doesn't end [a.b] (ie. captured period is ecaped)
				]);
				assertLineScope(view, styler, 0, [
					[8, 11, "punctuation.definition.tag.blah", "[b]"],
					[11, 21, "entity.name.tag.blah", "ENTERPRISE"],
					[21, 25, "punctuation.definition.tag.blah", "[/b]"]
				]);
				assertLineScope(view, styler, 1, [ [0, 5, "punctuation.definition.tag.blah", "[del]"] ]);
				assertLineScope(view, styler, 2, [ [0, 10, "entity.name.tag.blah", "i'm line 2"] ]);
				assertLineScope(view, styler, 3, [ [0, 6, "punctuation.definition.tag.blah", "[/del]"] ]);
				assertLineScope(view, styler, 4, [
					[0, 5, "punctuation.definition.tag.blah", "[one]"],
					[5, 25, "entity.name.tag.blah", "aaa[two]bbb[/two]ccc"],
					[25, 31, "punctuation.definition.tag.blah", "[/one]"]
				]);
				assertLineScope(view, styler, 5, [
					[0, 5, "punctuation.definition.tag.blah", "[a.b]"],
					[5, 15, "entity.name.tag.blah", "xx[/axb]xx"],
					[15, 21, "punctuation.definition.tag.blah", "[/a.b]"]
				]);
			});
			it("recursive includes", function() {
				var styler = makeStyler(view, mTestGrammars.RecursiveIncludeGrammar);
				setLines(view, [
					'aa"foo"bb',
					"[]",
					'["aa"]',
					'["aa", "bb"]',
					"[[], []]"
				]);
				assertLineScope(view, styler, 0, [
					[2, 3, "punctuation.definition.string.delimiter", "\""],
					[3, 6, "string.quoted.double", "foo"],
					[6, 7, "punctuation.definition.string.delimiter", "\""]
				]);
				assertLineScope(view, styler, 1, [
					[0, 1, "punctuation.definition.array.begin", "["],
					[1, 2, "punctuation.definition.array.end", "]"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 1, "punctuation.definition.array.begin", "["],
					[1, 2, "punctuation.definition.string.delimiter", "\""],
					[2, 4, "string.quoted.double", "aa"],
					[4, 5, "punctuation.definition.string.delimiter", "\""],
					[5, 6, "punctuation.definition.array.end", "]"]
				]);
				assertLineScope(view, styler, 3, [
					[0, 1, "punctuation.definition.array.begin", "["],
					[1, 2, "punctuation.definition.string.delimiter", "\""],
					[2, 4, "string.quoted.double", "aa"],
					[4, 5, "punctuation.definition.string.delimiter", "\""],
					[5, 6, "punctuation.array.separator", ","],
					[7, 8, "punctuation.definition.string.delimiter", "\""],
					[8, 10, "string.quoted.double", "bb"],
					[10, 11, "punctuation.definition.string.delimiter", "\""],
					[11, 12, "punctuation.definition.array.end", "]"]
				]);
				assertLineScope(view, styler, 4, [
					[0, 2, "punctuation.definition.array.begin", "[["],
					[2, 3, "punctuation.definition.array.end", "]"],
					[3, 4, "punctuation.array.separator", ","],
					[5, 6, "punctuation.definition.array.begin", "["],
					[6, 8, "punctuation.definition.array.end", "]]"]
				]);
			});
			it("external grammar reference", function() {
				var grammars = [mTestGrammars.ExternalGrammar1, mTestGrammars.ExternalGrammar2];
				var styler = makeStyler(view, grammars[0], grammars);
				setLines(view, [
					"<b><?php",
					"$fizz = 0",
					"?></b>"
				]);
				assertLineScope(view, styler, 0, [
					[0, 3, "tag.bold", "<b>"],
					[3, 8, "tag.fakephp", "<?php"]
				]);
				assertLineScope(view, styler, 1, [
					[0, 5, "variable", "$fizz"]
				]);
				assertLineScope(view, styler, 2, [
					[0, 2, "tag.fakephp", "?>"],
					[2, 6, "tag.bold", "</b>"]
				]);
			});
		}); // describe(references)
	});


	/***************************************************************************************
	 *
	 * Test helpers
	 *
	 **************************************************************************************/

	// TODO: run tests with both Windows and Linux delimiters since a few cases have failed with
	// one but not the other
	var NL = "\r\n";//new eclipse.TextModel().getLineDelimiter();
	
	/**
	 * @param {Function(textView)} testBody
	 * @param {Boolean} [doTearDown]
	 */
//	function makeTest(testBody, doTearDown) {
//		function createTextView() {
//			var options = {parent: "editorDiv", readonly: true};
//			//return new mTextView.TextView(options);
//			return new mMockTextView.MockTextView(options);
//		}
//		
//		/** Called after each test to remove view from DOM */
//		function tearDown(view) {
//			if (view) { view.destroy(); }
//		}
//		
//		doTearDown = typeof(doTearDown) === "undefined" ? true : doTearDown;
//		if (typeof(testBody) !== "function") { throw new Error("testBody must be a function"); }
//		return function() {
//			var view;
//			try {
//				view = createTextView();
//				testBody(view);
//			} finally {
//				if (doTearDown) {
//					tearDown(view);
//				}
//			}
//		};
//	}

	function createTextView() {
		var options = {parent: "editorDiv", readonly: true};
		//return new mTextView.TextView(options);
		return new mMockTextView.MockTextView(options);
	}

	function makeStyler(view, grammar, grammars) {
		return new mTextMateStyler.TextMateStyler(view, grammar, grammars);
	}
	
	/** Sets the given lines as the view text */
	function setLines(view, /**String[] or varargs*/ lines) {
		if (typeof(lines) === "string") {
			lines = Array.prototype.slice.call(arguments, 1);
		}
		view.setText(lines.join(NL));
	}
	
	/** Does a setText() on the range [col1,col2) in the given line. */
	function changeLine(view, text, lineIndex, col1, col2) {
		var lineStart = view.getModel().getLineStart(lineIndex);
		view.setText(text, lineStart+col1, lineStart+col2);
	}
	
	function arraysEqual(a, b, sameOrder) {
		if (a.length !== b.length) { return false; }
		for (var i=0; i < a.length; i++) {
			var item = a[i];
			var j = b.indexOf(item);
			if (j === -1 || (sameOrder && i !== j)) { return false; }
		}
		return true;
	}
	
	function scope2Classes(/**String*/ scope) {
		return scope.split(".").map(function(seg, i, segs) {
				return segs.slice(0, i+1).join("-");
			});
	}

	/** @returns true if style corresponds exactly to the given scope. */
	function styleMatchesScope(/**eclipse.Style*/ style, /**String*/ scope) {
		var classes = style.styleClass.split(/\s+/);
		return arraysEqual(classes, scope2Classes(scope)); 
	}
	
	/**
	 * Helper
	 * @param {orion.editor.StyleRange[]} styleRanges
	 * @param {Array} scopeRegions
	 */
	function assertStylesMatchScopes(view, styler, lineIndex, styleRanges, /**Array*/ scopeRegions) {
		var lineText = view.getModel().getLine(lineIndex);
		var lineStart = view.getModel().getLineStart(lineIndex);
		var lineEnd = view.getModel().getLineEnd(lineIndex);
		
		assert.ok(styleRanges !== null && styleRanges !== undefined, true, "lineStyleEvent.ranges exists");
		assert.equal(styleRanges.length, scopeRegions.length, "Line " + lineIndex + ": Number of styled regions matches");
		var ok, last;
		ok = scopeRegions.every(function(scopeRegion) {
				return styleRanges.some(function(styleRange) {
					var start = scopeRegion[0],
					    end = scopeRegion[1],
					    scope = scopeRegion[2],
					    text = scopeRegion[3];
					last = "start=" + start + " end=" + end + " scope=" + scope + (typeof(text) === "string" ? " text=" + text : "");
					return (styleRange.start === lineStart + start
						&& styleRange.end === lineStart + end
						&& styleMatchesScope(styleRange.style, scope)
						&& (typeof(text) !== "string" || text === view.getText(styleRange.start, styleRange.end)));
				});
			});
		
		var rangeStrs = styleRanges.map(function(styleRange) {
				var start = styleRange.start - lineStart,
				    end = styleRange.end - lineStart,
				    nicerScope = styleRange.style.styleClass.split(" ").pop().replace(/-/g, "."); // make easier to read
				return "{start:" + start + ", end:" + end + ", scope:" + nicerScope + "}";
			});
		assert.ok(ok, "No StyleRange in Line " + lineIndex + " matched expected {" + last + "}. StyleRanges were [" + rangeStrs.join(",") + "]");
	}
	
//	/**
//	 * Fails if the {@link orion.editor.StyleRange[]} ranges returned by running the styler on the line number
//	 * <tt>lineIndex</tt> do not exactly match the expected result given in <tt>scopeRegions</tt>.
//	 * @see assertDisplayedLineScope
//	 */
//	function assertLineScopeByOnLineStyle(view, styler, lineIndex, scopeRegions) {
//		var lineText = view.getModel().getLine(lineIndex);
//		var lineStart = view.getModel().getLineStart(lineIndex);
//		var lineEnd = view.getModel().getLineEnd(lineIndex);
//		var lineStyleEvent = {lineIndex: lineIndex, lineText: lineText, lineStart: lineStart, lineEnd: lineEnd};
//		view.onLineStyle(lineStyleEvent);
//		assertStylesMatchScopes(view, styler, lineIndex, lineStyleEvent.ranges, scopeRegions);
//	}
	
	/**
	 * Fails if the currently-displayed styles for the line at <tt>lineIndex</tt> don't match the expected <tt>scopeRegions</tt>.
	 * @param {Array} scopeRegions Each element of scopeRegions is an Array with the elements:
	 *   [0] {Number} start Line-relative index
	 *   [1] {Number} end Line-relative index
	 *   [2] {String} scope
	 *   [3] {String} text? Optional, if provided we assert that it equals the text in this region.
	 */
	function assertLineScope(view, styler, lineIndex, scopeRegions) {
		var lineStart = view.getModel().getLineStart(lineIndex);
		var lineStyle = view._getLineStyle(lineIndex);
		var styleRanges = (lineStyle && lineStyle.ranges) || [];
//		for (var i=0; i < ranges.length; i++) {
//			var child = ranges[i];
//			var ignoreChars = typeof child.ignoreChars === "number" ? child.ignoreChars : 0;
//			var length = child.textContent.length - ignoreChars;
//			var styleClass = child.className;
//			if (length > 0 /*omit ignored*/ && styleClass !== "" /*omit unstyled*/) {
//				styleRanges.push({
//					start: lineStart + charNum,
//					end: lineStart + charNum + length,
//					style: { styleClass: styleClass }
//				});
//			}
//			charNum += length;
//		}
		assertStylesMatchScopes(view, styler, lineIndex, styleRanges, scopeRegions);
	}
	
	function assertDoesntHaveProps(obj /*, propNames..*/) {
		var propNames = Array.prototype.slice.call(arguments, 1);
		for (var i=0; i < propNames.length; i++) {
			assert.ok(!obj.hasOwnProperty(propNames[i]));
		}
	}
	
	function assertHasProps(obj /*, propNames..*/) {
		var propNames = Array.prototype.slice.call(arguments, 1);
		for (var i=0; i < propNames.length; i++) {
			assert.ok(obj.hasOwnProperty(propNames[i]));
		}
	}
	
	function assertEmptyRegex(r) {
		// See ECMA-262 v5.1, "15.10.4.1 new RegExp(pattern, flags)"
		assert.ok(r.source === "" || r.source === "(?:)", "Regex is empty");
	}

});