On Monday and Tuesday I started learning Erlang through the Erlang Koans, and around Wednesday night I decided to use my newfound knowledge to implement a simple text editor (Akin to ed(1)). Currently I have the following (also on GitLab):
-module(text).
-compile(export_all).
%% State => {file, buffer, cursor}
main() ->
{ok, Arg} = init:get_argument(f),
Name = lists:nth(1, lists:nth(1, Arg)),
main(Name).
main (String) ->
{ok, File} = file:open(String, [read, write]),
{ok, Buffer, Lines} = buffer(File),
io:fwrite("~B lines read of ~s~n", [Lines, String]),
loop({File, Buffer, 1}).
loop(State) ->
NewState = command(State),
case NewState of
{_, _, _} -> loop(NewState);
_ -> ok
end.
append(Buffer, Line) ->
erlang:insert_element(tuple_size(Buffer)+1, Buffer, Line).
buffer(File) ->
buffer(File, {}).
buffer(File, Buffer) ->
Line = io:get_line(File, ""),
case Line of
eof -> {ok, Buffer, tuple_size(Buffer)};
{error, Description} -> {error, Description};
_ -> buffer(File, append(Buffer, Line))
end.
put(stdout, {_, {}, _}, _) ->
'nothing to write';
put(stdout, {_, Buffer, _}, Line) ->
io:fwrite(" ~s", [element(Line, Buffer)]);
put(file, {File, Buffer, _}, Line) ->
io:fwrite(File, "~s", [element(Line, Buffer)]).
%% Ew Ew Ew Ew Ew
write_buffer(stdout, {File, Buffer, Pos}) ->
write_buffer(stdout, {File, Buffer, Pos}, 1, tuple_size(Buffer)).
write_buffer({File, Buffer, Pos}) ->
file:position(File, bof),
write_buffer(file, {File, Buffer, Pos}, 1, tuple_size(Buffer)).
write_buffer(Out, State, N, Length) when N =< Length ->
put(Out, State, N),
write_buffer(Out, State, N+1, Length);
write_buffer(_, {File, {}, Pos}, _, _) ->
file:truncate(File),
{File, {}, Pos};
write_buffer(_, State, _, _) ->
State.
put_cursor(N, {File, Buffer, Pos}) ->
put_cursor(Pos, Pos+N, {File, Buffer, Pos}).
put_cursor(Current, End, State) when Current < End ->
put(stdout, State, Current),
put_cursor(Current+1, End, State);
put_cursor(_, _, State) ->
State.
is_stop(Line) ->
(string:len(Line) == 2) and (string:substr(Line, 1, 1) == ".").
input() ->
Line = io:get_line(" "),
case is_stop(Line) of
false -> Line;
true -> stop
end.
modify(replace, State) ->
modify(replace, State, input());
modify(append, State) ->
modify(append, State, input());
modify(change, State) ->
modify(change, State, input()).
modify(_, State, stop) ->
State;
modify(replace, {File, Buffer, Pos}, Input) ->
modify(replace, {File, replace(Buffer, Pos, Input), Pos+1}, input());
modify(append, {File, Buffer, Pos}, Input) ->
modify(append, {File, insert(Buffer, Pos+1, Input), Pos+1}, input());
modify(change, {File, Buffer, Pos}, Input)
when tuple_size(Buffer) == 0; Pos =< 0; Pos > tuple_size(Buffer) ->
modify(append, {File, replace(Buffer, Pos, Input), Pos}, input());
modify(change, State, Input) ->
modify(append, State, Input).
replace(Buffer, Pos, _)
when tuple_size(Buffer) == 0; Pos =< 0; Pos > tuple_size(Buffer) ->
Buffer;
replace(Buffer, Pos, Input) ->
setelement(Pos, Buffer, Input).
insert(Buffer, Pos, Input) when Pos =< 0 ->
erlang:insert_element(1, Buffer, Input);
insert(Buffer, Pos, Input) when Pos > tuple_size(Buffer) ->
erlang:insert_element(tuple_size(Buffer) + 1, Buffer, Input);
insert(Buffer, Pos, Input) ->
erlang:insert_element(Pos, Buffer, Input).
guard_position({File, Buffer, Pos}) when tuple_size(Buffer) == 0; Pos < 1 ->
{File, Buffer, 1};
guard_position({File, Buffer, Pos}) when Pos > tuple_size(Buffer) ->
{File, Buffer, tuple_size(Buffer)};
guard_position(State) ->
State.
get_arg(String) when is_list(String) ->
{Integer, _} = string:to_integer(String),
case is_integer(Integer) andalso Integer > 0 of
true -> Integer;
_ -> 1
end;
get_arg(_) -> 1.
position([Char|Tail], {File, Buffer, Pos}) ->
case Char of
%% +, -, <number>
43 -> {File, Buffer, Pos+1};
45 -> {File, Buffer, Pos-1};
_ -> {Integer, "\n"} = string:to_integer([Char] ++ Tail),
{File, Buffer, Integer}
end.
remove(N, {File, Buffer, Pos}) ->
{File, erlang:delete_element(N, Buffer), Pos}.
delete([], {File, Buffer, Pos}) ->
remove(Pos, {File, Buffer, Pos});
delete([Char|Tail], {File, Buffer, Pos}) ->
case Char of
%% *,
42 -> {File, {}, Pos};
_ -> remove(get_arg([Char] ++ Tail), {File, Buffer, Pos})
end;
delete(N, {File, Buffer, Pos}) ->
delete(Pos, Pos+N, {File, Buffer, Pos}).
delete(Current, End, State) when Current < End ->
delete(Current+1, End, remove(Current, State));
delete(_, _, State) ->
State.
search(_, Line, {File, Buffer, Pos}) when Line > tuple_size(Buffer) ->
io:fwrite("not found~n"),
{File, Buffer, Pos};
search(String, Line, {F, Buffer, Pos}) when tuple_size(Buffer) > 0, Line > 0 ->
case string:str(element(Line, Buffer), String) of
0 -> search(String, Line+1, {F, Buffer, Pos});
_ -> {F, Buffer, Line}
end.
search(String, {File, Buffer, Pos}) when is_list(String) ->
search(string:strip(String, both, $\n), Pos+1, {File, Buffer, Pos});
search(_, State) ->
io:fwrite("?~n"),
State.
join({File, Buffer, Pos}) ->
{File, join(Pos, Buffer), Pos}.
join(Line, Buffer) when Line+1 =< tuple_size(Buffer) ->
Concat = string:join([string:strip(element(Line, Buffer), right, $\n),
element(Line+1, Buffer)], ""),
NewBuffer = setelement(Line, Buffer, Concat),
erlang:delete_element(Line+1, NewBuffer);
join(_, Buffer) ->
Buffer.
help() ->
HelpString =
"erled -- erlang line editor, Finn O'leary\
Started on 2016-05-31. Current iteration: 2016-06-03_3.\
commands:\
q, ?, w, p -- quit, help, write file, print file.\
a, c, r -- append, change, replace.\
Please note that append, change and replace all accept
input until a single line with a `.` has been input.\
d, d*, d<n> -- delete line, file, <n> lines.\
P, P<n> -- print line, lines from current position.\
/<s>, j -- search for <s>, join next line to current.\
g+, g-, g<n> -- next line, prev line, go to line <n>.\
",
io:fwrite(HelpString).
command(State) ->
command(io:get_line(""), guard_position(State)).
command([Char|_Tail], State) ->
case Char of
%% q, w, c, a, r, p, P, g, d, /, j, ?, ...
113 -> {ok, State};
119 -> write_buffer(State);
99 -> modify(change, State);
97 -> modify(append, State);
114 -> modify(replace, State);
112 -> write_buffer(stdout, State);
80 -> put_cursor(get_arg(_Tail), State);
103 -> position(_Tail, State);
100 -> delete(_Tail, State);
47 -> search(_Tail, State);
106 -> join(State);
63 -> help(), State;
_ -> io:fwrite("?~n"), State
end.
On the whole I dislike the design, as I feel that I repeat some patterns rather too much. I also feel that the code is rather verbose. Could you give any advice on how to clean it up and perhaps make it idiomatic for Erlang code? I toyed with the idea of using gen_fsm, but it seemed like overkill for my needs at the time. I also attempted to use records at some point, but found I somehow ended up with more code than I started with (In part due to the State#state.foo
syntax).