Created
December 20, 2012 13:59
-
-
Save ferd/4345454 to your computer and use it in GitHub Desktop.
Add search to Erlang shell's history Search mode can be entered by pressing ctrl-r. Enter terms and press
ctrl-r again to search backwards, or ctrl-s to then search forward (if
you terminal doesn't eat up that one). Press enter to execute the line,
or use tab, arrow keys, or other control sequences (^D, ^K, etc.) to
exit search mode while remain…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From d98e6c32d44e00f954c3912b08c6ebf48d55729c Mon Sep 17 00:00:00 2001 | |
From: Fred Hebert <[email protected]> | |
Date: Thu, 20 Dec 2012 08:41:33 -0500 | |
Subject: [PATCH] Add search to Erlang shell's history | |
Search mode can be entered by pressing ctrl-r. Enter terms and press | |
ctrl-r again to search backwards, or ctrl-s to then search forward (if | |
you terminal doesn't eat up that one). Press enter to execute the line, | |
or use tab, arrow keys, or other control sequences (^D, ^K, etc.) to | |
exit search mode while remaining on the last found line. | |
The search mode is a simpler version of the one available in bash or | |
zsh shells. | |
This adds a few modes to the shell (search, on top of none and meta) in | |
group.erl for history search, and a few more in edlin.erl to change the | |
meaning of control sequences while searching. | |
--- | |
lib/kernel/src/group.erl | 138 ++++++++++++++++++++++++++++++++++++++++++++-- | |
lib/stdlib/src/edlin.erl | 108 ++++++++++++++++++++++++++++++++++-- | |
2 files changed, 234 insertions(+), 12 deletions(-) | |
diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl | |
index f92c6f7..48d0ba8 100644 | |
--- a/lib/kernel/src/group.erl | |
+++ b/lib/kernel/src/group.erl | |
@@ -515,6 +515,27 @@ get_line1({undefined,{_A,Mode,Char},Cs,Cont,Rs}, Drv, Ls0, Encoding) | |
Drv, | |
Ls, Encoding) | |
end; | |
+%% ^R = backward search, ^S = forward search. | |
+%% Search is tricky to implement and does a lot of back-and-forth | |
+%% work with edlin.erl (from stdlib). Edlin takes care of writing | |
+%% and handling lines and escape characters to get out of search, | |
+%% whereas this module does the actual searching and appending to lines. | |
+%% Erlang's shell wasn't exactly meant to traverse the wall between | |
+%% line and line stack, so we at least restrict it by introducing | |
+%% new modes: search, search_quit, search_found. These are added to | |
+%% the regular ones (none, meta_left_sq_bracket) and handle special | |
+%% cases of history search. | |
+get_line1({undefined,{_A,Mode,Char},Cs,Cont,Rs}, Drv, Ls, Encoding) | |
+ when ((Mode =:= none) and (Char =:= $\^R)) -> | |
+ send_drv_reqs(Drv, Rs), | |
+ %% drop current line, move to search mode. We store the current | |
+ %% prompt ('N>') and substitute it with the search prompt. | |
+ send_drv_reqs(Drv, edlin:erase_line(Cont)), | |
+ put(search_quit_prompt, edlin:prompt(Cont)), | |
+ Pbs = prompt_bytes("(search)`': "), | |
+ {more_chars,Ncont,Nrs} = edlin:start(Pbs, search), | |
+ send_drv_reqs(Drv, Nrs), | |
+ get_line1(edlin:edit_line1(Cs, Ncont), Drv, Ls, Encoding); | |
get_line1({expand, Before, Cs0, Cont,Rs}, Drv, Ls0, Encoding) -> | |
send_drv_reqs(Drv, Rs), | |
ExpandFun = get(expand_fun), | |
@@ -535,8 +556,59 @@ get_line1({undefined,_Char,Cs,Cont,Rs}, Drv, Ls, Encoding) -> | |
send_drv_reqs(Drv, Rs), | |
send_drv(Drv, beep), | |
get_line1(edlin:edit_line(Cs, Cont), Drv, Ls, Encoding); | |
+%% The search item was found and accepted (new line entered on the exact | |
+%% result found) | |
+get_line1({_What,Cont={line,_Prompt,_Chars,search_found},Rs}, Drv, Ls0, Encoding) -> | |
+ Line = edlin:current_line(Cont), | |
+ %% this may create duplicate entries. | |
+ Ls = save_line(new_stack(get_lines(Ls0)), Line), | |
+ get_line1({done, Line, "", Rs}, Drv, Ls, Encoding); | |
+%% The search mode has been exited, but the user wants to remain in line | |
+%% editing mode wherever that was, but editing the search result. | |
+get_line1({What,Cont={line,_Prompt,_Chars,search_quit},Rs}, Drv, Ls, Encoding) -> | |
+ Line = edlin:current_chars(Cont), | |
+ %% Load back the old prompt with the correct line number. | |
+ case get(search_quit_prompt) of | |
+ undefined -> % should not happen. Fallback. | |
+ LsFallback = save_line(new_stack(get_lines(Ls)), Line), | |
+ get_line1({done, "\n", Line, Rs}, Drv, LsFallback, Encoding); | |
+ Prompt -> % redraw the line and keep going with the same stack position | |
+ NCont = {line,Prompt,{lists:reverse(Line),[]},none}, | |
+ send_drv_reqs(Drv, Rs), | |
+ send_drv_reqs(Drv, edlin:erase_line(Cont)), | |
+ send_drv_reqs(Drv, edlin:redraw_line(NCont)), | |
+ get_line1({What, NCont ,[]}, Drv, pad_stack(Ls), Encoding) | |
+ end; | |
+%% Search mode is entered. | |
+get_line1({What,{line,Prompt,{RevCmd0,_Aft},search},Rs}, | |
+ Drv, Ls0, Encoding) -> | |
+ send_drv_reqs(Drv, Rs), | |
+ %% Figure out search direction. ^S and ^R are returned through edlin | |
+ %% whenever we received a search while being already in search mode. | |
+ {Search, Ls1, RevCmd} = case RevCmd0 of | |
+ [$\^S|RevCmd1] -> | |
+ {fun search_down_stack/2, Ls0, RevCmd1}; | |
+ [$\^R|RevCmd1] -> | |
+ {fun search_up_stack/2, Ls0, RevCmd1}; | |
+ _ -> % new search, rewind stack for a proper search. | |
+ {fun search_up_stack/2, new_stack(get_lines(Ls0)), RevCmd0} | |
+ end, | |
+ Cmd = lists:reverse(RevCmd), | |
+ {Ls, NewStack} = case Search(Ls1, Cmd) of | |
+ {none, Ls2} -> | |
+ send_drv(Drv, beep), | |
+ {Ls2, {RevCmd, "': "}}; | |
+ {Line, Ls2} -> % found. Complete the output edlin couldn't have done. | |
+ send_drv_reqs(Drv, [{put_chars, Encoding, Line}]), | |
+ {Ls2, {RevCmd, "': "++Line}} | |
+ end, | |
+ Cont = {line,Prompt,NewStack,search}, | |
+ more_data(What, Cont, Drv, Ls, Encoding); | |
get_line1({What,Cont0,Rs}, Drv, Ls, Encoding) -> | |
send_drv_reqs(Drv, Rs), | |
+ more_data(What, Cont0, Drv, Ls, Encoding). | |
+ | |
+more_data(What, Cont0, Drv, Ls, Encoding) -> | |
receive | |
{Drv,{data,Cs}} -> | |
get_line1(edlin:edit_line(Cs, Cont0), Drv, Ls, Encoding); | |
@@ -557,7 +629,6 @@ get_line1({What,Cont0,Rs}, Drv, Ls, Encoding) -> | |
get_line1(edlin:edit_line([], Cont0), Drv, Ls, Encoding) | |
end. | |
- | |
get_line_echo_off(Chars, Pbs, Drv) -> | |
send_drv_reqs(Drv, [{put_chars, unicode,Pbs}]), | |
get_line_echo_off1(edit_line(Chars,[]), Drv). | |
@@ -632,12 +703,46 @@ save_line({stack, U, {}, []}, Line) -> | |
save_line({stack, U, _L, D}, Line) -> | |
{stack, U, Line, D}. | |
-get_lines({stack, U, {}, []}) -> | |
+get_lines(Ls) -> get_all_lines(Ls). | |
+%get_lines({stack, U, {}, []}) -> | |
+% U; | |
+%get_lines({stack, U, {}, D}) -> | |
+% tl(lists:reverse(D, U)); | |
+%get_lines({stack, U, L, D}) -> | |
+% get_lines({stack, U, {}, [L|D]}). | |
+ | |
+%% There's a funny behaviour whenever the line stack doesn't have a "\n" | |
+%% at its end -- get_lines() seemed to work on the assumption it *will* be | |
+%% there, but the manipulations done with search history do not require it. | |
+%% | |
+%% It is an assumption because the function was built with either the full | |
+%% stack being on the 'Up' side (we're on the new line) where it isn't | |
+%% stripped. The only other case when it isn't on the 'Up' side is when | |
+%% someone has used the up/down arrows (or ^P and ^N) to navigate lines, | |
+%% in which case, a line with only a \n is stored at the end of the stack | |
+%% (the \n is returned by edlin:current_line/1). | |
+%% | |
+%% get_all_lines works the same as get_lines, but only strips the trailing | |
+%% character if it's a linebreak. Otherwise it's kept the same. This is | |
+%% because traversing the stack due to search history will *not* insert | |
+%% said empty line in the stack at the same time as other commands do, | |
+%% and thus it should not always be stripped unless we know a new line | |
+%% is the last entry. | |
+get_all_lines({stack, U, {}, []}) -> | |
U; | |
-get_lines({stack, U, {}, D}) -> | |
- tl(lists:reverse(D, U)); | |
-get_lines({stack, U, L, D}) -> | |
- get_lines({stack, U, {}, [L|D]}). | |
+get_all_lines({stack, U, {}, D}) -> | |
+ case lists:reverse(D, U) of | |
+ ["\n"|Lines] -> Lines; | |
+ Lines -> Lines | |
+ end; | |
+get_all_lines({stack, U, L, D}) -> | |
+ get_all_lines({stack, U, {}, [L|D]}). | |
+ | |
+%% For the same reason as above, though, we need to expand the stack | |
+%% in some cases to make sure we play nice with up/down arrows. We need | |
+%% to insert newlines, but not always. | |
+pad_stack({stack, U, L, D}) -> | |
+ {stack, U, L, D++["\n"]}. | |
save_line_buffer("\n", Lines) -> | |
save_line_buffer(Lines); | |
@@ -649,6 +754,27 @@ save_line_buffer(Line, Lines) -> | |
save_line_buffer(Lines) -> | |
put(line_buffer, Lines). | |
+search_up_stack(Stack, Substr) -> | |
+ case up_stack(Stack) of | |
+ {none,NewStack} -> {none,NewStack}; | |
+ {L, NewStack} -> | |
+ case string:str(L, Substr) of | |
+ 0 -> search_up_stack(NewStack, Substr); | |
+ _ -> {string:strip(L,right,$\n), NewStack} | |
+ end | |
+ end. | |
+ | |
+search_down_stack(Stack, Substr) -> | |
+ case down_stack(Stack) of | |
+ {none,NewStack} -> {none,NewStack}; | |
+ {L, NewStack} -> | |
+ case string:str(L, Substr) of | |
+ 0 -> search_down_stack(NewStack, Substr); | |
+ _ -> {string:strip(L,right,$\n), NewStack} | |
+ end | |
+ end. | |
+ | |
+ | |
%% This is get_line without line editing (except for backspace) and | |
%% without echo. | |
get_password_line(Chars, Drv) -> | |
diff --git a/lib/stdlib/src/edlin.erl b/lib/stdlib/src/edlin.erl | |
index 026bd90..758262b 100644 | |
--- a/lib/stdlib/src/edlin.erl | |
+++ b/lib/stdlib/src/edlin.erl | |
@@ -21,10 +21,10 @@ | |
%% A simple Emacs-like line editor. | |
%% About Latin-1 characters: see the beginning of erl_scan.erl. | |
--export([init/0,start/1,edit_line/2,prefix_arg/1]). | |
+-export([init/0,start/1,start/2,edit_line/2,prefix_arg/1]). | |
-export([erase_line/1,erase_inp/1,redraw_line/1]). | |
-export([length_before/1,length_after/1,prompt/1]). | |
--export([current_line/1]). | |
+-export([current_line/1, current_chars/1]). | |
%%-export([expand/1]). | |
-export([edit_line1/2]). | |
@@ -53,7 +53,12 @@ init() -> | |
%% {undefined,Char,Rest,Cont,Requests} | |
start(Pbs) -> | |
- {more_chars,{line,Pbs,{[],[]},none},[{put_chars,unicode,Pbs}]}. | |
+ start(Pbs, none). | |
+ | |
+%% Only two modes used: 'none' and 'search'. Other modes can be | |
+%% handled inline through specific character handling. | |
+start(Pbs, Mode) -> | |
+ {more_chars,{line,Pbs,{[],[]},Mode},[{put_chars,unicode,Pbs}]}. | |
edit_line(Cs, {line,P,L,{blink,N}}) -> | |
edit(Cs, P, L, none, [{move_rel,N}]); | |
@@ -75,6 +80,10 @@ edit([C|Cs], P, {Bef,Aft}, Prefix, Rs0) -> | |
edit(Cs, P, {Bef,Aft}, meta, Rs0); | |
meta_left_sq_bracket -> | |
edit(Cs, P, {Bef,Aft}, meta_left_sq_bracket, Rs0); | |
+ search_meta -> | |
+ edit(Cs, P, {Bef,Aft}, search_meta, Rs0); | |
+ search_meta_left_sq_bracket -> | |
+ edit(Cs, P, {Bef,Aft}, search_meta_left_sq_bracket, Rs0); | |
ctlx -> | |
edit(Cs, P, {Bef,Aft}, ctlx, Rs0); | |
new_line -> | |
@@ -114,6 +123,8 @@ edit([C|Cs], P, {Bef,Aft}, Prefix, Rs0) -> | |
case do_op(Op, Bef, Aft, Rs0) of | |
{blink,N,Line,Rs} -> | |
edit(Cs, P, Line, {blink,N}, Rs); | |
+ {Line, Rs, Mode} -> % allow custom modes from do_op | |
+ edit(Cs, P, Line, Mode, Rs); | |
{Line,Rs} -> | |
edit(Cs, P, Line, none, Rs) | |
end | |
@@ -167,9 +178,15 @@ key_map($\^], none) -> auto_blink; | |
key_map($\^X, none) -> ctlx; | |
key_map($\^Y, none) -> yank; | |
key_map($\e, none) -> meta; | |
-key_map($), Prefix) when Prefix =/= meta -> {blink,$),$(}; | |
-key_map($}, Prefix) when Prefix =/= meta -> {blink,$},${}; | |
-key_map($], Prefix) when Prefix =/= meta -> {blink,$],$[}; | |
+key_map($), Prefix) when Prefix =/= meta, | |
+ Prefix =/= search, | |
+ Prefix =/= search_meta -> {blink,$),$(}; | |
+key_map($}, Prefix) when Prefix =/= meta, | |
+ Prefix =/= search, | |
+ Prefix =/= search_meta -> {blink,$},${}; | |
+key_map($], Prefix) when Prefix =/= meta, | |
+ Prefix =/= search, | |
+ Prefix =/= search_meta -> {blink,$],$[}; | |
key_map($B, meta) -> backward_word; | |
key_map($D, meta) -> kill_word; | |
key_map($F, meta) -> forward_word; | |
@@ -187,6 +204,31 @@ key_map($D, meta_left_sq_bracket) -> backward_char; | |
key_map($C, meta_left_sq_bracket) -> forward_char; | |
key_map(C, none) when C >= $\s -> | |
{insert,C}; | |
+%% for search, we need smarter line handling and so | |
+%% we cheat a bit on the dispatching, and allow to | |
+%% return a mode. | |
+key_map($\^H, search) -> {search, backward_delete_char}; | |
+key_map($\177, search) -> {search, backward_delete_char}; | |
+key_map($\^R, search) -> {search, skip_up}; | |
+key_map($\^S, search) -> {search, skip_down}; | |
+key_map($\n, search) -> {search, search_found}; | |
+key_map($\r, search) -> {search, search_found}; | |
+key_map($\^A, search) -> {search, search_quit}; | |
+key_map($\^B, search) -> {search, search_quit}; | |
+key_map($\^D, search) -> {search, search_quit}; | |
+key_map($\^E, search) -> {search, search_quit}; | |
+key_map($\^F, search) -> {search, search_quit}; | |
+key_map($\t, search) -> {search, search_quit}; | |
+key_map($\^L, search) -> {search, search_quit}; | |
+key_map($\^T, search) -> {search, search_quit}; | |
+key_map($\^U, search) -> {search, search_quit}; | |
+key_map($\^], search) -> {search, search_quit}; | |
+key_map($\^X, search) -> {search, search_quit}; | |
+key_map($\^Y, search) -> {search, search_quit}; | |
+key_map($\e, search) -> search_meta; | |
+key_map($[, search_meta) -> search_meta_left_sq_bracket; | |
+key_map(_C, search_meta_left_sq_bracket) -> {search, search_quit}; | |
+key_map(C, search) -> {insert_search,C}; | |
key_map(C, _) -> {undefined,C}. | |
%% do_op(Action, Before, After, Requests) | |
@@ -195,6 +237,57 @@ do_op({insert,C}, Bef, [], Rs) -> | |
{{[C|Bef],[]},[{put_chars, unicode,[C]}|Rs]}; | |
do_op({insert,C}, Bef, Aft, Rs) -> | |
{{[C|Bef],Aft},[{insert_chars, unicode, [C]}|Rs]}; | |
+%% Search mode prompt always looks like (search)`$TERMS': $RESULT. | |
+%% the {insert_search, _} handlings allow to share this implementation | |
+%% correctly with group.erl. This module provides $TERMS, and group.erl | |
+%% is in charge of providing $RESULT. | |
+%% This require a bit of trickery. Because search disables moving around | |
+%% on the line (left/right arrow keys and other shortcuts that just exit | |
+%% search mode), we can use the Bef and Aft variables to hold each | |
+%% part of the line. Bef takes charge of "(search)`$TERMS" and Aft | |
+%% takes charge of "': $RESULT". | |
+do_op({insert_search, C}, Bef, [], Rs) -> | |
+ Aft="': ", | |
+ {{[C|Bef],Aft}, | |
+ [{insert_chars, unicode, [C]++Aft}, {delete_chars,-3} | Rs], | |
+ search}; | |
+do_op({insert_search, C}, Bef, Aft, Rs) -> | |
+ Offset= length(Aft), | |
+ NAft = "': ", | |
+ {{[C|Bef],NAft}, | |
+ [{insert_chars, unicode, [C]++NAft}, {delete_chars,-Offset} | Rs], | |
+ search}; | |
+do_op({search, backward_delete_char}, [_|Bef], Aft, Rs) -> | |
+ Offset= length(Aft)+1, | |
+ NAft = "': ", | |
+ {{Bef,NAft}, | |
+ [{insert_chars, unicode, NAft}, {delete_chars,-Offset}|Rs], | |
+ search}; | |
+do_op({search, backward_delete_char}, [], _Aft, Rs) -> | |
+ Aft="': ", | |
+ {{[],Aft}, Rs, search}; | |
+do_op({search, skip_up}, Bef, Aft, Rs) -> | |
+ Offset= length(Aft), | |
+ NAft = "': ", | |
+ {{[$\^R|Bef],NAft}, % we insert ^R as a flag to whoever called us | |
+ [{insert_chars, unicode, NAft}, {delete_chars,-Offset}|Rs], | |
+ search}; | |
+do_op({search, skip_down}, Bef, Aft, Rs) -> | |
+ Offset= length(Aft), | |
+ NAft = "': ", | |
+ {{[$\^S|Bef],NAft}, % we insert ^S as a flag to whoever called us | |
+ [{insert_chars, unicode, NAft}, {delete_chars,-Offset}|Rs], | |
+ search}; | |
+do_op({search, search_found}, _Bef, Aft, Rs) -> | |
+ "': "++NAft = Aft, | |
+ {{[],NAft}, | |
+ [{put_chars, unicode, "\n"}, {move_rel,length(Aft)} | Rs], | |
+ search_found}; | |
+do_op({search, search_quit}, _Bef, Aft, Rs) -> | |
+ "': "++NAft = Aft, | |
+ {{[],NAft}, | |
+ [{put_chars, unicode, "\n"}, {move_rel,length(Aft)} | Rs], | |
+ search_quit}; | |
%% do blink after $$ | |
do_op({blink,C,M}, Bef=[$$,$$|_], Aft, Rs) -> | |
N = over_paren(Bef, C, M), | |
@@ -452,6 +545,9 @@ prompt({line,Pbs,_,_}) -> | |
current_line({line,_,{Bef, Aft},_}) -> | |
reverse(Bef, Aft ++ "\n"). | |
+current_chars({line,_,{Bef,Aft},_}) -> | |
+ reverse(Bef, Aft). | |
+ | |
%% %% expand(CurrentBefore) -> | |
%% %% {yes,Expansion} | no | |
%% %% Try to expand the word before as either a module name or a function | |
-- | |
1.7.10.2 (Apple Git-33) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm looking into ways to customize the key map. Is hacking the code the only way?