add support for auto outdent in CSS, JS and HTML modes

This commit is contained in:
Fabian Jakobs 2010-04-27 12:20:00 +02:00
commit b1fcad961d
14 changed files with 199 additions and 80 deletions

View file

@ -52,6 +52,7 @@
<script src="../src/ace/mode/CssHighlightRules.js" type="text/javascript" charset="utf-8"></script>
<script src="../src/ace/mode/Xml.js" type="text/javascript" charset="utf-8"></script>
<script src="../src/ace/mode/XmlHighlightRules.js" type="text/javascript" charset="utf-8"></script>
<script src="../src/ace/mode/MatchingBraceOutdent.js" type="text/javascript" charset="utf-8"></script>
<script src="../src/ace/MEventEmitter.js" type="text/javascript" charset="utf-8"></script>
<script src="../src/ace/Selection.js" type="text/javascript" charset="utf-8"></script>
<script src="../src/ace/Document.js" type="text/javascript" charset="utf-8"></script>

View file

@ -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;

View file

@ -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);
};

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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) {

View file

@ -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());
},

View file

@ -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", " /*{ ", " "));
}
});

View file

@ -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", " "));
// }
});

View file

@ -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", " "));
}
});

View file

@ -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());
}
});