diff --git a/demo/editor.html b/demo/editor.html index 9ea770af..bb947174 100644 --- a/demo/editor.html +++ b/demo/editor.html @@ -52,6 +52,7 @@ + diff --git a/src/ace/Document.js b/src/ace/Document.js index 1efce9bd..144ef517 100644 --- a/src/ace/Document.js +++ b/src/ace/Document.js @@ -12,7 +12,11 @@ ace.Document = function(text, mode) { this.setMode(mode); } - this.insert({row: 0, column: 0}, text); + if (ace.isArray(text)) { + this.$insertLines(0, text); + } else { + this.$insert({row: 0, column: 0}, text); + } }; (function() { @@ -149,6 +153,10 @@ ace.Document = function(text, mode) { return this.lines[row] || ""; }; + this.getLines = function(firstRow, lastRow) { + return this.lines.slice(firstRow, lastRow+1); + }; + this.getLength = function() { return this.lines.length; }; @@ -167,10 +175,6 @@ ace.Document = function(text, mode) { } }; - this.getLines = function(firstRow, lastRow) { - return this.lines.slice(firstRow, lastRow+1); - }; - this.findMatchingBracket = function(position) { if (position.column == 0) return null; diff --git a/src/ace/Editor.js b/src/ace/Editor.js index 671ff918..e856688f 100644 --- a/src/ace/Editor.js +++ b/src/ace/Editor.js @@ -285,23 +285,25 @@ ace.Editor = function(renderer, doc) { return; var cursor = this.getCursorPosition(); - text = text.replace("\t", this.doc.getTabString()); if (!this.selection.isEmpty()) { - var end = this.doc.replace(this.getSelectionRange(), text); + var cursor = this.doc.remove(this.getSelectionRange()); this.clearSelection(); } - else { - var end = this.doc.insert(cursor, text); - } + + var lineState = this.bgTokenizer.getState(cursor.row-1); + var shouldOutdent = this.mode.checkOutdent(lineState, this.doc.getLine(cursor.row), text); + + var end = this.doc.insert(cursor, text); + + var row = cursor.row; + var line = this.doc.getLine(row); + var lineState = this.bgTokenizer.getState(row); // multi line insert - var row = cursor.row; if (row !== end.row) { - var line = this.doc.getLine(row); - var lineState = this.bgTokenizer.getState(row); - var indent = this.mode.getNextLineIndent(line, lineState, this.doc.getTabString()); + var indent = this.mode.getNextLineIndent(lineState, line, this.doc.getTabString()); if (indent) { var indentRange = { start: { @@ -312,6 +314,10 @@ ace.Editor = function(renderer, doc) { }; end.column += this.doc.indentRows(indentRange, indent); } + } else { + if (shouldOutdent) { + end.column += this.mode.autoOutdent(lineState, this.doc, row); + } } this.moveCursorToPosition(end); @@ -424,7 +430,7 @@ ace.Editor = function(renderer, doc) { } }; var state = this.bgTokenizer.getState(this.getCursorPosition().row); - var addedColumns = this.mode.toggleCommentLines(this.doc, range, state); + var addedColumns = this.mode.toggleCommentLines(state, this.doc, range); this.selection.shiftSelection(addedColumns); }; diff --git a/src/ace/ace.js b/src/ace/ace.js index f05f9416..214ea027 100644 --- a/src/ace/ace.js +++ b/src/ace/ace.js @@ -171,6 +171,10 @@ else { }; } +ace.isArray = function(value) { + return Object.prototype.toString.call(value) == "[object Array]"; +}; + ace.bind = function(fcn, context) { return function() { return fcn.apply(context, arguments); diff --git a/src/ace/mode/Css.js b/src/ace/mode/Css.js index d07720ee..886f10e2 100644 --- a/src/ace/mode/Css.js +++ b/src/ace/mode/Css.js @@ -2,12 +2,13 @@ ace.provide("ace.mode.Css"); ace.mode.Css = function() { this.$tokenizer = new ace.Tokenizer(new ace.mode.CssHighlightRules().getRules()); + this.$outdent = new ace.mode.MatchingBraceOutdent(); }; ace.inherits(ace.mode.Css, ace.mode.Text); (function() { - this.getNextLineIndent = function(line, state, tab) { + this.getNextLineIndent = function(state, line, tab) { var indent = this.$getIndent(line); // ignore braces in comments @@ -24,4 +25,12 @@ ace.inherits(ace.mode.Css, ace.mode.Text); return indent; }; + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + return this.$outdent.autoOutdent(doc, row); + }; + }).call(ace.mode.Css.prototype); \ No newline at end of file diff --git a/src/ace/mode/Html.js b/src/ace/mode/Html.js index a1fdc32d..cbac2e2f 100644 --- a/src/ace/mode/Html.js +++ b/src/ace/mode/Html.js @@ -10,32 +10,44 @@ ace.inherits(ace.mode.Html, ace.mode.Text); (function() { - this.toggleCommentLines = function(doc, range, state) { - var split = state.split("js-"); - if (!split[0] && split[1]) { - return this.$js.toggleCommentLines(doc, range, state); - } - - var split = state.split("css-"); - if (!split[0] && split[1]) { - return this.$css.toggleCommentLines(doc, range, state); - } - - return 0; + this.toggleCommentLines = function(state, doc, range) { + return this.$delegate("toggleCommentLines", arguments, function() { + return 0; + }); }; - this.getNextLineIndent = function(line, state, tab) { + this.getNextLineIndent = function(state, line, tab) { + return this.$delegate("getNextLineIndent", arguments, function() { + return ""; + }); + }; + + this.checkOutdent = function(state, line, input) { + return this.$delegate("checkOutdent", arguments, function() { + return false; + }); + }; + + this.autoOutdent = function(state, doc, row) { + return this.$delegate("autoOutdent", arguments); + }; + + this.$delegate = function(method, args, defaultHandler) { + var state = args[0]; var split = state.split("js-"); + if (!split[0] && split[1]) { - return this.$js.getNextLineIndent(line, split[1], tab); + args[0] = split[1]; + return this.$js[method].apply(this.$js, args); } var split = state.split("css-"); if (!split[0] && split[1]) { - return this.$css.getNextLineIndent(line, split[1], tab); + args[0] = split[1]; + return this.$css[method].apply(this.$css, args); } - return ""; + return defaultHandler ? defaultHandler() : undefined; }; }).call(ace.mode.Html.prototype); \ No newline at end of file diff --git a/src/ace/mode/JavaScript.js b/src/ace/mode/JavaScript.js index cb0cee7a..35329f03 100644 --- a/src/ace/mode/JavaScript.js +++ b/src/ace/mode/JavaScript.js @@ -2,12 +2,13 @@ ace.provide("ace.mode.JavaScript"); ace.mode.JavaScript = function() { this.$tokenizer = new ace.Tokenizer(new ace.mode.JavaScriptHighlightRules().getRules()); + this.$outdent = new ace.mode.MatchingBraceOutdent(); }; ace.inherits(ace.mode.JavaScript, ace.mode.Text); (function() { - this.toggleCommentLines = function(doc, range, state) { + this.toggleCommentLines = function(state, doc, range) { var addedRows = doc.outdentRows(range, "//"); if (addedRows == 0) { var addedRows = doc.indentRows(range, "//"); @@ -15,7 +16,7 @@ ace.inherits(ace.mode.JavaScript, ace.mode.Text); return addedRows; }; - this.getNextLineIndent = function(line, state, tab) { + this.getNextLineIndent = function(state, line, tab) { var indent = this.$getIndent(line); var tokenizedLine = this.$tokenizer.getLineTokens(line, state); @@ -47,4 +48,12 @@ ace.inherits(ace.mode.JavaScript, ace.mode.Text); return indent; }; + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + return this.$outdent.autoOutdent(doc, row); + }; + }).call(ace.mode.JavaScript.prototype); \ No newline at end of file diff --git a/src/ace/mode/MatchingBraceOutdent.js b/src/ace/mode/MatchingBraceOutdent.js new file mode 100644 index 00000000..5c6e5d0b --- /dev/null +++ b/src/ace/mode/MatchingBraceOutdent.js @@ -0,0 +1,51 @@ +ace.provide("ace.mode.MatchingBraceOutdent"); + +ace.mode.MatchingBraceOutdent = function() {}; + +(function() { + + this.checkOutdent = function(line, input) { + if (! /^\s+$/.test(line)) + return false; + + return /^\s*\}/.test(input); + }; + + this.autoOutdent = function(doc, row) { + var line = doc.getLine(row); + var match = line.match(/^(\s*\})/); + + if (!match) return 0; + + var column = match[1].length; + var openBracePos = doc.findMatchingBracket({row: row, column: column}); + + if (!openBracePos || openBracePos.row == row) return 0; + + var indent = this.$getIndent(doc.getLine(openBracePos.row)); + + var range = { + start: { + row: row, + column: 0 + }, + end: { + row: row, + column: column-1 + } + }; + doc.replace(range, indent); + + return indent.length - (column-1); + }; + + this.$getIndent = function(line) { + var match = line.match(/^(\s+)/); + if (match) { + return match[1]; + } + + return ""; + }; + +}).call(ace.mode.MatchingBraceOutdent.prototype); \ No newline at end of file diff --git a/src/ace/mode/Text.js b/src/ace/mode/Text.js index 013d24b5..e7ed42a0 100644 --- a/src/ace/mode/Text.js +++ b/src/ace/mode/Text.js @@ -10,14 +10,21 @@ ace.mode.Text = function() { return this.$tokenizer; }; - this.toggleCommentLines = function(doc, range, state) { + this.toggleCommentLines = function(state, doc, range) { return 0; }; - this.getNextLineIndent = function(line, state, tab) { + this.getNextLineIndent = function(state, line, tab) { return ""; }; + this.checkOutdent = function(state, line, input) { + return false; + }; + + this.autoOutdent = function(state, doc, row) { + }; + this.$getIndent = function(line) { var match = line.match(/^(\s+)/); if (match) { diff --git a/src/test/DocumentTest.js b/src/test/DocumentTest.js index ad66f640..d1002ede 100644 --- a/src/test/DocumentTest.js +++ b/src/test/DocumentTest.js @@ -1,7 +1,7 @@ var TextDocumentTest = new TestCase("TextDocumentTest", { "test: find matching opening bracket" : function() { - var doc = new ace.Document(["(()(", "())))"].join("\n")); + var doc = new ace.Document(["(()(", "())))"]); assertPosition(0, 1, doc.findMatchingBracket({row: 0, column: 3})); assertPosition(1, 0, doc.findMatchingBracket({row: 1, column: 2})); @@ -11,7 +11,7 @@ var TextDocumentTest = new TestCase("TextDocumentTest", { }, "test: find matching closing bracket" : function() { - var doc = new ace.Document(["(()(", "())))"].join("\n")); + var doc = new ace.Document(["(()(", "())))"]); assertPosition(1, 1, doc.findMatchingBracket({row: 1, column: 1})); assertPosition(1, 1, doc.findMatchingBracket({row: 1, column: 1})); @@ -22,7 +22,7 @@ var TextDocumentTest = new TestCase("TextDocumentTest", { }, "test: match different bracket types" : function() { - var doc = new ace.Document(["({[", ")]}"].join("\n")); + var doc = new ace.Document(["({[", ")]}"]); assertPosition(1, 0, doc.findMatchingBracket({row: 0, column: 1})); assertPosition(1, 2, doc.findMatchingBracket({row: 0, column: 2})); @@ -34,7 +34,7 @@ var TextDocumentTest = new TestCase("TextDocumentTest", { }, "test: move lines down" : function() { - var doc = new ace.Document(["1", "2", "3", "4"].join("\n")); + var doc = new ace.Document(["1", "2", "3", "4"]); doc.moveLinesDown(0, 1); assertEquals(["3", "1", "2", "4"].join("\n"), doc.toString()); @@ -50,7 +50,7 @@ var TextDocumentTest = new TestCase("TextDocumentTest", { }, "test: move lines up" : function() { - var doc = new ace.Document(["1", "2", "3", "4"].join("\n")); + var doc = new ace.Document(["1", "2", "3", "4"]); doc.moveLinesUp(2, 3); assertEquals(["1", "3", "4", "2"].join("\n"), doc.toString()); @@ -66,28 +66,28 @@ var TextDocumentTest = new TestCase("TextDocumentTest", { }, "test: duplicate lines" : function() { - var doc = new ace.Document(["1", "2", "3", "4"].join("\n")); + var doc = new ace.Document(["1", "2", "3", "4"]); doc.duplicateLines(1, 2); assertEquals(["1", "2", "3", "2", "3", "4"].join("\n"), doc.toString()); }, "test: duplicate last line" : function() { - var doc = new ace.Document(["1", "2", "3"].join("\n")); + var doc = new ace.Document(["1", "2", "3"]); doc.duplicateLines(2, 2); assertEquals(["1", "2", "3", "3"].join("\n"), doc.toString()); }, "test: duplicate first line" : function() { - var doc = new ace.Document(["1", "2", "3"].join("\n")); + var doc = new ace.Document(["1", "2", "3"]); doc.duplicateLines(0, 0); assertEquals(["1", "1", "2", "3"].join("\n"), doc.toString()); }, "test: should handle unix style new lines" : function() { - var doc = new ace.Document(["1", "2", "3"].join("\n")); + var doc = new ace.Document(["1", "2", "3"]); assertEquals(["1", "2", "3"].join("\n"), doc.toString()); }, diff --git a/src/test/mode/CssTest.js b/src/test/mode/CssTest.js index 56eec460..19e8d781 100644 --- a/src/test/mode/CssTest.js +++ b/src/test/mode/CssTest.js @@ -12,23 +12,23 @@ var CssTest = new TestCase("mode.CssTest", { end: {row: 1, column: 1} }; - var comment = this.mode.toggleCommentLines(doc, range, "start"); + var comment = this.mode.toggleCommentLines("start", doc, range); assertEquals([" abc", "cde", "fg"].join("\n"), doc.toString()); }, "test: lines should keep indentation" : function() { - assertEquals(" ", this.mode.getNextLineIndent(" abc", "start", " ")); - assertEquals("\t", this.mode.getNextLineIndent("\tabc", "start", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " abc", " ")); + assertEquals("\t", this.mode.getNextLineIndent("start", "\tabc", " ")); }, "test: new line after { should increase indent" : function() { - assertEquals(" ", this.mode.getNextLineIndent(" abc{", "start", " ")); - assertEquals("\t ", this.mode.getNextLineIndent("\tabc { ", "start", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " abc{", " ")); + assertEquals("\t ", this.mode.getNextLineIndent("start", "\tabc { ", " ")); }, "test: no indent increase after { in a comment" : function() { - assertEquals(" ", this.mode.getNextLineIndent(" /*{", "start", " ")); - assertEquals(" ", this.mode.getNextLineIndent(" /*{ ", "start", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " /*{", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " /*{ ", " ")); } }); \ No newline at end of file diff --git a/src/test/mode/JavaScriptTest.js b/src/test/mode/JavaScriptTest.js index e4eb1f94..fb5ad112 100644 --- a/src/test/mode/JavaScriptTest.js +++ b/src/test/mode/JavaScriptTest.js @@ -14,74 +14,90 @@ var JavaScriptTest = new TestCase("mode.JavaScriptTest", { }, "test: toggle comment lines should prepend '//' to each line" : function() { - var doc = new ace.Document([" abc", "cde", "fg"].join("\n")); + var doc = new ace.Document([" abc", "cde", "fg"]); var range = { start: {row: 0, column: 3}, end: {row: 1, column: 1} }; - var comment = this.mode.toggleCommentLines(doc, range, "start"); + var comment = this.mode.toggleCommentLines("start", doc, range); assertEquals(["// abc", "//cde", "fg"].join("\n"), doc.toString()); }, "test: toggle comment on commented lines should remove leading '//' chars" : function() { - var doc = new ace.Document(["// abc", "//cde", "fg"].join("\n")); + var doc = new ace.Document(["// abc", "//cde", "fg"]); var range = { start: {row: 0, column: 3}, end: {row: 1, column: 1} }; - var comment = this.mode.toggleCommentLines(doc, range, "start"); + var comment = this.mode.toggleCommentLines("start", doc, range); assertEquals([" abc", "cde", "fg"].join("\n"), doc.toString()); }, "test: toggle comment on multiple lines with one commented line prepend '//' to each line" : function() { - var doc = new ace.Document(["// abc", "//cde", "fg"].join("\n")); + var doc = new ace.Document(["// abc", "//cde", "fg"]); var range = { start: {row: 0, column: 3}, end: {row: 2, column: 1} }; - var comment = this.mode.toggleCommentLines(doc, range, "start"); + var comment = this.mode.toggleCommentLines("start", doc, range); assertEquals(["//// abc", "////cde", "//fg"].join("\n"), doc.toString()); }, "test: auto indent after opening brace" : function() { - assertEquals(" ", this.mode.getNextLineIndent("if () {", "start", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", "if () {", " ")); }, "test: no auto indent after opening brace in multi line comment" : function() { - assertEquals("", this.mode.getNextLineIndent("/*if () {", "start", " ")); - assertEquals(" ", this.mode.getNextLineIndent(" abcd", "comment", " ")); + assertEquals("", this.mode.getNextLineIndent("start", "/*if () {", " ")); + assertEquals(" ", this.mode.getNextLineIndent("comment", " abcd", " ")); }, "test: no auto indent after opening brace in single line comment" : function() { - assertEquals("", this.mode.getNextLineIndent("//if () {", "start", " ")); - assertEquals(" ", this.mode.getNextLineIndent(" //if () {", "start", " ")); + assertEquals("", this.mode.getNextLineIndent("start", "//if () {", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " //if () {", " ")); }, "test: no auto indent should add to existing indent" : function() { - assertEquals(" ", this.mode.getNextLineIndent(" if () {", "start", " ")); - assertEquals(" ", this.mode.getNextLineIndent(" cde", "start", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " if () {", " ")); + assertEquals(" ", this.mode.getNextLineIndent("start", " cde", " ")); }, "test: special indent in doc comments" : function() { - assertEquals(" * ", this.mode.getNextLineIndent("/**", "doc-start", " ")); - assertEquals(" * ", this.mode.getNextLineIndent(" /**", "doc-start", " ")); - assertEquals(" * ", this.mode.getNextLineIndent(" *", "doc-start", " ")); - assertEquals(" * ", this.mode.getNextLineIndent(" *", "doc-start", " ")); - assertEquals(" ", this.mode.getNextLineIndent(" abc", "doc-start", " ")); + assertEquals(" * ", this.mode.getNextLineIndent("doc-start", "/**", " ")); + assertEquals(" * ", this.mode.getNextLineIndent("doc-start", " /**", " ")); + assertEquals(" * ", this.mode.getNextLineIndent("doc-start", " *", " ")); + assertEquals(" * ", this.mode.getNextLineIndent("doc-start", " *", " ")); + assertEquals(" ", this.mode.getNextLineIndent("doc-start", " abc", " ")); }, "test: no indent after doc comments" : function() { - assertEquals("", this.mode.getNextLineIndent(" */", "doc-start", " ")); + assertEquals("", this.mode.getNextLineIndent("doc-start", " */", " ")); + }, + + "test: trigger outdent if line is space and new text starts with closing brace" : function() { + assertTrue(this.mode.checkOutdent("start", " ", " }")); + assertFalse(this.mode.checkOutdent("start", " a ", " }")); + assertFalse(this.mode.checkOutdent("start", "", "}")); + assertFalse(this.mode.checkOutdent("start", " ", "a }")); + assertFalse(this.mode.checkOutdent("start", " }", "}")); + }, + + "test: auto outdent should indent the line with the same indent as the line with the matching opening brace" : function() { + var doc = new ace.Document([" function foo() {", " bla", " }"]); + this.mode.autoOutdent("start", doc, 2); + assertEquals(" }", doc.getLine(2)); + }, + + "test: no auto outdent if no matching brace is found" : function() { + var doc = new ace.Document([" function foo()", " bla", " }"]); + this.mode.autoOutdent("start", doc, 2); + assertEquals(" }", doc.getLine(2)); } -// "test: outdent if first non WS character in line is a closing brace" : function() { -// assertEquals("", this.mode.getNextLineIndent(")", "start", " ")); -// assertEquals(" ", this.mode.getNextLineIndent(" )", "start", " ")); -// } }); \ No newline at end of file diff --git a/src/test/mode/TextTest.js b/src/test/mode/TextTest.js index 2d53207b..4150c796 100644 --- a/src/test/mode/TextTest.js +++ b/src/test/mode/TextTest.js @@ -5,19 +5,19 @@ var TextTest = new TestCase("mode.TextTest", { }, "test: toggle comment lines should not do anything" : function() { - var doc = new ace.Document([" abc", "cde", "fg"].join("\n")); + var doc = new ace.Document([" abc", "cde", "fg"]); var range = { start: {row: 0, column: 3}, end: {row: 1, column: 1} }; - var comment = this.mode.toggleCommentLines(doc, range, "start"); + var comment = this.mode.toggleCommentLines("start", doc, range); assertEquals([" abc", "cde", "fg"].join("\n"), doc.toString()); }, "text: lines should not be indented" : function() { - assertEquals("", this.mode.getNextLineIndent(" abc", " ")); + assertEquals("", this.mode.getNextLineIndent("start", " abc", " ")); } }); \ No newline at end of file diff --git a/src/test/mode/XmlTest.js b/src/test/mode/XmlTest.js index 0b928c98..df938a67 100644 --- a/src/test/mode/XmlTest.js +++ b/src/test/mode/XmlTest.js @@ -14,14 +14,14 @@ var XmlTest = new TestCase("mode.XmlTest", { }, "test: toggle comment lines should not do anything" : function() { - var doc = new ace.Document([" abc", "cde", "fg"].join("\n")); + var doc = new ace.Document([" abc", "cde", "fg"]); var range = { start: {row: 0, column: 3}, end: {row: 1, column: 1} }; - var comment = this.mode.toggleCommentLines(doc, range, "start"); + var comment = this.mode.toggleCommentLines("start", doc, range); assertEquals([" abc", "cde", "fg"].join("\n"), doc.toString()); } }); \ No newline at end of file