620 lines
18 KiB
JavaScript
Executable File
620 lines
18 KiB
JavaScript
Executable File
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
// Distributed under an MIT license: https://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 (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{
|
|
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) {
|
|
if (!s.length) return s
|
|
var last = s.length-1;
|
|
|
|
if (s[last].type === "dot") {
|
|
return [];
|
|
}
|
|
if (last > 1 && s[last].type === "fun" && s[last-1].token === "fun") {
|
|
return s.slice(0,last-1);
|
|
}
|
|
switch (s[last].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: "%"
|
|
};
|
|
});
|
|
|
|
});
|