diff options
Diffstat (limited to 'mods/src/em_erlang.erl')
-rw-r--r-- | mods/src/em_erlang.erl | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/mods/src/em_erlang.erl b/mods/src/em_erlang.erl new file mode 100644 index 0000000..dd13e9d --- /dev/null +++ b/mods/src/em_erlang.erl @@ -0,0 +1,501 @@ +%%%---------------------------------------------------------------------- +%%% File : em_erlang.erl +%%% Author : Luke Gorrie <luke@bluetail.com> +%%% Purpose : Erlang "Major Mode" +%%% Created : 10 Mar 2001 by Luke Gorrie <luke@bluetail.com> +%%%---------------------------------------------------------------------- + +-module(em_erlang). +-author('luke@bluetail.com'). + +-include_lib("ermacs/include/edit.hrl"). +-import(edit_lib, [buffer/1]). +-export([mod_init/0, erlang_mode/1, reindent_cmd/1]). + +-define(keymap, erlang_mode_map). + +%% Called by the editor when this module is load/require'd +%% +%% NB: may be called several times due to explicit "load" calls. +mod_init() -> + catch edit_keymap:delete(?keymap), + init_map(), + edit_keymap:global_set_key("C-x e", {?MODULE, erlang_mode, []}), + edit_var:add_to_list(auto_mode_alist, + {"\.erl$$", {em_erlang, erlang_mode}}), + ok. + +erlang_mode(State) -> + Mode = #mode{name="Erlang", + id=erlang, + keymaps=[?keymap]}, + Buf = buffer(State), + edit_buf:set_mode(Buf, Mode), + State. + +init_map() -> + edit_keymap:new(?keymap), + edit_keymap:bind_each(?keymap, bindings()). + +bindings() -> + [{"C-i", {?MODULE, reindent_cmd, []}} + ]. + +reindent_cmd(State) -> + case scan_fun(State) of + no_previous_line -> + State; + Scan -> + B = buffer(State), + Indent = max(0, + idt(Scan)), %% + indent_cur_line_adjust(B)), + reindent(B, edit_lib:beginning_of_line_pos(B), Indent) + end. + +indent_cur_line_adjust(B) -> + case cur_line_first_token(B) of + nomatch -> + 0; + {match, Tok} -> + adjust_value(Tok) + end. + +adjust_value('when') -> 2; +adjust_value(')') -> -1; +adjust_value('}') -> -1; +adjust_value(']') -> -1; +adjust_value('>>') -> -2; +adjust_value(_) -> 0. + +strip_scan(X) -> lists:flatten(strip_scan1(X)). + +strip_scan1([]) -> []; +strip_scan1([{lit,_}|T]) -> ["lit "|strip_scan1(T)]; +strip_scan1([{X,Y}|T]) -> [atom_to_list(X) ++ " "|strip_scan1(T)]. + +reindent(Buf, Pos, Lvl) -> + Pred = fun(C) -> (C /= $ ) and (C /= $\t) end, + End = max(Pos, edit_lib:find_char_forward(Buf, Pred, Pos, 1)), + edit_buf:replace(Buf, lists:duplicate(Lvl, $ ), Pos, End). + +max(X, Y) when X > Y -> X; +max(X, Y) -> Y. + +min(X, Y) when X < Y -> X; +min(X, Y) -> Y. + + +beginning_of_function(State) -> + B = buffer(State), + Pos = beginning_of_function_pos(B), + edit_buf:move_mark(B, point, Pos). + +beginning_of_function_pos(B) -> + Point = min(edit_buf:point_max(B) - 1, + max(1, edit_lib:beginning_of_line_pos(B) - 1)), + Cord = edit_buf:get_cord(B), + case cord_regexp:first_match("^[a-z'$-?]", Cord, Point, backward) of + nomatch -> + 1; + {match, Start, End} -> + Start + end. + +%%% ---------------------------------------------------------------------- +%%% Indentation +%%% ---------------------------------------------------------------------- + +%% Calculate the indentation level for the next line of a +%% function. `Scan' is the result of tokenising from the start of the +%% function. +idt(Scan) -> + idt(Scan, [], [], 0). + +%% Stack item +-record(si, + {id, % icr | '->' | open + tok, % token + col, % column number of `tok' + old_indent % indent level when `tok' was found + }). + +%% icr means "if/case/receive(/begin/fun)" - really anything that's +%% matched by an 'end' + +%% Variable names: C = Column, O = OldToken, S = Stack, I = IndentLevel + +%% if case receive begin +idt([{Icr, C} | T], O, S, I) + when Icr == 'if'; Icr == 'case'; Icr == 'receive'; Icr == 'begin'-> + CI = calc_clause_indent({Icr, C}, T, C), + idt(T, Icr, push_icr(Icr, C, I, S), CI); + +%% fun +idt([{'fun', C}, {'(', OpenC} | T], O, S, I) -> + Rest = [{'(', OpenC} | T], + CI = calc_clause_indent({'fun', C}, Rest, C), + idt(Rest, 'fun', push_icr('fun', C, I, S), CI); + +%% end +idt([{'end', C} | T], O, S, I) -> + case unwind_icr(S) of + nomatch -> + idt(T, 'end', [], 0); + {SI, S2} -> + idt(T, 'end', S2, SI#si.old_indent) + end; + +%% ( [ { << +idt([{Open, C} | T], O, S, I) + when Open == '('; Open == '['; Open == '{'; Open == '<<' -> + Width = length(atom_to_list(Open)), + idt(T, Open, push_open(Open, C, I, S), C+Width); + +%% ) ] } >> +idt([{Close, C} | T], O, S, I) + when Close == ')'; Close == ']'; Close == '}'; Close == '>>' -> + case unwind_close(matching(Close), S) of + nomatch -> + idt(T, Close, [], 0); + {SI, S2} -> + idt(T, Close, S2, SI#si.old_indent) + end; + +%% -> +idt([{'->', C} | T], O, S, I) -> + NewIndent = case unwind_icr(S) of + nomatch -> + 4; + {ICR_SI, ICR_S2} -> + ICR_SI#si.col + 8 + end, + case unwind_when(S) of + {SI, S2} -> + idt(T, '->', push_arrow(C, SI#si.old_indent, S2), NewIndent); + nomatch -> + idt(T, '->', push_arrow(C, I, S), NewIndent) + end; + +%% when +idt([{'when', C}, {Next, NextC} | T], Old, S, I) when Next /= '->' -> + idt(T, 'when', push_when(C, I, S), NextC); + + +%% ; +idt([{';', C} | T], O, S = [SI = #si{id='when'}|_], I) -> + idt(T, ';', S, I); + +idt([{',', C} | T], O, S = [SI = #si{id='when'}|_], I) -> + idt(T, ';', S, I); + +idt([{';', C} | T], O, S, I) -> + case unwind_arrow(S) of + nomatch -> + idt(T, ';', [], 0); + {SI, S2} -> + idt(T, ';', S2, SI#si.old_indent) + end; + +idt([{'when', C} | T], '\n', S, I) -> + idt(T, 'when', S, I+2); + +%% Period: end of function +idt([{'.', C} | T], O, S, I) -> + idt(T, start, [], 0); + +%% Skip blank lines +idt([{'\n', C}, {'\n', _} | T], O, S, I) -> + idt([{'\n', C} | T], O, S, I); + +idt([{'\n', C} | T], O, S = [#si{id=open}|_], I) -> + idt(T, '\n', S, I); + +%% End of line. The preceeding token indicates whether we need to be unwinding +idt([{'\n', C} | T], Old, S, I) +%%when member(Old, ?ICR_TOKENS) -> + when Old == '.'; Old == ';'; Old == 'of'; Old == 'begin'; + Old == 'receive'; Old == 'if'; Old == '->'; Old == ','; + Old == '|' -> + idt(T, '\n', S, I); + +idt([{'\n', C} | T], O, S = [#si{id=open, old_indent=OldIndent}|_], I) -> + idt(T, '\n', S, OldIndent); + +idt([{'\n', C} | T], O, S, I) -> + case unwind_icr(S) of + nomatch -> + idt(T, '\n', [], 0); + {SI, _S2} -> + idt(T, '\n', S, SI#si.col) + end; + +idt([{'||', C} | T], O, S, I) -> + idt(T, '||', S, C + 3); + +%% Quotes + +idt([{Quote, C} | T], O, S, I) when Quote == '\''; + Quote == '"' -> + match_quote(Quote, T, S, I); + +%% Boring stuff +idt([{Tok,_}|T], O, S, I) -> + idt(T, Tok, S, I); + +idt([], O, S, I) -> + I. + +push_icr(Atom, Col, Indent, Stack) -> + [#si{id=icr, tok=Atom, col=Col, old_indent=Indent} | Stack]. + +push_open(Atom, Col, Indent, Stack) -> + [#si{id=open, tok=Atom, col=Col, old_indent=Indent} | Stack]. + +push_arrow(Col, Indent, Stack) -> + [#si{id=arrow, tok='->', col=Col, old_indent=Indent} | Stack]. + +push_when(Col, Indent, Stack) -> + [#si{id='when', tok='when', col=Col, old_indent=Indent} | Stack]. + +%% Unwind the stack to find a match for a closing bracket. For a +%% syntactically correct function, the head of the stack should always +%% match, but otherwise I just keep unwinding to be "forgiving" - +%% i.e. just give the wrong answer instead of an error :-) + +unwind_close(Tok, [SI = #si{id=open, tok=Tok} | T]) -> + {SI, T}; +unwind_close(Tok, [H|T]) -> + unwind_close(Tok, T); +unwind_close(Tok, []) -> + nomatch. + +unwind_icr([SI = #si{id=icr} | T]) -> + {SI, T}; +unwind_icr([H|T]) -> + unwind_icr(T); +unwind_icr([]) -> + nomatch. + +%% Returns: {SI, ICR_SI, Stack} +unwind_arrow([SI = #si{id=arrow} | T]) -> + {SI, T}; +unwind_arrow([H|T]) -> + unwind_arrow(T); +unwind_arrow([]) -> + nomatch. + +unwind_when([SI = #si{id='when'}|T]) -> + {SI, T}; +unwind_when([H|T]) -> + unwind_when(T); +unwind_when([]) -> + nomatch. + +match_quote(Quote, [{'\\', _}, _ | T], S, I) -> + %% Something is being escaped, hop over it + match_quote(Quote, T, S, I); +match_quote(Quote, [Tok = {Quote, _} | T], S, I) -> + idt(T, Quote, S, I); +match_quote(Quote, [Tok|T], S, I) -> + match_quote(Quote, T, S, I); +match_quote(Quote, [], S, I) -> + %% Out of tokens while inside a quote + 0. + +calc_clause_indent({IRF, X}, [{Next, Y}|T], I) + when IRF == 'if'; IRF == 'receive'; IRF == 'fun' -> + if + Next == '\n' -> + I + 4; + true -> + Y + end; +calc_clause_indent(_, _, I) -> + I + 4. + +matching(')') -> '('; +matching(']') -> '['; +matching('}') -> '{'; +matching('>>') -> '<<'. + +%%% ---------------------------------------------------------------------- +%%% Scanner +%%% ---------------------------------------------------------------------- + +% make_erlang_scanner() -> +% em_scan:make_scanner(em_erlang_scan:yystate(), +% {em_erlang_scan, yystate}, +% {em_erlang_scan, yyaction}). + +% scan_fun(State) -> +% B = buffer(State), +% RStart = beginning_of_function_pos(B), +% REnd = min(edit_lib:beginning_of_line_pos(B), +% edit_buf:point_max(B) - 1), +% Region = if +% REnd > RStart -> +% edit_buf:get_region(B, RStart, REnd); +% true -> +% "" +% end, +% scan(cord:new(Region)). + +% scan(Cord) -> +% Scanner = make_erlang_scanner(), +% Walker = cord:walker(Cord), +% case em_scan:edit_scan(Scanner, Walker) of +% {error, Rsn} -> +% {error, Rsn}; +% {ok, Toks} -> +% %% This module expects {Class, Column} +% [{Type, Col} || {Type, Col, Line} <- Toks] +% end. + +%%%%%%% OLD (current) SCANNER + +%% Token = {Class, Column} + +-define(MAX_TOKEN_LENGTH, 64). + +first_token(Str) -> + case scan(Str) of + [] -> + nomatch; + [{Tok,_}|_] -> + {match, Tok} + end. + +%% Returns: nomatch | {match, Token} +cur_line_first_token(Buf) -> + Max = edit_buf:point_max(Buf), + Start = edit_lib:beginning_of_line_pos(Buf), + End = min(Max - 1, + min(Start + ?MAX_TOKEN_LENGTH, + edit_lib:end_of_line_pos(Buf))), + if + End > Start -> + Str = edit_buf:get_region(Buf, Start, End), + first_token(Str); + true -> + nomatch + end. + +scan_fun(State) -> + B = buffer(State), + RStart = beginning_of_function_pos(B), + REnd = min(edit_lib:beginning_of_line_pos(B), + edit_buf:point_max(B) - 1), + Region = if + REnd > RStart -> + edit_buf:get_region(B, RStart, REnd); + true -> + "" + end, + scan(Region). + +strip(X) -> [element(1, E) || E <- X]. + +scan(Str) -> + S = merge_literals(scan1(Str, true, 0)), + S. + +merge_literals([]) -> []; +merge_literals([{lit,P},{lit,_}|T]) -> merge_literals([{lit,P}|T]); +merge_literals([H|T]) -> [H|merge_literals(T)]. + +scan1([], _, P) -> + []; +scan1([$%|T], _, P) -> + skip_comments(T, P); +scan1([$$,_|T], _, P) -> + [{lit,P}|scan1(T, true, P+2)]; +scan1([$.,X|T], _, P) -> + case lists:member(X, ws()) of + true -> + [{'.',P} | scan1([X|T], true, P)]; + false -> + [{lit,P}|scan1([X|T], true, P)] + end; +scan1(Str, Fresh, P) -> + case match(Str, Fresh) of + nomatch when hd(Str) /= $ , hd(Str) /= $\t -> + [{lit,P}|scan1(tl(Str), not symchar(hd(Str)), P+1)]; + nomatch -> + scan1(tl(Str), not symchar(hd(Str)), P+1); + {match, "\n"} -> + [{'\n',P} | + scan1(tl(Str), true, 0)]; + {match, M} -> + Len = length(M), + Rest = lists:nthtail(Len, Str), + [{list_to_atom(M), P} | + scan1(Rest, true, P+Len)] + end. + +match(Str, true) -> + case match_alone(Str) of + nomatch -> + match(Str, false); + X -> + X + end; +match(Str, false) -> + match_anywhere(Str). + +match_alone(Str) -> + match_alone(Str, alone()). + +match_alone(Str, [Str|_]) -> + {match, Str}; +match_alone(Str, [X|T]) -> + case lists:prefix(X, Str) of + true -> + case not symchar(lists:nth(length(X)+1, Str)) of + true -> + {match, X}; + false -> + match_alone(Str, T) + end; + false -> + match_alone(Str, T) + end; +match_alone(Str, [_|T]) -> + match_alone(Str, T); +match_alone(Str, []) -> + nomatch. + +match_anywhere(Str) -> + match_anywhere(Str, anywhere()). + +match_anywhere(Str, [X|T]) -> + case lists:prefix(X, Str) of + true -> {match, X}; + false -> match_anywhere(Str, T) + end; +match_anywhere(Str, []) -> + nomatch. + +skip_comments([$\n|T], P) -> + scan1(T, true, P); +skip_comments([_|T], P) -> + skip_comments(T, 0); +skip_comments([], P) -> + []. + +%% Tokens that can appear anywhere +anywhere() -> + ["{","}","<<",">>","(",")","[","]",";",".","->","||","|",";",",","\n", + "\"", "'", "\\"]. + +%% Tokens that must appear "alone" +alone() -> + ["begin", "end", "case", "if", "receive", "fun", "of", "when"]. + +ws() -> "\r\t\n ". + +symchar(C) when C >= $a, C =< $z -> true; +symchar(C) when C >= $A, C =< $Z -> true; +symchar(C) when C >= $0, C =< $9 -> true; +symchar($_) -> true; +symchar(_) -> false. + |