text: // CodeMirror, copyright (c) by Marijn Haverbeke and others
  // Distributed under an MIT license: http://codemirror.net/LICENSE
  
  /*jshint unused:true, eqnull:true, curly:true, bitwise:true */
  /*jshint undef:true, latedef:true, trailing:true */
  /*global CodeMirror:true */
  
  // erlang mode.
  // tokenizer -> token types -> CodeMirror styles
  // tokenizer maintains a parse stack
  // indenter uses the parse stack
  
  // TODO indenter:
  //   bit syntax
  //   old guard/bif/conversion clashes (e.g. "float/1")
  //   type/spec/opaque
  
  (function(mod) {
    if (typeof exports == "object" && typeof module == "object") // CommonJS
      mod(require("../../lib/codemirror"));
    else if (typeof define == "function" && define.amd) // AMD
      define(["../../lib/codemirror"], mod);
    else // Plain browser env
      mod(CodeMirror);
  })(function(CodeMirror) {
  "use strict";
  
  CodeMirror.defineMIME("text/x-erlang", "erlang");
  
  CodeMirror.defineMode("erlang", function(cmCfg) {
    "use strict";
  
  /////////////////////////////////////////////////////////////////////////////
  // constants
  
    var typeWords = [
      "-type", "-spec", "-export_type", "-opaque"];
  
    var keywordWords = [
      "after","begin","catch","case","cond","end","fun","if",
      "let","of","query","receive","try","when"];
  
    var separatorRE    = /[\->,;]/;
    var separatorWords = [
      "->",";",","];
  
    var operatorAtomWords = [
      "and","andalso","band","bnot","bor","bsl","bsr","bxor",
      "div","not","or","orelse","rem","xor"];
  
    var operatorSymbolRE    = /[\+\-\*\/<>=\|:!]/;
    var operatorSymbolWords = [
      "=","+","-","*","/",">",">=","<","=<","=:=","==","=/=","/=","||","<-","!"];
  
    var openParenRE    = /[<\(\[\{]/;
    var openParenWords = [
      "<<","(","[","{"];
  
    var closeParenRE    = /[>\)\]\}]/;
    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"];
  
  // upper case: [A-Z] [Ø-Þ] [À-Ö]
  // lower case: [a-z] [ß-ö] [ø-ÿ]
    var anumRE       = /[\w@Ø-ÞÀ-Öß-öø-ÿ]/;
    var escapesRE    =
      /[0-7]{1,3}|[bdefnrstv\\"']|\^[a-zA-Z]|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}/;
  
  /////////////////////////////////////////////////////////////////////////////
  // tokenizer
  
    function tokenizer(stream,state) {
      // in multi-line string
      if (state.in_string) {
        state.in_string = (!doubleQuote(stream));
        return rval(state,stream,"string");
      }
  
      // in multi-line atom
      if (state.in_atom) {
        state.in_atom = (!singleQuote(stream));
        return rval(state,stream,"atom");
      }
  
      // whitespace
      if (stream.eatSpace()) {
        return rval(state,stream,"whitespace");
      }
  
      // attributes and type specs
      if (!peekToken(state) &&
          stream.match(/-\s*[a-zß-öø-ÿ][\wØ-ÞÀ-Öß-öø-ÿ]*/)) {
        if (is_member(stream.current(),typeWords)) {
          return rval(state,stream,"type");
        }else{
          return rval(state,stream,"attribute");
        }
      }
  
      var ch = stream.next();
  
      // comment
      if (ch == '%') {
        stream.skipToEnd();
        return rval(state,stream,"comment");
      }
  
      // colon
      if (ch == ":") {
        return rval(state,stream,"colon");
      }
  
      // macro
      if (ch == '?') {
        stream.eatSpace();
        stream.eatWhile(anumRE);
        return rval(state,stream,"macro");
      }
  
      // record
      if (ch == "#") {
        stream.eatSpace();
        stream.eatWhile(anumRE);
        return rval(state,stream,"record");
      }
  
      // dollar escape
      if (ch == "$") {
        if (stream.next() == "\\" && !stream.match(escapesRE)) {
          return rval(state,stream,"error");
        }
        return rval(state,stream,"number");
      }
  
      // dot
      if (ch == ".") {
        return rval(state,stream,"dot");
      }
  
      // quoted atom
      if (ch == '\'') {
        if (!(state.in_atom = (!singleQuote(stream)))) {
          if (stream.match(/\s*\/\s*[0-9]/,false)) {
            stream.match(/\s*\/\s*[0-9]/,true);
            return rval(state,stream,"fun");      // 'f'/0 style fun
          }
          if (stream.match(/\s*\(/,false) || stream.match(/\s*:/,false)) {
            return rval(state,stream,"function");
          }
        }
        return rval(state,stream,"atom");
      }
  
      // string
      if (ch == '"') {
        state.in_string = (!doubleQuote(stream));
        return rval(state,stream,"string");
      }
  
      // variable
      if (/[A-Z_Ø-ÞÀ-Ö]/.test(ch)) {
        stream.eatWhile(anumRE);
        return rval(state,stream,"variable");
      }
  
      // atom/keyword/BIF/function
      if (/[a-z_ß-öø-ÿ]/.test(ch)) {
        stream.eatWhile(anumRE);
  
        if (stream.match(/\s*\/\s*[0-9]/,false)) {
          stream.match(/\s*\/\s*[0-9]/,true);
          return rval(state,stream,"fun");      // f/0 style fun
        }
  
        var w = stream.current();
  
        if (is_member(w,keywordWords)) {
          return rval(state,stream,"keyword");
        }else if (is_member(w,operatorAtomWords)) {
          return rval(state,stream,"operator");
        }else if (stream.match(/\s*\(/,false)) {
          // 'put' and 'erlang:put' are bifs, 'foo:put' is not
          if (is_member(w,bifWords) &&
              ((peekToken(state).token != ":") ||
               (peekToken(state,2).token == "erlang"))) {
            return rval(state,stream,"builtin");
          }else if (is_member(w,guardWords)) {
            return rval(state,stream,"guard");
          }else{
            return rval(state,stream,"function");
          }
        }else if (is_member(w,operatorAtomWords)) {
          return rval(state,stream,"operator");
        }else if (lookahead(stream) == ":") {
          if (w == "erlang") {
            return rval(state,stream,"builtin");
          } else {
            return rval(state,stream,"function");
          }
        }else if (is_member(w,["true","false"])) {
          return rval(state,stream,"boolean");
        }else if (is_member(w,["true","false"])) {
          return rval(state,stream,"boolean");
        }else{
          return rval(state,stream,"atom");
        }
      }
  
      // number
      var digitRE      = /[0-9]/;
      var radixRE      = /[0-9a-zA-Z]/;         // 36#zZ style int
      if (digitRE.test(ch)) {
        stream.eatWhile(digitRE);
        if (stream.eat('#')) {                // 36#aZ  style integer
          if (!stream.eatWhile(radixRE)) {
            stream.backUp(1);                 //"36#" - syntax error
          }
        } else if (stream.eat('.')) {       // float
          if (!stream.eatWhile(digitRE)) {
            stream.backUp(1);        // "3." - probably end of function
          } else {
            if (stream.eat(/[eE]/)) {        // float with exponent
              if (stream.eat(/[-+]/)) {
                if (!stream.eatWhile(digitRE)) {
                  stream.backUp(2);            // "2e-" - syntax error
                }
              } else {
                if (!stream.eatWhile(digitRE)) {
                  stream.backUp(1);            // "2e" - syntax error
                }
              }
            }
          }
        }
        return rval(state,stream,"number");   // normal integer
      }
  
      // open parens
      if (nongreedy(stream,openParenRE,openParenWords)) {
        return rval(state,stream,"open_paren");
      }
  
      // close parens
      if (nongreedy(stream,closeParenRE,closeParenWords)) {
        return rval(state,stream,"close_paren");
      }
  
      // separators
      if (greedy(stream,separatorRE,separatorWords)) {
        return rval(state,stream,"separator");
      }
  
      // operators
      if (greedy(stream,operatorSymbolRE,operatorSymbolWords)) {
        return rval(state,stream,"operator");
      }
  
      return rval(state,stream,null);
    }
  
  /////////////////////////////////////////////////////////////////////////////
  // utilities
    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 (is_member(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 (is_member(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 lookahead(stream) {
      var m = stream.match(/([\n\s]+|%[^\n]*\n)*(.)/,false);
      return m ? m.pop() : "";
    }
  
    function is_member(element,list) {
      return (-1 < list.indexOf(element));
    }
  
    function rval(state,stream,type) {
  
      // parse stack
      pushToken(state,realToken(type,stream));
  
      // map erlang token type to CodeMirror style class
      //     erlang             -> CodeMirror tag
      switch (type) {
        case "atom":        return "atom";
        case "attribute":   return "attribute";
        case "boolean":     return "atom";
        case "builtin":     return "builtin";
        case "close_paren": return null;
        case "colon":       return null;
        case "comment":     return "comment";
        case "dot":         return null;
        case "error":       return "error";
        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 "open_paren":  return null;
        case "operator":    return "operator";
        case "record":      return "bracket";
        case "separator":   return null;
        case "string":      return "string";
        case "type":        return "def";
        case "variable":    return "variable";
        default:            return null;
      }
    }
  
    function aToken(tok,col,ind,typ) {
      return {token:  tok,
              column: col,
              indent: ind,
              type:   typ};
    }
  
    function realToken(type,stream) {
      return aToken(stream.current(),
                   stream.column(),
                   stream.indentation(),
                   type);
    }
  
    function fakeToken(type) {
      return aToken(type,0,0,type);
    }
  
    function peekToken(state,depth) {
      var len = state.tokenStack.length;
      var dep = (depth ? depth : 1);
  
      if (len < dep) {
        return false;
      }else{
        return state.tokenStack[len-dep];
      }
    }
  
    function pushToken(state,token) {
  
      if (!(token.type == "comment" || token.type == "whitespace")) {
        state.tokenStack = maybe_drop_pre(state.tokenStack,token);
        state.tokenStack = maybe_drop_post(state.tokenStack);
      }
    }
  
    function maybe_drop_pre(s,token) {
      var last = s.length-1;
  
      if (0 < last && s[last].type === "record" && token.type === "dot") {
        s.pop();
      }else if (0 < last && s[last].type === "group") {
        s.pop();
        s.push(token);
      }else{
        s.push(token);
      }
      return s;
    }
  
    function maybe_drop_post(s) {
      var last = s.length-1;
  
      if (s[last].type === "dot") {
        return [];
      }
      if (s[last].type === "fun" && s[last-1].token === "fun") {
        return s.slice(0,last-1);
      }
      switch (s[s.length-1].token) {
        case "}":    return d(s,{g:["{"]});
        case "]":    return d(s,{i:["["]});
        case ")":    return d(s,{i:["("]});
        case ">>":   return d(s,{i:["<<"]});
        case "end":  return d(s,{i:["begin","case","fun","if","receive","try"]});
        case ",":    return d(s,{e:["begin","try","when","->",
                                    ",","(","[","{","<<"]});
        case "->":   return d(s,{r:["when"],
                                 m:["try","if","case","receive"]});
        case ";":    return d(s,{E:["case","fun","if","receive","try","when"]});
        case "catch":return d(s,{e:["try"]});
        case "of":   return d(s,{e:["case"]});
        case "after":return d(s,{e:["receive","try"]});
        default:     return s;
      }
    }
  
    function d(stack,tt) {
      // stack is a stack of Token objects.
      // tt is an object; {type:tokens}
      // type is a char, tokens is a list of token strings.
      // The function returns (possibly truncated) stack.
      // It will descend the stack, looking for a Token such that Token.token
      //  is a member of tokens. If it does not find that, it will normally (but
      //  see "E" below) return stack. If it does find a match, it will remove
      //  all the Tokens between the top and the matched Token.
      // If type is "m", that is all it does.
      // If type is "i", it will also remove the matched Token and the top Token.
      // If type is "g", like "i", but add a fake "group" token at the top.
      // If type is "r", it will remove the matched Token, but not the top Token.
      // If type is "e", it will keep the matched Token but not the top Token.
      // If type is "E", it behaves as for type "e", except if there is no match,
      //  in which case it will return an empty stack.
  
      for (var type in tt) {
        var len = stack.length-1;
        var tokens = tt[type];
        for (var i = len-1; -1 < i ; i--) {
          if (is_member(stack[i].token,tokens)) {
            var ss = stack.slice(0,i);
            switch (type) {
                case "m": return ss.concat(stack[i]).concat(stack[len]);
                case "r": return ss.concat(stack[len]);
                case "i": return ss;
                case "g": return ss.concat(fakeToken("group"));
                case "E": return ss.concat(stack[i]);
                case "e": return ss.concat(stack[i]);
            }
          }
        }
      }
      return (type == "E" ? [] : stack);
    }
  
  /////////////////////////////////////////////////////////////////////////////
  // indenter
  
    function indenter(state,textAfter) {
      var t;
      var unit = cmCfg.indentUnit;
      var wordAfter = wordafter(textAfter);
      var currT = peekToken(state,1);
      var prevT = peekToken(state,2);
  
      if (state.in_string || state.in_atom) {
        return CodeMirror.Pass;
      }else if (!prevT) {
        return 0;
      }else if (currT.token == "when") {
        return currT.column+unit;
      }else if (wordAfter === "when" && prevT.type === "function") {
        return prevT.indent+unit;
      }else if (wordAfter === "(" && currT.token === "fun") {
        return  currT.column+3;
      }else if (wordAfter === "catch" && (t = getToken(state,["try"]))) {
        return t.column;
      }else if (is_member(wordAfter,["end","after","of"])) {
        t = getToken(state,["begin","case","fun","if","receive","try"]);
        return t ? t.column : CodeMirror.Pass;
      }else if (is_member(wordAfter,closeParenWords)) {
        t = getToken(state,openParenWords);
        return t ? t.column : CodeMirror.Pass;
      }else if (is_member(currT.token,[",","|","||"]) ||
                is_member(wordAfter,[",","|","||"])) {
        t = postcommaToken(state);
        return t ? t.column+t.token.length : unit;
      }else if (currT.token == "->") {
        if (is_member(prevT.token, ["receive","case","if","try"])) {
          return prevT.column+unit+unit;
        }else{
          return prevT.column+unit;
        }
      }else if (is_member(currT.token,openParenWords)) {
        return currT.column+currT.token.length;
      }else{
        t = defaultToken(state);
        return truthy(t) ? t.column+unit : 0;
      }
    }
  
    function wordafter(str) {
      var m = str.match(/,|[a-z]+|\}|\]|\)|>>|\|+|\(/);
  
      return truthy(m) && (m.index === 0) ? m[0] : "";
    }
  
    function postcommaToken(state) {
      var objs = state.tokenStack.slice(0,-1);
      var i = getTokenIndex(objs,"type",["open_paren"]);
  
      return truthy(objs[i]) ? objs[i] : false;
    }
  
    function defaultToken(state) {
      var objs = state.tokenStack;
      var stop = getTokenIndex(objs,"type",["open_paren","separator","keyword"]);
      var oper = getTokenIndex(objs,"type",["operator"]);
  
      if (truthy(stop) && truthy(oper) && stop < oper) {
        return objs[stop+1];
      } else if (truthy(stop)) {
        return objs[stop];
      } else {
        return false;
      }
    }
  
    function getToken(state,tokens) {
      var objs = state.tokenStack;
      var i = getTokenIndex(objs,"token",tokens);
  
      return truthy(objs[i]) ? objs[i] : false;
    }
  
    function getTokenIndex(objs,propname,propvals) {
  
      for (var i = objs.length-1; -1 < i ; i--) {
        if (is_member(objs[i][propname],propvals)) {
          return i;
        }
      }
      return false;
    }
  
    function truthy(x) {
      return (x !== false) && (x != null);
    }
  
  /////////////////////////////////////////////////////////////////////////////
  // this object defines the mode
  
    return {
      startState:
        function() {
          return {tokenStack: [],
                  in_string:  false,
                  in_atom:    false};
        },
  
      token:
        function(stream, state) {
          return tokenizer(stream, state);
        },
  
      indent:
        function(state, textAfter) {
          return indenter(state,textAfter);
        },
  
      lineComment: "%"
    };
  });
  
  });
  