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