diff options
Diffstat (limited to 'src/edit_eval.erl')
-rw-r--r-- | src/edit_eval.erl | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/src/edit_eval.erl b/src/edit_eval.erl new file mode 100644 index 0000000..24a102e --- /dev/null +++ b/src/edit_eval.erl @@ -0,0 +1,318 @@ +%%%---------------------------------------------------------------------- +%%% File : edit_eval.erl +%%% Author : Luke Gorrie <luke@bluetail.com> +%%% Purpose : Erlang code evaluation +%%% Created : 21 Jan 2001 by Luke Gorrie <luke@bluetail.com> +%%%---------------------------------------------------------------------- + +-module(edit_eval). +-author('luke@bluetail.com'). + +-include_lib("ermacs/include/edit.hrl"). + +-compile({parse_transform, edit_transform}). + +-import(edit_lib, [buffer/1]). + +-compile(export_all). +%%-export([Function/Arity, ...]). + +-define(history, erlang_interaction_history). + +eval_buffer(State) -> + B = buffer(State), + Text = edit_buf:get_text(B), + {ok, Scan, _} = erl_scan:string(Text), % safe ? + case erl_parse:parse_exprs(Scan) of + {ok, Parse} -> + case catch erl_eval:exprs(Parse, []) of + {value, V, _} -> + edit_util:status_msg(State, "~p", [V]); + {'EXIT', Reason} -> + edit_util:status_msg(State, "** exited: ~p **", [Reason]) + end; + {error, {_, erl_parse, Err}} -> + edit_util:status_msg(State, "~p", [Err]) + end. + +eval_string(State, String) -> + {ok, Scan, _} = erl_scan:string(String), % safe ? + eval_tokens(State, Scan, []). + +eval_tokens(Buf, Tokens, Bindings) -> + case erl_parse:parse_exprs(Tokens) of + {ok, Parse} -> + case erl_eval:exprs(Parse, Bindings, lf_handler(Buf)) of + {value, V, NewBs} -> + {ok, V, NewBs}; + Error -> + {error, Error} + end; + {error, {_, erl_parse, Err}} -> + {error, fmt("~s", [Err])} + end. + +lf_handler(Buf) -> + {eval, {?MODULE, local_func}, [Buf]}. + +fmt(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). + +-command({eval_expression, [{erl_expr, "Eval:"}]}). + +eval_expression(State, Expr) -> + Str = case eval_string(State, Expr) of + {ok, Val, NewBS} -> + fmt("~p", [Val]); + {error, Rsn} -> + fmt("~p", [Rsn]) + end, + edit_util:popup_message(State, '*Eval*', Str). + +region(Buffer) -> + {find_start(Buffer), edit_lib:end_of_line_pos(Buffer)}. + +%% ---------------------------------------------------------------------- +%% Interactive evaluation major mode +%% ---------------------------------------------------------------------- + +-define(keymap, erlang_interaction_map). +-define(output_mark, erlang_output). +-define(PROMPT, ">> "). + +erlang_interaction_mode(State) -> + case edit_keymap:keymap_exists(?keymap) of + false -> + init_map(); + true -> + ok + end, + Mode = #mode{name="Erlang Interaction", + id=erlang_interaction, + keymaps=[?keymap]}, + Buf = buffer(State), + edit_buf:set_mode(Buf, Mode), + edit_buf:add_mark(Buf, ?output_mark, 1, forward), + ok. + +init_map() -> + edit_keymap:new(?keymap), + edit_keymap:bind_each(?keymap, bindings()). + +bindings() -> + [{"C-m", {?MODULE, interactive_eval, []}}, + {"C-a", {?MODULE, beginning_of_prompt, []}}] + ++ edit_history:bindings(?history, {?MODULE, region}). + +interactive_eval(State) -> + P = find_start(buffer(State)), + interactive_eval1(State, P, P). + +interactive_eval1(State, CodeStart, SearchStart) -> + Buf = buffer(State), + Max = edit_buf:point_max(Buf), + Pred = fun(C) -> C == $\n end, + CodeEnd = edit_lib:find_char_forward(Buf, Pred, SearchStart, Max), + Region = edit_buf:get_region(Buf, CodeStart, CodeEnd), + Bindings = edit_var:lookup(erlang_interaction_bindings, []), + case erl_scan:tokens([], Region ++ "\n", 1) of + {done, {ok, Tokens, _}, _} -> % ok - enough + %% Move point to the end + edit_buf:move_mark(Buf, point, CodeEnd), + %% eval + edit_util:spawn_with([Buf], + fun() -> + eval_async(Buf, Tokens, Bindings) + end), + edit_history:add(?history, Region); + {more, _} when CodeEnd == Max -> + edit_buf:insert(Buf, "\n", edit_buf:mark_pos(Buf, point)); + {more, _} -> + interactive_eval1(State, CodeStart, CodeEnd + 1) + end. + +eval_async(Buf, Tokens, Bindings) -> + ensure_serv_started(Buf), + %% update output marker + edit_buf:insert(Buf, "\n", edit_buf:mark_pos(Buf, point)), + InsertionPoint = edit_buf:mark_pos(Buf, point), + edit_buf:move_mark(Buf, ?output_mark, InsertionPoint), + %% eval + Str = case serv_eval(Buf, Tokens, Bindings) of + {ok, Value, NewBs} -> + %% FIXME: bindings + edit_var:set(erlang_interaction_bindings, NewBs), + fmt("=> ~p\n", [Value]); + {error, {'EXIT', Rsn}} -> + fmt("** exited: ~p **\n", [Rsn]); + {error, Rsn} -> + fmt("** ~s **\n", [Rsn]) + end, + edit_buf:insert(Buf, Str, edit_buf:mark_pos(Buf, point)), + PromptPos = edit_buf:mark_pos(Buf, point), + edit_buf:insert(Buf, ?PROMPT, PromptPos), + edit_buf:move_mark(Buf, ?output_mark, PromptPos), + redraw(), + ok. + +redraw() -> + edit:invoke_async(edit_lib, nop, []). + +%% Go to the beginning of the line or directly after the prompt, +%% whichever is closer. +beginning_of_prompt(State) -> + Buf = buffer(State), + edit_buf:move_mark(Buf, point, beginning_of_prompt_pos(Buf)). + +beginning_of_prompt_pos(Buf) -> + Point = edit_buf:mark_pos(Buf, point), + BOL = edit_lib:beginning_of_line_pos(Buf), + case find_start(Buf) of + Pos when Pos > BOL, Pos =< Point -> + Pos; + _ -> + BOL + end. + +find_start(Buf) -> + case edit_lib:find_string_backward(Buf, ?PROMPT) of + not_found -> + 1; + X -> + edit_lib:min(X + length(?PROMPT), edit_buf:point_max(Buf)) + end. + +%% local_func(Function, Args, Bindings, Shell) -> +%% {value,Val,Bs} +%% Evaluate local functions, including shell commands. + +local_func(F, As0, Bs0, Buf) -> + {As,Bs} = erl_eval:expr_list(As0, Bs0, {eval,{?MODULE,local_func},[Buf]}), + case erlang:function_exported(user_default, F, length(As)) of + true -> + {value,apply(user_default, F, As),Bs}; + false -> + {value,apply(shell_default, F, As),Bs} + end; +local_func(F, As0, Bs0, Buf) -> + {As,Bs} = erl_eval:expr_list(As0, Bs0, {eval,{?MODULE,local_func},[Buf]}), + {value,apply(F, As),Bs}. + + +%% ---------------------------------------------------------------------- +%% Evaluation server API + +%% ok | already_started +ensure_serv_started(Buffer) -> + case whereis(serv_name(Buffer)) of + undefined -> + Pid = spawn(?MODULE, eval_serv_init, [Buffer]), + register(serv_name(Buffer), Pid), + ok; + Pid -> + already_started + end. + +serv_eval(Buffer, Tokens, Bindings) -> + Serv = whereis(serv_name(Buffer)), + erlang:monitor(process, Serv), + Serv ! {call, self(), {eval, Tokens, Bindings}}, + receive + {reply, Result} -> + Result; + {'DOWN', _, process, Serv, Rsn} -> + {error, {'EXIT', Rsn}} + end. + +%% serv_name(foo) -> 'foo:eval_serv' +serv_name(Buffer) -> list_to_atom(atom_to_list(Buffer) ++ ":eval_serv"). + +%% ---------------------------------------------------------------------- +%% Eval server loop + +eval_serv_init(Buffer) -> + erlang:monitor(process, whereis(Buffer)), + GL = gl_spawn_link(Buffer), + group_leader(GL, self()), + eval_serv_loop(Buffer, GL). + +eval_serv_loop(Buffer, GL) -> + receive + {call, From, {eval, Tokens, Bindings}} -> + GL ! {got_shared_lock, self()}, + Result = eval_tokens(Buffer, Tokens, Bindings), + GL ! {releasing_shared_lock, self()}, + receive gl_ack -> ok end, + From ! {reply, Result}, + eval_serv_loop(Buffer, GL); + {'DOWN', _, _, _, _} -> + exit(buffer_died) + end. + +%% ---------------------------------------------------------------------- +%% Group leader process for putting output into a buffer. +%% +%% The evaluation server sends us: +%% {got_shared_lock, HolderPid} +%% followed by: +%% {releasing_shared_lock, HolderPid} +%% Between these messages, we informally share the lock on the buffer. + +gl_spawn_link(Buffer) -> + spawn_link(?MODULE, gl_serv_init, [Buffer]). + +gl_serv_init(Buffer) -> + gl_serv_without_lock(Buffer). + +%% State: Nothing known about Buffer's lock +gl_serv_without_lock(Buffer) -> + ?debug("STATE: no lock~n", []), + receive + Msg = {io_request, From, ReplyAs, Req} -> + gl_serv_work(Buffer, Msg); + {got_shared_lock, Holder} -> + gl_serv_with_lock(Buffer) + end. + +%% State: Buffer is locked by eval_server, so we can use it. +gl_serv_with_lock(Buffer) -> + receive + Msg = {io_request, From, ReplyAs, Req} -> + do_gl_request(Buffer, Msg), + gl_serv_with_lock(Buffer); + {releasing_shared_lock, Holder} -> + Holder ! gl_ack, + gl_serv_without_lock(Buffer) + end. + +%% Action: Acquire the lock on Buffer, perform a request, and release. +gl_serv_work(Buffer, Req) -> + edit_buf:async_borrow(Buffer), + receive + {loan, Buffer} -> + do_gl_request(Buffer, Req), + edit_buf:async_return(Buffer), + gl_serv_without_lock(Buffer); + got_shared_lock -> + do_gl_request(Buffer, Req), + gl_serv_with_lock(Buffer) + end. + +%% Perform an I/O request by writing the result into the buffer. +do_gl_request(Buffer, {io_request, From, ReplyAs, {put_chars, M, F, A}}) -> + case catch apply(M, F, A) of + {'EXIT', Reason} -> + exit(From, Reason); + IOList -> + do_gl_request(Buffer, + {io_request, From, ReplyAs, {put_chars, IOList}}) + end; +do_gl_request(Buffer, {io_request, From, ReplyAs, {put_chars, IOList}}) -> + From ! {io_reply, ReplyAs, ok}, + Bin = list_to_binary(IOList), + Pos = edit_buf:mark_pos(Buffer, ?output_mark), + edit_buf:insert(Buffer, Bin, Pos), + redraw(); +do_gl_request(Buffer, {io_request, From, ReplyAs, {get_until, _, _, _}}) -> + From ! {io_reply, ReplyAs, eof}. + + |