1%%%----------------------------------------------------------------------
2%%% File    : eed.erl
3%%% Author  : Bjorn Gustavsson <bjorn@strider>
4%%% Purpose : Unix `ed' look-alike.
5%%% Created : 24 Aug 1997 by Bjorn Gustavsson <bjorn@strider>
6%%%----------------------------------------------------------------------
7
8-module(eed).
9-author('bjorn@strider').
10
11-export([edit/0, edit/1, file/1, cmd_line/1]).
12
13-compile({no_auto_import,[error/1]}).
14
15-record(state, {dot = 0,			% Line number of dot.
16		upto_dot = [],			% Lines up to dot (reversed).
17		after_dot = [],			% Lines after dot.
18		lines = 0,			% Total number of lines.
19		print=false,			% Print after command.
20		filename=[],			% Current file.
21		pattern,			% Current pattern.
22		in_global=false,		% True if executing global command.
23		input=[],			% Global input stream.
24		undo,				% Last undo state.
25		marks=[],			% List of marks.
26		modified=false,			% Buffer is modified.
27		opts=[{prompt, ''}],		% Options.
28		last_error,			% The last error encountered.
29		input_fd			% Input file descriptor.
30	       }).
31
32-record(line, {contents,			% Contents of line.
33	       mark=false			% Marked (for global prefix).
34	      }).
35
36cmd_line([Script]) ->
37    file(Script),
38    halt().
39
40file(Script) ->
41    case file:open(Script, [read]) of
42	{ok,Fd} ->
43	    loop(#state{input_fd=Fd}),
44	    ok;
45	{error,E} ->
46	    {error,E}
47    end.
48
49edit() ->
50    loop(#state{input_fd=group_leader()}).
51
52edit(Name) ->
53    loop(command([$e|Name], #state{input_fd=group_leader()})).
54
55loop(St0) ->
56    {ok, St1, Cmd} = get_line(St0),
57    case catch command(nonl(Cmd), St1) of
58	{'EXIT', Reason} ->
59	    %% XXX Should clear outstanding global command here.
60	    loop(print_error({'EXIT', Reason}, St1));
61	quit ->
62	    ok;
63	{error, Reason} ->
64	    loop(print_error(Reason, St1));
65	St2 when is_record(St2, state) ->
66	    loop(St2)
67    end.
68
69nonl([$\n]) -> [];
70nonl([]) -> [];
71nonl([H|T]) -> [H|nonl(T)].
72
73command(Cmd, St) ->
74    case parse_command(Cmd, St) of
75	quit ->
76	    quit;
77	St1 when is_function(St1#state.print) ->
78	    if
79		St1#state.dot /= 0 ->
80		    print_current(St1);
81		true ->
82		    ok
83	    end,
84	    St1#state{print=false};
85	St1 when is_record(St1, state) ->
86	    St1
87    end.
88
89get_line(St) ->
90    Opts = St#state.opts,
91    {value, {prompt, Prompt}} = lists:keysearch(prompt, 1, Opts),
92    get_line(Prompt, St).
93
94get_line(Prompt, St) when St#state.input == [] ->
95    Line = get_line1(St#state.input_fd, Prompt, []),
96    {ok, St, Line};
97get_line(_, St) ->
98    get_input(St#state.input, St, []).
99
100get_input([eof], St, []) ->
101    {ok, St, eof};
102get_input([eof], St, Result) ->
103    {ok, St#state{input=[eof]}, lists:reverse(Result)};
104get_input([$\n|Rest], St, Result) ->
105    {ok, St#state{input=Rest}, lists:reverse(Result)};
106get_input([C|Rest], St, Result) ->
107    get_input(Rest, St, [C|Result]).
108
109get_line1(Io, Prompt, Result) ->
110    get_line2(Io, io:get_line(Io, Prompt), Result).
111
112get_line2(_Io, eof, []) ->
113    eof;
114get_line2(_Io, eof, Result) ->
115    lists:reverse(Result);
116get_line2(Io, [$\\, $\n], Result) ->
117    get_line1(Io, '', [$\n|Result]);
118get_line2(_Io, [$\n], Result) ->
119    lists:reverse(Result, [$\n]);
120get_line2(Io, [C|Rest], Result) ->
121    get_line2(Io, Rest, [C|Result]).
122
123print_error(Reason, St0) ->
124    St1 = St0#state{last_error=Reason},
125    io:put_chars("?\n"),
126    case lists:member(help_always, St1#state.opts) of
127	true ->
128	    help_command([], [], St1),
129	    St1;
130	false ->
131	    St1
132    end.
133
134format_error(bad_command) -> "unknown command";
135format_error(bad_filename) -> "illegal or missing filename";
136format_error(bad_file) -> "cannot open input file";
137format_error(bad_linenum) -> "line out of range";
138format_error(bad_delimiter) -> "illegal or missing delimiter";
139format_error(bad_undo) -> "nothing to undo";
140format_error(bad_mark) -> "mark not lower case ascii";
141format_error(bad_pattern) -> "invalid regular expression";
142format_error(buffer_modified) -> "warning: expecting `w'";
143format_error(nested_globals) -> "multiple globals not allowed";
144format_error(nomatch) -> "search string not found";
145format_error(missing_space) -> "no space after command";
146format_error(garbage_after_command) -> "illegal suffix";
147format_error(not_implemented) -> "not implemented yet";
148format_error({'EXIT', {Code, {Mod, Func, Args}}}) ->
149    lists:flatten(io_lib:format("aborted due to bug (~p)",
150				[{Code, {Mod, Func, length(Args)}}]));
151format_error(A) -> atom_to_list(A).
152
153
154
155%%% Parsing commands.
156
157parse_command(Cmd, St) ->
158    parse_command(Cmd, St, []).
159
160parse_command(Cmd, State, Nums) ->
161    case get_one(Cmd, State) of
162	{ok, Num, Rest, NewState} ->
163	    parse_next_address(Rest, NewState, [Num|Nums]);
164	false ->
165	    parse_command1(Cmd, State, Nums)
166    end.
167
168parse_next_address([$,|Rest], State, Nums) ->
169    parse_command(Rest, State, Nums);
170parse_next_address([$;|Rest], State, [Num|Nums]) ->
171    parse_command(Rest, move_to(Num, State), [Num|Nums]);
172parse_next_address(Rest, State, Nums) ->
173    parse_command1(Rest, State, Nums).
174
175parse_command1([Letter|Rest], State, Nums) ->
176    Cont = fun(Fun, NumLines, Def) ->
177		   execute_command(Fun, NumLines, Def, State, Nums, Rest) end,
178    parse_cmd_char(Letter, Cont);
179parse_command1([], State, Nums) ->
180    execute_command(fun print_command/3, 1, next, State, Nums, []).
181
182get_one(Cmd, St) ->
183    case get_address(Cmd, St) of
184	{ok, Addr, Cmd1, St1} ->
185	    get_one1(Cmd1, Addr, St1);
186	false ->
187	    get_one1(Cmd, false, St)
188    end.
189
190get_one1([D|Rest], false, St) when $0 =< D, D =< $9 ->
191    get_one2(get_number([D|Rest]), 1, 0, St);
192get_one1([D|Rest], Sum, St) when $0 =< D, D =< $9 ->
193    get_one2(get_number([D|Rest]), 1, Sum, St);
194get_one1([$+, D|Rest], Sum, St) when $0 =< D, D =< $9 ->
195    get_one2(get_number([D|Rest]), 1, Sum, St);
196get_one1([$-, D|Rest], Sum, St) when $0 =< D, D =< $9 ->
197    get_one2(get_number([D|Rest]), -1, Sum, St);
198get_one1([$+|Rest], Sum, St) ->
199    get_one2({ok, 1, Rest}, 1, Sum, St);
200get_one1([$-|Rest], Sum, St) ->
201    get_one2({ok, 1, Rest}, -1, Sum, St);
202get_one1(_Cmd, false, _St) ->
203    false;
204get_one1(Cmd, Sum, St) ->
205    {ok, Sum, Cmd, St}.
206
207get_one2({ok, Number, Rest}, Mul, false, St) ->
208    get_one1(Rest, St#state.dot+Mul*Number, St);
209get_one2({ok, Number, Rest}, Mul, Sum, St) ->
210    get_one1(Rest, Sum+Mul*Number, St).
211
212get_number(Cmd) ->
213    get_number(Cmd, 0).
214
215get_number([D|Rest], Result) when $0 =< D, D =< $9 ->
216    get_number(Rest, Result*10+D-$0);
217get_number(Rest, Result) ->
218    {ok, Result, Rest}.
219
220get_address([$.|Rest], State) ->
221    {ok, State#state.dot, Rest, State};
222get_address([$$|Rest], State) ->
223    {ok, State#state.lines, Rest, State};
224get_address([$', Mark|Rest], St) when $a =< Mark, Mark =< $z ->
225    case lists:keysearch(Mark, 2, St#state.marks) of
226	{value, {Line, Mark}} ->
227	    {ok, Line, Rest, St};
228	false ->
229	    {ok, 0, Rest, St}
230    end;
231get_address([$'|_Rest], _State) ->
232    error(bad_mark);
233get_address([$/|Rest], State) ->
234    scan_forward($/, Rest, State);
235get_address([$?|_Rest], _State) ->
236    error(not_implemented);
237get_address(_Cmd, _St) ->
238    false.
239
240scan_forward(End, Patt0, State) ->
241    {ok, Rest, NewState} = get_pattern(End, Patt0, State),
242    Dot = NewState#state.dot,
243    After = NewState#state.after_dot,
244    scan_forward1(Dot+1, After, NewState, Rest).
245
246scan_forward1(Linenum, [Line|Rest], State, RestCmd) ->
247    case re:run(Line#line.contents, State#state.pattern, [{capture, none}]) of
248	match ->
249	    {ok, Linenum, RestCmd, State};
250	nomatch ->
251	    scan_forward1(Linenum+1, Rest, State, RestCmd)
252    end;
253scan_forward1(_, [], State, RestCmd) ->
254    Dot = State#state.dot,
255    Upto = State#state.upto_dot,
256    case scan_forward2(Dot, Upto, State, RestCmd) of
257	false ->
258	    error(bad_linenum);
259	Other ->
260	    Other
261    end.
262
263scan_forward2(0, [], _State, _RestCmd) ->
264    false;
265scan_forward2(Linenum, [Line|Rest], State, RestCmd) ->
266    case scan_forward2(Linenum-1, Rest, State, RestCmd) of
267	false ->
268	    case re:run(Line#line.contents, State#state.pattern,
269			[{capture, none}]) of
270		match ->
271		    {ok, Linenum, RestCmd, State};
272		nomatch ->
273		    false
274	    end;
275	Other ->
276	    Other
277    end.
278
279parse_cmd_char($S, Cont) -> Cont(fun quest_command/3, 0, none);
280parse_cmd_char($T, Cont) -> Cont(fun time_command/3, 0, none);
281parse_cmd_char($=, Cont) -> Cont(fun print_linenum/3, 1, last);
282parse_cmd_char($a, Cont) -> Cont(fun append_command/3, 1, dot);
283parse_cmd_char($c, Cont) -> Cont(fun change_command/3, 2, dot);
284parse_cmd_char($d, Cont) -> Cont(fun delete_command/3, 2, dot);
285parse_cmd_char($e, Cont) -> Cont(fun enter_command/3, 0, none);
286parse_cmd_char($E, Cont) -> Cont(fun enter_always_command/3, 0, none);
287parse_cmd_char($f, Cont) -> Cont(fun file_command/3, 0, none);
288parse_cmd_char($g, Cont) -> Cont(fun global_command/3, 2, all);
289parse_cmd_char($h, Cont) -> Cont(fun help_command/3, 0, none);
290parse_cmd_char($H, Cont) -> Cont(fun help_always_command/3, 0, none);
291parse_cmd_char($i, Cont) -> Cont(fun insert_command/3, 1, dot);
292parse_cmd_char($k, Cont) -> Cont(fun mark_command/3, 1, dot);
293parse_cmd_char($l, Cont) -> Cont(fun list_command/3, 2, dot);
294parse_cmd_char($m, Cont) -> Cont(fun move_command/3, 2, dot);
295parse_cmd_char($n, Cont) -> Cont(fun number_command/3, 2, dot);
296parse_cmd_char($p, Cont) -> Cont(fun print_command/3, 2, dot);
297parse_cmd_char($P, Cont) -> Cont(fun prompt_command/3, 0, none);
298parse_cmd_char($q, Cont) -> Cont(fun quit_command/3, 0, none);
299parse_cmd_char($Q, Cont) -> Cont(fun quit_always_command/3, 0, none);
300parse_cmd_char($r, Cont) -> Cont(fun read_command/3, 1, last);
301parse_cmd_char($s, Cont) -> Cont(fun subst_command/3, 2, dot);
302parse_cmd_char($t, Cont) -> Cont(fun transpose_command/3, 2, dot);
303parse_cmd_char($u, Cont) -> Cont(fun undo_command/3, 0, none);
304parse_cmd_char($v, Cont) -> Cont(fun vglobal_command/3, 2, all);
305parse_cmd_char($w, Cont) -> Cont(fun write_command/3, 2, all);
306parse_cmd_char(_, _Cont)  -> error(bad_command).
307
308execute_command(Fun, NumLines, Def, State, Nums, Rest) ->
309    Lines = check_lines(NumLines, Def, Nums, State),
310    Fun(Rest, Lines, State).
311
312check_lines(0, _, [], _State) ->
313    [];
314check_lines(1, dot, [], #state{dot=Dot}) ->
315    [Dot];
316check_lines(1, next, [], State) when State#state.dot < State#state.lines ->
317    [State#state.dot+1];
318check_lines(1, last, [], State) ->
319    [State#state.lines];
320check_lines(1, _, [Num|_], State) when 0 =< Num, Num =< State#state.lines ->
321    [Num];
322check_lines(2, dot, [], #state{dot=Dot}) ->
323    [Dot, Dot];
324check_lines(2, all, [], #state{lines=Lines}) ->
325    [1, Lines];
326check_lines(2, _, [Num], State) when 0 =< Num, Num =< State#state.lines ->
327    [Num, Num];
328check_lines(2, _, [Num2, Num1|_], State)
329when 0 =< Num1, Num1 =< Num2, Num2 =< State#state.lines ->
330    [Num1, Num2];
331check_lines(_, _, _, _) ->
332    error(bad_linenum).
333
334
335%%% Executing commands.
336
337%% ($)= - print line number
338
339print_linenum(Rest, [Line], State) ->
340    NewState = check_trailing_p(Rest, State),
341    io:format("~w\n", [Line]),
342    NewState.
343
344%% ? - print state (for debugging)
345
346quest_command([], [], State) ->
347    io:format("~p\n", [State]),
348    State.
349
350%% Tcmd - time command
351
352time_command(Cmd, [], St) ->
353    Fun = fun parse_command/2,
354    erlang:garbage_collect(),
355    {Elapsed, Val} = timer:tc(erlang, apply, [Fun, [Cmd, St]]),
356    io:format("Time used: ~p s~n", [Elapsed/1000000.0]),
357    case Val of
358	{error, Reason} ->
359	    throw({error, Reason});
360	Other ->
361	    Other
362    end.
363
364%% (.)a - append text
365
366append_command(Rest, [Line], St0) ->
367    St1 = save_for_undo(St0),
368    append(move_to(Line, check_trailing_p(Rest, St1))).
369
370append(St0) ->
371    {ok, St1, Line0} = get_line('', St0),
372    case Line0 of
373	eof ->
374	    St1;
375	".\n" ->
376	    St1;
377	Line ->
378	    append(insert_line(Line, St1))
379    end.
380
381%% (.,.)c
382
383change_command(Rest, Lines, St0) ->
384    St1 = delete_command(Rest, Lines, St0),
385    St2 = append_command([], [St1#state.dot-1], St1),
386    save_for_undo(St2, St0).
387
388%% (.,.)d - delete lines
389
390delete_command(_Rest, [0, _Last], _St) ->
391    error(bad_linenum);
392delete_command(Rest, [First, Last], St0) ->
393    St1 = check_trailing_p(Rest, save_for_undo(St0)),
394    delete(Last-First+1, move_to(Last, St1)).
395
396delete(0, St) when St#state.dot == St#state.lines ->
397    St;
398delete(0, St) ->
399    next_line(St);
400delete(Left, St0) ->
401    St1 = delete_current_line(St0),
402    delete(Left-1, St1).
403
404%% e file - replace buffer with new file
405
406enter_command(_Name, [], St) when St#state.modified == true ->
407    error(buffer_modified);
408enter_command(Name, [], St0) ->
409    enter_always_command(Name, [], St0).
410
411%% E file - replace buffer with new file
412
413enter_always_command(Name, [], St0) ->
414    St1 = read_command(Name, [0], #state{filename=St0#state.filename,
415					 opts=St0#state.opts}),
416    St1#state{modified=false}.
417
418%% f file - print filename; set filename
419
420file_command([], [], St) ->
421    io:format("~s~n", [St#state.filename]),
422    St;
423file_command([$_|Name0], [], St) ->
424    Name = skip_blanks(Name0),
425    file_command([], [], St#state{filename=Name});
426file_command(_, _, _) ->
427    error(missing_space).
428
429%% (1,$)g/RE/commands - execute commands on all matching lines.
430%% (1,$)v/RE/commands - execute commands on all non-matching lines.
431
432global_command(Cmd, Lines, St) ->
433    check_global0(true, Cmd, Lines, St).
434
435vglobal_command(Cmd, Lines, St) ->
436    check_global0(false, Cmd, Lines, St).
437
438check_global0(_, _, _, St) when St#state.in_global == true ->
439    error(nested_globals);
440check_global0(Sense, [Sep|Pattern], Lines, St0) ->
441    {ok, Cmd, St1} = get_pattern(Sep, Pattern, St0),
442    St2 = mark(Sense, Lines, St1),
443    do_global_command(Cmd, St2#state{in_global=true}, 0).
444
445mark(Sense, [First, Last], St0) ->
446    St1 = move_to(Last, St0),
447    mark1(Sense, First-1, St1).
448
449mark1(_Sense, First, St) when St#state.dot == First ->
450    St;
451mark1(Sense, First, St) ->
452    [Line|Prev] = St#state.upto_dot,
453    NewLine = case match(St) of
454		  true  -> Line#line{mark=Sense};
455		  false -> Line#line{mark=not(Sense)}
456	      end,
457    mark1(Sense, First, prev_line(St#state{upto_dot=[NewLine|Prev]})).
458
459do_global_command(Cmd, St0, Matches) ->
460    case find_mark(St0) of
461	{ok, St1} ->
462	    St2 = St1#state{input=Cmd++[eof]},
463	    {ok, St3, Cmd1} = get_line(St2),
464	    St4 = command(Cmd1, St3),
465	    %% XXX There might be several commands.
466	    do_global_command(Cmd, St4, Matches+1);
467	false when Matches == 0 ->
468	    error(nomatch);
469	false ->
470	    St0#state{in_global=false, input=[]}
471    end.
472
473find_mark(State) ->
474    find_mark(State#state.lines, State).
475
476find_mark(0, _State) ->
477    false;
478find_mark(Limit, State) when State#state.dot == 0 ->
479    find_mark(Limit, next_line(State));
480find_mark(Limit, State) ->
481    case State#state.upto_dot of
482	[Line|Prev] when Line#line.mark == true ->
483	    NewLine = Line#line{mark=false},
484	    {ok, State#state{upto_dot=[NewLine|Prev]}};
485	_Other ->
486	    find_mark(Limit-1, wrap_next_line(State))
487    end.
488
489%% h - print info about last error
490
491help_command([], [], St) ->
492    case St#state.last_error of
493	undefined ->
494	    St;
495	Reason ->
496	    io:put_chars(format_error(Reason)),
497	    io:nl(),
498	    St
499    end;
500help_command(_, _, _) ->
501    error(garbage_after_command).
502
503%% H - toggle automatic help mode on/off
504
505help_always_command([], [], St) ->
506    Opts = St#state.opts,
507    case lists:member(help_always, Opts) of
508	true ->
509	    St#state{opts=Opts--[help_always]};
510	false ->
511	    help_command([], [], St),
512	    St#state{opts=[help_always|Opts]}
513    end.
514
515%% (.)i - insert text
516
517insert_command(_Rest, [0], _State) ->
518    error(bad_linenum);
519insert_command(Rest, [Line], State) ->
520    append_command(Rest, [Line-1], State).
521
522%% (.)kx - mark line
523
524mark_command(_, [0], _St) ->
525    error(bad_linenum);
526mark_command([Mark|_Rest], [_Line], _St) when $a =< Mark, Mark =< $z ->
527    error(not_implemented);
528mark_command(_, _, _) ->
529    error(bad_mark).
530
531%% (.,.)l - list lines
532
533list_command(Rest, Lines, St) ->
534    print([$l|Rest], Lines, St).
535
536%% (.,.)m - move lines
537
538move_command(_Cmd, [_First, _Last], _St) ->
539    error(not_implemented).
540
541%% (.,.)t - copy lines
542
543transpose_command(_Cmd, [_First, _Last], _St) ->
544    error(not_implemented).
545
546%% (.,.)n - print lines with line numbers
547
548number_command(Rest, Lines, St) ->
549    print([$n|Rest], Lines, St).
550
551%% (.,.)p - print lines
552
553print_command(Rest, Lines, St) ->
554    print([$p|Rest], Lines, St).
555
556%% P - toggle prompt
557
558prompt_command([], [], St) ->
559    Opts = St#state.opts,
560    case lists:keysearch(prompt, 1, Opts) of
561	{value, {prompt, ''}} ->
562	    St#state{opts=[{prompt, '*'}|Opts]};
563	{value, Value} ->
564	    St#state{opts=[{prompt, ''} | Opts--[Value]]}
565    end;
566prompt_command(_, _, _) ->
567    error(garbage_after_command).
568
569%% q - quit editor
570
571quit_command([], [], _) ->
572    quit;
573quit_command(_, _, _) ->
574    error(garbage_after_command).
575
576%% Q - quit editor
577
578quit_always_command([], [], _) ->
579    quit;
580quit_always_command(_, _, _) ->
581    error(garbage_after_command).
582
583%% ($)r file - read file
584
585read_command([], _, St) when St#state.filename == [] ->
586    error(bad_filename);
587read_command([], [After], St) ->
588    read(After, St#state.filename, St);
589read_command([$ |Name0], [After], St) when St#state.filename == [] ->
590    Name = skip_blanks(Name0),
591    read(After, Name, St#state{filename=Name});
592read_command([$ |Name0], [After], St) ->
593    Name = skip_blanks(Name0),
594    read(After, Name, St);
595read_command(_, _, _) ->
596    error(missing_space).
597
598read(After, Name, St0) ->
599    case file:read_file(Name) of
600	{ok, Bin} ->
601	    Chars = size(Bin),
602	    St1 = move_to(After, St0),
603	    St2 = insert_line(binary_to_list(Bin), St1),
604	    io:format("~w~n", [Chars]),
605	    St2;
606	{error, _} ->
607	    error(bad_file)
608    end.
609
610%% s/pattern/replacement/gp
611
612subst_command(_, [0, _], _) ->
613    error(bad_linenum);
614subst_command([$ |_Cmd0], [_First, _Last], _St0) ->
615    error(bad_delimiter);
616subst_command([$\n|_Cmd0], [_First, _Last], _St0) ->
617    error(bad_delimiter);
618subst_command([Sep|Cmd0], [First, Last], St0) ->
619    St1 = save_for_undo(St0),
620    {ok, Cmd1, St2} = get_pattern(Sep, Cmd0, St1),
621    {ok, Replacement, Cmd2} = get_replacement(Sep, Cmd1),
622    {ok, Opts, Cmd3} = subst_check_gflag(Cmd2),
623    St3 = check_trailing_p(Cmd3, St2),
624    subst_command(Last-First+1, Opts, Replacement,
625		  move_to(First-1, St3), nomatch);
626subst_command([], _, _) ->
627    error(bad_delimiter).
628
629subst_command(0, _, _, _, nomatch) ->
630    error(nomatch);
631subst_command(0, _, _, _, StLast) when is_record(StLast, state) ->
632    StLast;
633subst_command(Left, Opts, Repl, St0, LastMatch) ->
634    St1 = next_line(St0),
635    [Line|_] = St1#state.upto_dot,
636    Contents = Line#line.contents,
637    case re:replace(Contents, St1#state.pattern, Repl, Opts) of
638	Contents ->
639	    subst_command(Left-1, Opts, Repl, St1, LastMatch);
640	NewContents ->
641	    %% XXX This doesn't work with marks.
642	    St2 = delete_current_line(St1),
643	    St3 = insert_line(NewContents, St2),
644	    subst_command(Left-1, Opts, Repl, St3, St3)
645    end.
646
647subst_check_gflag([$g|Cmd]) -> {ok, [global,{return,list}], Cmd};
648subst_check_gflag(Cmd)      -> {ok, [{return,list}], Cmd}.
649
650%% u - undo
651
652undo_command([], [], St) when St#state.undo == undefined ->
653    error(bad_undo);
654undo_command([], [], #state{undo=Undo}) ->
655    Undo;
656undo_command(_, _, _) ->
657    error(garbage_after_command).
658
659%% (1,$)w - write buffer to file
660
661write_command(_Cmd, [_First, _Last], _St) ->
662    error(not_implemented).
663
664
665%%% Primitive buffer operations.
666
667print_current(St) ->
668    [Line|_] = St#state.upto_dot,
669    Printer = St#state.print,
670    Printer(Line#line.contents, St).
671
672delete_current_line(St) when St#state.dot == 0 ->
673    error(bad_linenum);
674delete_current_line(St) ->
675    Lines = St#state.lines,
676    [_|Prev] = St#state.upto_dot,
677    St#state{dot=St#state.dot-1, upto_dot=Prev, lines=Lines-1, modified=true}.
678
679insert_line(Line, State) ->
680    insert_line1(Line, State, []).
681
682insert_line1([$\n|Rest], State, Result) ->
683    NewState = insert_single_line(lists:reverse(Result, [$\n]), State),
684    insert_line1(Rest, NewState, []);
685insert_line1([C|Rest], State, Result) ->
686    insert_line1(Rest, State, [C|Result]);
687insert_line1([], State, []) ->
688    State;
689insert_line1([], State, Result) ->
690    insert_single_line(lists:reverse(Result, [$\n]), State).
691
692insert_single_line(Line0, State) ->
693    Line = #line{contents=Line0},
694    Dot = State#state.dot,
695    Before = State#state.upto_dot,
696    Lines = State#state.lines,
697    %% XXX Avoid updating the record every time.
698    State#state{dot=Dot+1, upto_dot=[Line|Before], lines=Lines+1, modified=true}.
699
700move_to(Line, State) when Line < State#state.dot ->
701    move_to(Line, prev_line(State));
702move_to(Line, State) when State#state.dot < Line ->
703    move_to(Line, next_line(State));
704move_to(Line, State) when Line == State#state.dot ->
705    State.
706
707prev_line(State) ->
708    Dot = State#state.dot,
709    Before = State#state.upto_dot,
710    After = State#state.after_dot,
711    State#state{dot=Dot-1, upto_dot=tl(Before), after_dot=[hd(Before)|After]}.
712
713next_line(State) ->
714    Dot = State#state.dot,
715    Before = State#state.upto_dot,
716    After = State#state.after_dot,
717    State#state{dot=Dot+1, upto_dot=[hd(After)|Before], after_dot=tl(After)}.
718
719wrap_next_line(State) when State#state.dot == State#state.lines ->
720    move_to(1, State);
721wrap_next_line(State) ->
722    next_line(State).
723
724
725%%% Utilities.
726
727get_pattern(End, Cmd, State) ->
728    get_pattern(End, Cmd, State, []).
729
730get_pattern(End, [End|Rest], State, []) when State#state.pattern /= undefined ->
731    {ok, Rest, State};
732get_pattern(End, [End|Rest], State, Result) ->
733    case re:compile(lists:reverse(Result)) of
734	{error, _} ->
735	    error(bad_pattern);
736	{ok, Re} ->
737	    {ok, Rest, State#state{pattern=Re}}
738    end;
739get_pattern(End, [C|Rest], State, Result) ->
740    get_pattern(End, Rest, State, [C|Result]);
741get_pattern(End, [], State, Result) ->
742    get_pattern(End, [End], State, Result).
743
744get_replacement(End, Cmd) ->
745    get_replacement(End, Cmd, []).
746
747get_replacement(End, [End|Rest], Result) ->
748    {ok, lists:reverse(Result), Rest};
749get_replacement(End, [$\\, $&|Rest], Result) ->
750    get_replacement(End, Rest, [$&, $\\|Result]);
751get_replacement(End, [$\\, C|Rest], Result) ->
752    get_replacement(End, Rest, [C|Result]);
753get_replacement(End, [C|Rest], Result) ->
754    get_replacement(End, Rest, [C|Result]);
755get_replacement(End, [], Result) ->
756    get_replacement(End, [End], Result).
757
758check_trailing_p([$l], St) ->
759    St#state{print=fun(Line, _) -> lister(Line, 0) end};
760check_trailing_p([$n], St) ->
761    St#state{print=fun numberer/2};
762check_trailing_p([$p], St) ->
763    St#state{print=fun(Line, _) -> io:put_chars(Line) end};
764check_trailing_p([], State) ->
765    State;
766check_trailing_p(_Other, _State) ->
767    error(garbage_after_command).
768
769error(Reason) ->
770    throw({error, Reason}).
771
772match(State) when State#state.dot == 0 ->
773    false;
774match(State) ->
775    [Line|_] = State#state.upto_dot,
776    Re = State#state.pattern,
777    case re:run(Line#line.contents, Re, [{capture, none}]) of
778	match   -> true;
779	nomatch -> false
780    end.
781
782skip_blanks([$ |Rest]) ->
783    skip_blanks(Rest);
784skip_blanks(Rest) ->
785    Rest.
786
787print(Rest, [Line], St0) when Line > 0 ->
788    St1 = check_trailing_p(Rest, St0),
789    print(Line, move_to(Line-1, St1));
790print(Rest, [First, Last], St0) when First > 0 ->
791    St1 = check_trailing_p(Rest, St0),
792    print(Last, move_to(First-1, St1)).
793
794print(Last, St) when St#state.dot == Last ->
795    St#state{print=false};
796print(Last, St0) ->
797    St1 = next_line(St0),
798    print_current(St1),
799    print(Last, St1).
800
801lister(Rest, 64) ->
802    io:put_chars("\\\n"),
803    lister(Rest, 0);
804lister([C|Rest], Num) ->
805    list_char(C),
806    lister(Rest, Num+1);
807lister([], _) ->
808    ok.
809
810list_char($\t) ->
811    io:put_chars("\\t");
812list_char($\n) ->
813    io:put_chars("$\n");
814list_char(C) ->
815    io:put_chars([C]).
816
817numberer(Line, St) ->
818    io:format("~w\t~s", [St#state.dot, Line]).
819
820save_for_undo(St) ->
821    St#state{undo=St#state{undo=undefined, print=false}}.
822
823save_for_undo(St, OldSt) ->
824    St#state{undo=OldSt#state{undo=undefined, print=false}}.
825