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.

619 lines
18 KiB

2 months ago
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/5/LICENSE
  3. /*jshint unused:true, eqnull:true, curly:true, bitwise:true */
  4. /*jshint undef:true, latedef:true, trailing:true */
  5. /*global CodeMirror:true */
  6. // erlang mode.
  7. // tokenizer -> token types -> CodeMirror styles
  8. // tokenizer maintains a parse stack
  9. // indenter uses the parse stack
  10. // TODO indenter:
  11. // bit syntax
  12. // old guard/bif/conversion clashes (e.g. "float/1")
  13. // type/spec/opaque
  14. (function(mod) {
  15. if (typeof exports == "object" && typeof module == "object") // CommonJS
  16. mod(require("../../lib/codemirror"));
  17. else if (typeof define == "function" && define.amd) // AMD
  18. define(["../../lib/codemirror"], mod);
  19. else // Plain browser env
  20. mod(CodeMirror);
  21. })(function(CodeMirror) {
  22. "use strict";
  23. CodeMirror.defineMIME("text/x-erlang", "erlang");
  24. CodeMirror.defineMode("erlang", function(cmCfg) {
  25. "use strict";
  26. /////////////////////////////////////////////////////////////////////////////
  27. // constants
  28. var typeWords = [
  29. "-type", "-spec", "-export_type", "-opaque"];
  30. var keywordWords = [
  31. "after","begin","catch","case","cond","end","fun","if",
  32. "let","of","query","receive","try","when"];
  33. var separatorRE = /[\->,;]/;
  34. var separatorWords = [
  35. "->",";",","];
  36. var operatorAtomWords = [
  37. "and","andalso","band","bnot","bor","bsl","bsr","bxor",
  38. "div","not","or","orelse","rem","xor"];
  39. var operatorSymbolRE = /[\+\-\*\/<>=\|:!]/;
  40. var operatorSymbolWords = [
  41. "=","+","-","*","/",">",">=","<","=<","=:=","==","=/=","/=","||","<-","!"];
  42. var openParenRE = /[<\(\[\{]/;
  43. var openParenWords = [
  44. "<<","(","[","{"];
  45. var closeParenRE = /[>\)\]\}]/;
  46. var closeParenWords = [
  47. "}","]",")",">>"];
  48. var guardWords = [
  49. "is_atom","is_binary","is_bitstring","is_boolean","is_float",
  50. "is_function","is_integer","is_list","is_number","is_pid",
  51. "is_port","is_record","is_reference","is_tuple",
  52. "atom","binary","bitstring","boolean","function","integer","list",
  53. "number","pid","port","record","reference","tuple"];
  54. var bifWords = [
  55. "abs","adler32","adler32_combine","alive","apply","atom_to_binary",
  56. "atom_to_list","binary_to_atom","binary_to_existing_atom",
  57. "binary_to_list","binary_to_term","bit_size","bitstring_to_list",
  58. "byte_size","check_process_code","contact_binary","crc32",
  59. "crc32_combine","date","decode_packet","delete_module",
  60. "disconnect_node","element","erase","exit","float","float_to_list",
  61. "garbage_collect","get","get_keys","group_leader","halt","hd",
  62. "integer_to_list","internal_bif","iolist_size","iolist_to_binary",
  63. "is_alive","is_atom","is_binary","is_bitstring","is_boolean",
  64. "is_float","is_function","is_integer","is_list","is_number","is_pid",
  65. "is_port","is_process_alive","is_record","is_reference","is_tuple",
  66. "length","link","list_to_atom","list_to_binary","list_to_bitstring",
  67. "list_to_existing_atom","list_to_float","list_to_integer",
  68. "list_to_pid","list_to_tuple","load_module","make_ref","module_loaded",
  69. "monitor_node","node","node_link","node_unlink","nodes","notalive",
  70. "now","open_port","pid_to_list","port_close","port_command",
  71. "port_connect","port_control","pre_loaded","process_flag",
  72. "process_info","processes","purge_module","put","register",
  73. "registered","round","self","setelement","size","spawn","spawn_link",
  74. "spawn_monitor","spawn_opt","split_binary","statistics",
  75. "term_to_binary","time","throw","tl","trunc","tuple_size",
  76. "tuple_to_list","unlink","unregister","whereis"];
  77. // upper case: [A-Z] [Ø-Þ] [À-Ö]
  78. // lower case: [a-z] [ß-ö] [ø-ÿ]
  79. var anumRE = /[\w@Ø-ÞÀ-Öß-öø-ÿ]/;
  80. var escapesRE =
  81. /[0-7]{1,3}|[bdefnrstv\\"']|\^[a-zA-Z]|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}/;
  82. /////////////////////////////////////////////////////////////////////////////
  83. // tokenizer
  84. function tokenizer(stream,state) {
  85. // in multi-line string
  86. if (state.in_string) {
  87. state.in_string = (!doubleQuote(stream));
  88. return rval(state,stream,"string");
  89. }
  90. // in multi-line atom
  91. if (state.in_atom) {
  92. state.in_atom = (!singleQuote(stream));
  93. return rval(state,stream,"atom");
  94. }
  95. // whitespace
  96. if (stream.eatSpace()) {
  97. return rval(state,stream,"whitespace");
  98. }
  99. // attributes and type specs
  100. if (!peekToken(state) &&
  101. stream.match(/-\s*[a-zß-öø-ÿ][\wØ-ÞÀ-Öß-öø-ÿ]*/)) {
  102. if (is_member(stream.current(),typeWords)) {
  103. return rval(state,stream,"type");
  104. }else{
  105. return rval(state,stream,"attribute");
  106. }
  107. }
  108. var ch = stream.next();
  109. // comment
  110. if (ch == '%') {
  111. stream.skipToEnd();
  112. return rval(state,stream,"comment");
  113. }
  114. // colon
  115. if (ch == ":") {
  116. return rval(state,stream,"colon");
  117. }
  118. // macro
  119. if (ch == '?') {
  120. stream.eatSpace();
  121. stream.eatWhile(anumRE);
  122. return rval(state,stream,"macro");
  123. }
  124. // record
  125. if (ch == "#") {
  126. stream.eatSpace();
  127. stream.eatWhile(anumRE);
  128. return rval(state,stream,"record");
  129. }
  130. // dollar escape
  131. if (ch == "$") {
  132. if (stream.next() == "\\" && !stream.match(escapesRE)) {
  133. return rval(state,stream,"error");
  134. }
  135. return rval(state,stream,"number");
  136. }
  137. // dot
  138. if (ch == ".") {
  139. return rval(state,stream,"dot");
  140. }
  141. // quoted atom
  142. if (ch == '\'') {
  143. if (!(state.in_atom = (!singleQuote(stream)))) {
  144. if (stream.match(/\s*\/\s*[0-9]/,false)) {
  145. stream.match(/\s*\/\s*[0-9]/,true);
  146. return rval(state,stream,"fun"); // 'f'/0 style fun
  147. }
  148. if (stream.match(/\s*\(/,false) || stream.match(/\s*:/,false)) {
  149. return rval(state,stream,"function");
  150. }
  151. }
  152. return rval(state,stream,"atom");
  153. }
  154. // string
  155. if (ch == '"') {
  156. state.in_string = (!doubleQuote(stream));
  157. return rval(state,stream,"string");
  158. }
  159. // variable
  160. if (/[A-Z_Ø-ÞÀ-Ö]/.test(ch)) {
  161. stream.eatWhile(anumRE);
  162. return rval(state,stream,"variable");
  163. }
  164. // atom/keyword/BIF/function
  165. if (/[a-z_ß-öø-ÿ]/.test(ch)) {
  166. stream.eatWhile(anumRE);
  167. if (stream.match(/\s*\/\s*[0-9]/,false)) {
  168. stream.match(/\s*\/\s*[0-9]/,true);
  169. return rval(state,stream,"fun"); // f/0 style fun
  170. }
  171. var w = stream.current();
  172. if (is_member(w,keywordWords)) {
  173. return rval(state,stream,"keyword");
  174. }else if (is_member(w,operatorAtomWords)) {
  175. return rval(state,stream,"operator");
  176. }else if (stream.match(/\s*\(/,false)) {
  177. // 'put' and 'erlang:put' are bifs, 'foo:put' is not
  178. if (is_member(w,bifWords) &&
  179. ((peekToken(state).token != ":") ||
  180. (peekToken(state,2).token == "erlang"))) {
  181. return rval(state,stream,"builtin");
  182. }else if (is_member(w,guardWords)) {
  183. return rval(state,stream,"guard");
  184. }else{
  185. return rval(state,stream,"function");
  186. }
  187. }else if (lookahead(stream) == ":") {
  188. if (w == "erlang") {
  189. return rval(state,stream,"builtin");
  190. } else {
  191. return rval(state,stream,"function");
  192. }
  193. }else if (is_member(w,["true","false"])) {
  194. return rval(state,stream,"boolean");
  195. }else{
  196. return rval(state,stream,"atom");
  197. }
  198. }
  199. // number
  200. var digitRE = /[0-9]/;
  201. var radixRE = /[0-9a-zA-Z]/; // 36#zZ style int
  202. if (digitRE.test(ch)) {
  203. stream.eatWhile(digitRE);
  204. if (stream.eat('#')) { // 36#aZ style integer
  205. if (!stream.eatWhile(radixRE)) {
  206. stream.backUp(1); //"36#" - syntax error
  207. }
  208. } else if (stream.eat('.')) { // float
  209. if (!stream.eatWhile(digitRE)) {
  210. stream.backUp(1); // "3." - probably end of function
  211. } else {
  212. if (stream.eat(/[eE]/)) { // float with exponent
  213. if (stream.eat(/[-+]/)) {
  214. if (!stream.eatWhile(digitRE)) {
  215. stream.backUp(2); // "2e-" - syntax error
  216. }
  217. } else {
  218. if (!stream.eatWhile(digitRE)) {
  219. stream.backUp(1); // "2e" - syntax error
  220. }
  221. }
  222. }
  223. }
  224. }
  225. return rval(state,stream,"number"); // normal integer
  226. }
  227. // open parens
  228. if (nongreedy(stream,openParenRE,openParenWords)) {
  229. return rval(state,stream,"open_paren");
  230. }
  231. // close parens
  232. if (nongreedy(stream,closeParenRE,closeParenWords)) {
  233. return rval(state,stream,"close_paren");
  234. }
  235. // separators
  236. if (greedy(stream,separatorRE,separatorWords)) {
  237. return rval(state,stream,"separator");
  238. }
  239. // operators
  240. if (greedy(stream,operatorSymbolRE,operatorSymbolWords)) {
  241. return rval(state,stream,"operator");
  242. }
  243. return rval(state,stream,null);
  244. }
  245. /////////////////////////////////////////////////////////////////////////////
  246. // utilities
  247. function nongreedy(stream,re,words) {
  248. if (stream.current().length == 1 && re.test(stream.current())) {
  249. stream.backUp(1);
  250. while (re.test(stream.peek())) {
  251. stream.next();
  252. if (is_member(stream.current(),words)) {
  253. return true;
  254. }
  255. }
  256. stream.backUp(stream.current().length-1);
  257. }
  258. return false;
  259. }
  260. function greedy(stream,re,words) {
  261. if (stream.current().length == 1 && re.test(stream.current())) {
  262. while (re.test(stream.peek())) {
  263. stream.next();
  264. }
  265. while (0 < stream.current().length) {
  266. if (is_member(stream.current(),words)) {
  267. return true;
  268. }else{
  269. stream.backUp(1);
  270. }
  271. }
  272. stream.next();
  273. }
  274. return false;
  275. }
  276. function doubleQuote(stream) {
  277. return quote(stream, '"', '\\');
  278. }
  279. function singleQuote(stream) {
  280. return quote(stream,'\'','\\');
  281. }
  282. function quote(stream,quoteChar,escapeChar) {
  283. while (!stream.eol()) {
  284. var ch = stream.next();
  285. if (ch == quoteChar) {
  286. return true;
  287. }else if (ch == escapeChar) {
  288. stream.next();
  289. }
  290. }
  291. return false;
  292. }
  293. function lookahead(stream) {
  294. var m = stream.match(/^\s*([^\s%])/, false)
  295. return m ? m[1] : "";
  296. }
  297. function is_member(element,list) {
  298. return (-1 < list.indexOf(element));
  299. }
  300. function rval(state,stream,type) {
  301. // parse stack
  302. pushToken(state,realToken(type,stream));
  303. // map erlang token type to CodeMirror style class
  304. // erlang -> CodeMirror tag
  305. switch (type) {
  306. case "atom": return "atom";
  307. case "attribute": return "attribute";
  308. case "boolean": return "atom";
  309. case "builtin": return "builtin";
  310. case "close_paren": return null;
  311. case "colon": return null;
  312. case "comment": return "comment";
  313. case "dot": return null;
  314. case "error": return "error";
  315. case "fun": return "meta";
  316. case "function": return "tag";
  317. case "guard": return "property";
  318. case "keyword": return "keyword";
  319. case "macro": return "variable-2";
  320. case "number": return "number";
  321. case "open_paren": return null;
  322. case "operator": return "operator";
  323. case "record": return "bracket";
  324. case "separator": return null;
  325. case "string": return "string";
  326. case "type": return "def";
  327. case "variable": return "variable";
  328. default: return null;
  329. }
  330. }
  331. function aToken(tok,col,ind,typ) {
  332. return {token: tok,
  333. column: col,
  334. indent: ind,
  335. type: typ};
  336. }
  337. function realToken(type,stream) {
  338. return aToken(stream.current(),
  339. stream.column(),
  340. stream.indentation(),
  341. type);
  342. }
  343. function fakeToken(type) {
  344. return aToken(type,0,0,type);
  345. }
  346. function peekToken(state,depth) {
  347. var len = state.tokenStack.length;
  348. var dep = (depth ? depth : 1);
  349. if (len < dep) {
  350. return false;
  351. }else{
  352. return state.tokenStack[len-dep];
  353. }
  354. }
  355. function pushToken(state,token) {
  356. if (!(token.type == "comment" || token.type == "whitespace")) {
  357. state.tokenStack = maybe_drop_pre(state.tokenStack,token);
  358. state.tokenStack = maybe_drop_post(state.tokenStack);
  359. }
  360. }
  361. function maybe_drop_pre(s,token) {
  362. var last = s.length-1;
  363. if (0 < last && s[last].type === "record" && token.type === "dot") {
  364. s.pop();
  365. }else if (0 < last && s[last].type === "group") {
  366. s.pop();
  367. s.push(token);
  368. }else{
  369. s.push(token);
  370. }
  371. return s;
  372. }
  373. function maybe_drop_post(s) {
  374. if (!s.length) return s
  375. var last = s.length-1;
  376. if (s[last].type === "dot") {
  377. return [];
  378. }
  379. if (last > 1 && s[last].type === "fun" && s[last-1].token === "fun") {
  380. return s.slice(0,last-1);
  381. }
  382. switch (s[last].token) {
  383. case "}": return d(s,{g:["{"]});
  384. case "]": return d(s,{i:["["]});
  385. case ")": return d(s,{i:["("]});
  386. case ">>": return d(s,{i:["<<"]});
  387. case "end": return d(s,{i:["begin","case","fun","if","receive","try"]});
  388. case ",": return d(s,{e:["begin","try","when","->",
  389. ",","(","[","{","<<"]});
  390. case "->": return d(s,{r:["when"],
  391. m:["try","if","case","receive"]});
  392. case ";": return d(s,{E:["case","fun","if","receive","try","when"]});
  393. case "catch":return d(s,{e:["try"]});
  394. case "of": return d(s,{e:["case"]});
  395. case "after":return d(s,{e:["receive","try"]});
  396. default: return s;
  397. }
  398. }
  399. function d(stack,tt) {
  400. // stack is a stack of Token objects.
  401. // tt is an object; {type:tokens}
  402. // type is a char, tokens is a list of token strings.
  403. // The function returns (possibly truncated) stack.
  404. // It will descend the stack, looking for a Token such that Token.token
  405. // is a member of tokens. If it does not find that, it will normally (but
  406. // see "E" below) return stack. If it does find a match, it will remove
  407. // all the Tokens between the top and the matched Token.
  408. // If type is "m", that is all it does.
  409. // If type is "i", it will also remove the matched Token and the top Token.
  410. // If type is "g", like "i", but add a fake "group" token at the top.
  411. // If type is "r", it will remove the matched Token, but not the top Token.
  412. // If type is "e", it will keep the matched Token but not the top Token.
  413. // If type is "E", it behaves as for type "e", except if there is no match,
  414. // in which case it will return an empty stack.
  415. for (var type in tt) {
  416. var len = stack.length-1;
  417. var tokens = tt[type];
  418. for (var i = len-1; -1 < i ; i--) {
  419. if (is_member(stack[i].token,tokens)) {
  420. var ss = stack.slice(0,i);
  421. switch (type) {
  422. case "m": return ss.concat(stack[i]).concat(stack[len]);
  423. case "r": return ss.concat(stack[len]);
  424. case "i": return ss;
  425. case "g": return ss.concat(fakeToken("group"));
  426. case "E": return ss.concat(stack[i]);
  427. case "e": return ss.concat(stack[i]);
  428. }
  429. }
  430. }
  431. }
  432. return (type == "E" ? [] : stack);
  433. }
  434. /////////////////////////////////////////////////////////////////////////////
  435. // indenter
  436. function indenter(state,textAfter) {
  437. var t;
  438. var unit = cmCfg.indentUnit;
  439. var wordAfter = wordafter(textAfter);
  440. var currT = peekToken(state,1);
  441. var prevT = peekToken(state,2);
  442. if (state.in_string || state.in_atom) {
  443. return CodeMirror.Pass;
  444. }else if (!prevT) {
  445. return 0;
  446. }else if (currT.token == "when") {
  447. return currT.column+unit;
  448. }else if (wordAfter === "when" && prevT.type === "function") {
  449. return prevT.indent+unit;
  450. }else if (wordAfter === "(" && currT.token === "fun") {
  451. return currT.column+3;
  452. }else if (wordAfter === "catch" && (t = getToken(state,["try"]))) {
  453. return t.column;
  454. }else if (is_member(wordAfter,["end","after","of"])) {
  455. t = getToken(state,["begin","case","fun","if","receive","try"]);
  456. return t ? t.column : CodeMirror.Pass;
  457. }else if (is_member(wordAfter,closeParenWords)) {
  458. t = getToken(state,openParenWords);
  459. return t ? t.column : CodeMirror.Pass;
  460. }else if (is_member(currT.token,[",","|","||"]) ||
  461. is_member(wordAfter,[",","|","||"])) {
  462. t = postcommaToken(state);
  463. return t ? t.column+t.token.length : unit;
  464. }else if (currT.token == "->") {
  465. if (is_member(prevT.token, ["receive","case","if","try"])) {
  466. return prevT.column+unit+unit;
  467. }else{
  468. return prevT.column+unit;
  469. }
  470. }else if (is_member(currT.token,openParenWords)) {
  471. return currT.column+currT.token.length;
  472. }else{
  473. t = defaultToken(state);
  474. return truthy(t) ? t.column+unit : 0;
  475. }
  476. }
  477. function wordafter(str) {
  478. var m = str.match(/,|[a-z]+|\}|\]|\)|>>|\|+|\(/);
  479. return truthy(m) && (m.index === 0) ? m[0] : "";
  480. }
  481. function postcommaToken(state) {
  482. var objs = state.tokenStack.slice(0,-1);
  483. var i = getTokenIndex(objs,"type",["open_paren"]);
  484. return truthy(objs[i]) ? objs[i] : false;
  485. }
  486. function defaultToken(state) {
  487. var objs = state.tokenStack;
  488. var stop = getTokenIndex(objs,"type",["open_paren","separator","keyword"]);
  489. var oper = getTokenIndex(objs,"type",["operator"]);
  490. if (truthy(stop) && truthy(oper) && stop < oper) {
  491. return objs[stop+1];
  492. } else if (truthy(stop)) {
  493. return objs[stop];
  494. } else {
  495. return false;
  496. }
  497. }
  498. function getToken(state,tokens) {
  499. var objs = state.tokenStack;
  500. var i = getTokenIndex(objs,"token",tokens);
  501. return truthy(objs[i]) ? objs[i] : false;
  502. }
  503. function getTokenIndex(objs,propname,propvals) {
  504. for (var i = objs.length-1; -1 < i ; i--) {
  505. if (is_member(objs[i][propname],propvals)) {
  506. return i;
  507. }
  508. }
  509. return false;
  510. }
  511. function truthy(x) {
  512. return (x !== false) && (x != null);
  513. }
  514. /////////////////////////////////////////////////////////////////////////////
  515. // this object defines the mode
  516. return {
  517. startState:
  518. function() {
  519. return {tokenStack: [],
  520. in_string: false,
  521. in_atom: false};
  522. },
  523. token:
  524. function(stream, state) {
  525. return tokenizer(stream, state);
  526. },
  527. indent:
  528. function(state, textAfter) {
  529. return indenter(state,textAfter);
  530. },
  531. lineComment: "%"
  532. };
  533. });
  534. });