mirror of https://github.com/gogits/gogs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
618 lines
18 KiB
618 lines
18 KiB
// 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 (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) { |
|
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: "%" |
|
}; |
|
}); |
|
|
|
});
|
|
|