|
|
// block; "begin", "case", "fun", "if", "receive", "try": closed by "end"
|
|
|
// block internal; "after", "catch", "of"
|
|
|
// guard; "when", closed by "->"
|
|
|
// "->" opens a clause, closed by ";" or "."
|
|
|
// "<<" opens a binary, closed by ">>"
|
|
|
// "," appears in arglists, lists, tuples and terminates lines of code
|
|
|
// "." resets indentation to 0
|
|
|
// obsolete; "cond", "let", "query"
|
|
|
|
|
|
CodeMirror.defineMIME("text/x-erlang", "erlang");
|
|
|
|
|
|
CodeMirror.defineMode("erlang", function(cmCfg, modeCfg) {
|
|
|
|
|
|
function rval(state,stream,type) {
|
|
|
// distinguish between "." as terminator and record field operator
|
|
|
if (type == "record") {
|
|
|
state.context = "record";
|
|
|
}else{
|
|
|
state.context = false;
|
|
|
}
|
|
|
|
|
|
// remember last significant bit on last line for indenting
|
|
|
if (type != "whitespace" && type != "comment") {
|
|
|
state.lastToken = stream.current();
|
|
|
}
|
|
|
// erlang -> CodeMirror tag
|
|
|
switch (type) {
|
|
|
case "atom": return "atom";
|
|
|
case "attribute": return "attribute";
|
|
|
case "builtin": return "builtin";
|
|
|
case "comment": return "comment";
|
|
|
case "fun": return "meta";
|
|
|
case "function": return "tag";
|
|
|
case "guard": return "property";
|
|
|
case "keyword": return "keyword";
|
|
|
case "macro": return "variable-2";
|
|
|
case "number": return "number";
|
|
|
case "operator": return "operator";
|
|
|
case "record": return "bracket";
|
|
|
case "string": return "string";
|
|
|
case "type": return "def";
|
|
|
case "variable": return "variable";
|
|
|
case "error": return "error";
|
|
|
case "separator": return null;
|
|
|
case "open_paren": return null;
|
|
|
case "close_paren": return null;
|
|
|
default: return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var typeWords = [
|
|
|
"-type", "-spec", "-export_type", "-opaque"];
|
|
|
|
|
|
var keywordWords = [
|
|
|
"after","begin","catch","case","cond","end","fun","if",
|
|
|
"let","of","query","receive","try","when"];
|
|
|
|
|
|
var separatorWords = [
|
|
|
"->",";",":",".",","];
|
|
|
|
|
|
var operatorWords = [
|
|
|
"and","andalso","band","bnot","bor","bsl","bsr","bxor",
|
|
|
"div","not","or","orelse","rem","xor"];
|
|
|
|
|
|
var symbolWords = [
|
|
|
"+","-","*","/",">",">=","<","=<","=:=","==","=/=","/=","||","<-"];
|
|
|
|
|
|
var openParenWords = [
|
|
|
"<<","(","[","{"];
|
|
|
|
|
|
var closeParenWords = [
|
|
|
"}","]",")",">>"];
|
|
|
|
|
|
var guardWords = [
|
|
|
"is_atom","is_binary","is_bitstring","is_boolean","is_float",
|
|
|
"is_function","is_integer","is_list","is_number","is_pid",
|
|
|
"is_port","is_record","is_reference","is_tuple",
|
|
|
"atom","binary","bitstring","boolean","function","integer","list",
|
|
|
"number","pid","port","record","reference","tuple"];
|
|
|
|
|
|
var bifWords = [
|
|
|
"abs","adler32","adler32_combine","alive","apply","atom_to_binary",
|
|
|
"atom_to_list","binary_to_atom","binary_to_existing_atom",
|
|
|
"binary_to_list","binary_to_term","bit_size","bitstring_to_list",
|
|
|
"byte_size","check_process_code","contact_binary","crc32",
|
|
|
"crc32_combine","date","decode_packet","delete_module",
|
|
|
"disconnect_node","element","erase","exit","float","float_to_list",
|
|
|
"garbage_collect","get","get_keys","group_leader","halt","hd",
|
|
|
"integer_to_list","internal_bif","iolist_size","iolist_to_binary",
|
|
|
"is_alive","is_atom","is_binary","is_bitstring","is_boolean",
|
|
|
"is_float","is_function","is_integer","is_list","is_number","is_pid",
|
|
|
"is_port","is_process_alive","is_record","is_reference","is_tuple",
|
|
|
"length","link","list_to_atom","list_to_binary","list_to_bitstring",
|
|
|
"list_to_existing_atom","list_to_float","list_to_integer",
|
|
|
"list_to_pid","list_to_tuple","load_module","make_ref","module_loaded",
|
|
|
"monitor_node","node","node_link","node_unlink","nodes","notalive",
|
|
|
"now","open_port","pid_to_list","port_close","port_command",
|
|
|
"port_connect","port_control","pre_loaded","process_flag",
|
|
|
"process_info","processes","purge_module","put","register",
|
|
|
"registered","round","self","setelement","size","spawn","spawn_link",
|
|
|
"spawn_monitor","spawn_opt","split_binary","statistics",
|
|
|
"term_to_binary","time","throw","tl","trunc","tuple_size",
|
|
|
"tuple_to_list","unlink","unregister","whereis"];
|
|
|
|
|
|
// ignored for indenting purposes
|
|
|
var ignoreWords = [
|
|
|
",", ":", "catch", "after", "of", "cond", "let", "query"];
|
|
|
|
|
|
|
|
|
var smallRE = /[a-z_]/;
|
|
|
var largeRE = /[A-Z_]/;
|
|
|
var digitRE = /[0-9]/;
|
|
|
var octitRE = /[0-7]/;
|
|
|
var anumRE = /[a-z_A-Z0-9]/;
|
|
|
var symbolRE = /[\+\-\*\/<>=\|:]/;
|
|
|
var openParenRE = /[<\(\[\{]/;
|
|
|
var closeParenRE = /[>\)\]\}]/;
|
|
|
var sepRE = /[\->\.,:;]/;
|
|
|
|
|
|
function isMember(element,list) {
|
|
|
return (-1 < list.indexOf(element));
|
|
|
}
|
|
|
|
|
|
function isPrev(stream,string) {
|
|
|
var start = stream.start;
|
|
|
var len = string.length;
|
|
|
if (len <= start) {
|
|
|
var word = stream.string.slice(start-len,start);
|
|
|
return word == string;
|
|
|
}else{
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function tokenize(stream, state) {
|
|
|
if (stream.eatSpace()) {
|
|
|
return rval(state,stream,"whitespace");
|
|
|
}
|
|
|
|
|
|
// attributes and type specs
|
|
|
if ((peekToken(state).token == "" || peekToken(state).token == ".") &&
|
|
|
stream.peek() == '-') {
|
|
|
stream.next();
|
|
|
if (stream.eat(smallRE) && stream.eatWhile(anumRE)) {
|
|
|
if (isMember(stream.current(),typeWords)) {
|
|
|
return rval(state,stream,"type");
|
|
|
}else{
|
|
|
return rval(state,stream,"attribute");
|
|
|
}
|
|
|
}
|
|
|
stream.backUp(1);
|
|
|
}
|
|
|
|
|
|
var ch = stream.next();
|
|
|
|
|
|
// comment
|
|
|
if (ch == '%') {
|
|
|
stream.skipToEnd();
|
|
|
return rval(state,stream,"comment");
|
|
|
}
|
|
|
|
|
|
// macro
|
|
|
if (ch == '?') {
|
|
|
stream.eatWhile(anumRE);
|
|
|
return rval(state,stream,"macro");
|
|
|
}
|
|
|
|
|
|
// record
|
|
|
if ( ch == "#") {
|
|
|
stream.eatWhile(anumRE);
|
|
|
return rval(state,stream,"record");
|
|
|
}
|
|
|
|
|
|
// char
|
|
|
if ( ch == "$") {
|
|
|
if (stream.next() == "\\") {
|
|
|
if (!stream.eatWhile(octitRE)) {
|
|
|
stream.next();
|
|
|
}
|
|
|
}
|
|
|
return rval(state,stream,"string");
|
|
|
}
|
|
|
|
|
|
// quoted atom
|
|
|
if (ch == '\'') {
|
|
|
if (singleQuote(stream)) {
|
|
|
return rval(state,stream,"atom");
|
|
|
}else{
|
|
|
return rval(state,stream,"error");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// string
|
|
|
if (ch == '"') {
|
|
|
if (doubleQuote(stream)) {
|
|
|
return rval(state,stream,"string");
|
|
|
}else{
|
|
|
return rval(state,stream,"error");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// variable
|
|
|
if (largeRE.test(ch)) {
|
|
|
stream.eatWhile(anumRE);
|
|
|
return rval(state,stream,"variable");
|
|
|
}
|
|
|
|
|
|
// atom/keyword/BIF/function
|
|
|
if (smallRE.test(ch)) {
|
|
|
stream.eatWhile(anumRE);
|
|
|
|
|
|
if (stream.peek() == "/") {
|
|
|
stream.next();
|
|
|
if (stream.eatWhile(digitRE)) {
|
|
|
return rval(state,stream,"fun"); // f/0 style fun
|
|
|
}else{
|
|
|
stream.backUp(1);
|
|
|
return rval(state,stream,"atom");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var w = stream.current();
|
|
|
|
|
|
if (isMember(w,keywordWords)) {
|
|
|
pushToken(state,stream);
|
|
|
return rval(state,stream,"keyword");
|
|
|
}
|
|
|
if (stream.peek() == "(") {
|
|
|
// 'put' and 'erlang:put' are bifs, 'foo:put' is not
|
|
|
if (isMember(w,bifWords) &&
|
|
|
(!isPrev(stream,":") || isPrev(stream,"erlang:"))) {
|
|
|
return rval(state,stream,"builtin");
|
|
|
}else{
|
|
|
return rval(state,stream,"function");
|
|
|
}
|
|
|
}
|
|
|
if (isMember(w,guardWords)) {
|
|
|
return rval(state,stream,"guard");
|
|
|
}
|
|
|
if (isMember(w,operatorWords)) {
|
|
|
return rval(state,stream,"operator");
|
|
|
}
|
|
|
if (stream.peek() == ":") {
|
|
|
if (w == "erlang") {
|
|
|
return rval(state,stream,"builtin");
|
|
|
} else {
|
|
|
return rval(state,stream,"function");
|
|
|
}
|
|
|
}
|
|
|
return rval(state,stream,"atom");
|
|
|
}
|
|
|
|
|
|
// number
|
|
|
if (digitRE.test(ch)) {
|
|
|
stream.eatWhile(digitRE);
|
|
|
if (stream.eat('#')) {
|
|
|
stream.eatWhile(digitRE); // 16#10 style integer
|
|
|
} else {
|
|
|
if (stream.eat('.')) { // float
|
|
|
stream.eatWhile(digitRE);
|
|
|
}
|
|
|
if (stream.eat(/[eE]/)) {
|
|
|
stream.eat(/[-+]/); // float with exponent
|
|
|
stream.eatWhile(digitRE);
|
|
|
}
|
|
|
}
|
|
|
return rval(state,stream,"number"); // normal integer
|
|
|
}
|
|
|
|
|
|
// open parens
|
|
|
if (nongreedy(stream,openParenRE,openParenWords)) {
|
|
|
pushToken(state,stream);
|
|
|
return rval(state,stream,"open_paren");
|
|
|
}
|
|
|
|
|
|
// close parens
|
|
|
if (nongreedy(stream,closeParenRE,closeParenWords)) {
|
|
|
pushToken(state,stream);
|
|
|
return rval(state,stream,"close_paren");
|
|
|
}
|
|
|
|
|
|
// separators
|
|
|
if (greedy(stream,sepRE,separatorWords)) {
|
|
|
// distinguish between "." as terminator and record field operator
|
|
|
if (state.context == false) {
|
|
|
pushToken(state,stream);
|
|
|
}
|
|
|
return rval(state,stream,"separator");
|
|
|
}
|
|
|
|
|
|
// operators
|
|
|
if (greedy(stream,symbolRE,symbolWords)) {
|
|
|
return rval(state,stream,"operator");
|
|
|
}
|
|
|
|
|
|
return rval(state,stream,null);
|
|
|
}
|
|
|
|
|
|
function nongreedy(stream,re,words) {
|
|
|
if (stream.current().length == 1 && re.test(stream.current())) {
|
|
|
stream.backUp(1);
|
|
|
while (re.test(stream.peek())) {
|
|
|
stream.next();
|
|
|
if (isMember(stream.current(),words)) {
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
stream.backUp(stream.current().length-1);
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
function greedy(stream,re,words) {
|
|
|
if (stream.current().length == 1 && re.test(stream.current())) {
|
|
|
while (re.test(stream.peek())) {
|
|
|
stream.next();
|
|
|
}
|
|
|
while (0 < stream.current().length) {
|
|
|
if (isMember(stream.current(),words)) {
|
|
|
return true;
|
|
|
}else{
|
|
|
stream.backUp(1);
|
|
|
}
|
|
|
}
|
|
|
stream.next();
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
function doubleQuote(stream) {
|
|
|
return quote(stream, '"', '\\');
|
|
|
}
|
|
|
|
|
|
function singleQuote(stream) {
|
|
|
return quote(stream,'\'','\\');
|
|
|
}
|
|
|
|
|
|
function quote(stream,quoteChar,escapeChar) {
|
|
|
while (!stream.eol()) {
|
|
|
var ch = stream.next();
|
|
|
if (ch == quoteChar) {
|
|
|
return true;
|
|
|
}else if (ch == escapeChar) {
|
|
|
stream.next();
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
function Token(stream) {
|
|
|
this.token = stream ? stream.current() : "";
|
|
|
this.column = stream ? stream.column() : 0;
|
|
|
this.indent = stream ? stream.indentation() : 0;
|
|
|
}
|
|
|
|
|
|
function myIndent(state,textAfter) {
|
|
|
var indent = cmCfg.indentUnit;
|
|
|
var outdentWords = ["after","catch"];
|
|
|
var token = (peekToken(state)).token;
|
|
|
var wordAfter = takewhile(textAfter,/[^a-z]/);
|
|
|
|
|
|
if (isMember(token,openParenWords)) {
|
|
|
return (peekToken(state)).column+token.length;
|
|
|
}else if (token == "." || token == ""){
|
|
|
return 0;
|
|
|
}else if (token == "->") {
|
|
|
if (wordAfter == "end") {
|
|
|
return peekToken(state,2).column;
|
|
|
}else if (peekToken(state,2).token == "fun") {
|
|
|
return peekToken(state,2).column+indent;
|
|
|
}else{
|
|
|
return (peekToken(state)).indent+indent;
|
|
|
}
|
|
|
}else if (isMember(wordAfter,outdentWords)) {
|
|
|
return (peekToken(state)).indent;
|
|
|
}else{
|
|
|
return (peekToken(state)).column+indent;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function takewhile(str,re) {
|
|
|
var m = str.match(re);
|
|
|
return m ? str.slice(0,m.index) : str;
|
|
|
}
|
|
|
|
|
|
function popToken(state) {
|
|
|
return state.tokenStack.pop();
|
|
|
}
|
|
|
|
|
|
function peekToken(state,depth) {
|
|
|
var len = state.tokenStack.length;
|
|
|
var dep = (depth ? depth : 1);
|
|
|
if (len < dep) {
|
|
|
return new Token;
|
|
|
}else{
|
|
|
return state.tokenStack[len-dep];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function pushToken(state,stream) {
|
|
|
var token = stream.current();
|
|
|
var prev_token = peekToken(state).token;
|
|
|
if (isMember(token,ignoreWords)) {
|
|
|
return false;
|
|
|
}else if (drop_both(prev_token,token)) {
|
|
|
popToken(state);
|
|
|
return false;
|
|
|
}else if (drop_first(prev_token,token)) {
|
|
|
popToken(state);
|
|
|
return pushToken(state,stream);
|
|
|
}else{
|
|
|
state.tokenStack.push(new Token(stream));
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function drop_first(open, close) {
|
|
|
switch (open+" "+close) {
|
|
|
case "when ->": return true;
|
|
|
case "-> end": return true;
|
|
|
case "-> .": return true;
|
|
|
case ". .": return true;
|
|
|
default: return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function drop_both(open, close) {
|
|
|
switch (open+" "+close) {
|
|
|
case "( )": return true;
|
|
|
case "[ ]": return true;
|
|
|
case "{ }": return true;
|
|
|
case "<< >>": return true;
|
|
|
case "begin end": return true;
|
|
|
case "case end": return true;
|
|
|
case "fun end": return true;
|
|
|
case "if end": return true;
|
|
|
case "receive end": return true;
|
|
|
case "try end": return true;
|
|
|
case "-> ;": return true;
|
|
|
default: return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
startState:
|
|
|
function() {
|
|
|
return {tokenStack: [],
|
|
|
context: false,
|
|
|
lastToken: null};
|
|
|
},
|
|
|
|
|
|
token:
|
|
|
function(stream, state) {
|
|
|
return tokenize(stream, state);
|
|
|
},
|
|
|
|
|
|
indent:
|
|
|
function(state, textAfter) {
|
|
|
// console.log(state.tokenStack);
|
|
|
return myIndent(state,textAfter);
|
|
|
}
|
|
|
};
|
|
|
});
|
|
|
|