aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile21
-rw-r--r--src/cord.erl504
-rw-r--r--src/cord_regexp.erl296
-rw-r--r--src/edit.erl264
-rw-r--r--src/edit_bench.erl32
-rw-r--r--src/edit_buf.erl320
-rw-r--r--src/edit_complete.erl170
-rw-r--r--src/edit_display.erl154
-rw-r--r--src/edit_display.erl.slow160
-rw-r--r--src/edit_eval.erl318
-rw-r--r--src/edit_extended.erl149
-rw-r--r--src/edit_file.erl95
-rw-r--r--src/edit_genserv.erl50
-rw-r--r--src/edit_globalmap.erl126
-rw-r--r--src/edit_help.erl97
-rw-r--r--src/edit_history.erl126
-rw-r--r--src/edit_input.erl34
-rw-r--r--src/edit_keymap.erl109
-rw-r--r--src/edit_lex.erl14
-rw-r--r--src/edit_lib.erl801
-rw-r--r--src/edit_make.erl14
-rw-r--r--src/edit_mod.erl42
-rw-r--r--src/edit_prof.erl36
-rw-r--r--src/edit_terminal.erl77
-rw-r--r--src/edit_terminal_gterm.erl42
-rw-r--r--src/edit_text.erl148
-rw-r--r--src/edit_transform.erl69
-rw-r--r--src/edit_util.erl245
-rw-r--r--src/edit_var.erl171
-rw-r--r--src/edit_window.erl45
-rw-r--r--src/ermacs.in12
-rw-r--r--src/file_gl.erl62
32 files changed, 4803 insertions, 0 deletions
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..0e2630d
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,21 @@
+include ../../../support/include.mk
+
+ERLC_FLAGS += -I ../.. -pa ../ebin/
+
+SCRIPT=../bin/ermacs
+SED_EXPR = s\\%BASEDIR%\\`pwd`/..\\
+
+all: $(ERL_OBJECTS) $(SCRIPT)
+
+# edit_transform has to be built first. This rule is actually
+# circular, but that seems okay with GNU make.
+$(ERL_OBJECTS): ../ebin/edit_transform.beam
+
+$(SCRIPT): ermacs.in
+ sed ${SED_EXPR} < $< > $@
+ chmod +x $@
+
+clean:
+ -rm -f $(ERL_OBJECTS)
+ -rm -f ../bin/ermacs
+
diff --git a/src/cord.erl b/src/cord.erl
new file mode 100644
index 0000000..980248d
--- /dev/null
+++ b/src/cord.erl
@@ -0,0 +1,504 @@
+%%%----------------------------------------------------------------------
+%%% File : cord.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Data structure for large strings of text
+%%% Created : 21 Oct 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+%%
+%% Cords - a scalable data structure for strings of text.
+%%
+%% Cords are binary trees with erlang binaries as leaves. The trees
+%% are kept fairly balanced, and the sizes of the binary objects are
+%% kept within acceptable bounds. By using binaries, it should only
+%% cost about one byte per character.
+%%
+%% The main idea is to have a fast insert/replace operation which
+%% doesn't change the binaries too much. By keeping the binaries on
+%% the leaves fairly small, we generally get to move them about in one
+%% piece while we're balancing things after large updates or pulling
+%% out large regions. An added advantage of not disturbing the
+%% binaries is that it's not too expensive to keep old copies of cords
+%% around, since most of the binaries get shared on the heap.
+%%
+%% API:
+%% new():
+%% Creates a new, empty cord.
+%% new(ListOrBinary):
+%% Creates a new cord from some input characters.
+%% replace(Cord, NewText, Start, Length):
+%% Replaces the `Length' long portion of `Cord' starting at `Start'
+%% with `NewText' (a cord, binary, or iolist).
+%% join(Left, Right)
+%% Join two cords together into a new one.
+%% to_binary(Cord): Convert a cord into a binary
+%% to_list(Cord): Convert a cord into a list
+%% walk(Cord, Pos, Direction, Fun):
+%% "Walk" character by character along `Cord' in `Direction'. For each
+%% character, we call Fun(Char) which returns either {result, R}, or
+%% {more, NextFun}. If we run out of characters before the fun returns
+%% a result, we call Fun(finish), which is required to return
+%% {result, R}.
+
+-module(cord).
+-author('luke@bluetail.com').
+
+-compile(export_all).
+
+-export([new/0, new/1, cord_size/1,
+ replace/4, region/3, region_binary/3, region_list/3,
+ to_binary/1, to_list/1, to_iolist/1,
+ walk/4]).
+
+%% It seems that static values won't do for scaling to gigantic (tens
+%% of megabytes) sizes.
+-define(MIN_SIZE, 512).
+-define(MAX_SIZE, 2048).
+
+%% A cord is either a #cord record or a binary.
+-record(cord, {size, % Combined size
+ left, % cord()
+ right, % cord()
+ %% The dirty flag indicates that a cord (or one of its
+ %% children) has been changed since the last time the
+ %% cord was 'fixed'
+ dirty=false
+ }).
+
+-define(assert(X),
+ (case X of
+ true ->
+ true;
+ false ->
+ exit(lists:flatten(io_lib:format("Assertion failed at ~p:~p",
+ [?MODULE, ?LINE])))
+ end)).
+
+new() -> <<>>.
+
+new(B) when binary(B) -> fix_cord(B);
+new(L) when list(L) -> new(list_to_binary(L)).
+
+%% More efficient way to create a cord from a file. This
+%% implementation is not terribly clever (read small chunks, balance
+%% at the end), but at least it isn't a memory hog.
+%%
+%% Returns: {ok, cord()} | {error, Reason}
+new_from_file(Filename) ->
+ case file:open(Filename, [raw, binary]) of
+ X = {error, Rsn} ->
+ X;
+ {ok, F} ->
+ read_chunks(F)
+ end.
+
+read_chunks(F) ->
+ read_chunks(F, new()).
+
+read_chunks(F, Acc) ->
+ case file:read(F, ?MAX_SIZE) of
+ eof ->
+ {ok, fix_cord(Acc)};
+ {ok, Bin} ->
+ read_chunks(F, make_cord(Acc, Bin));
+ X = {error, Reason} ->
+ X
+ end.
+
+%% Make a cord.
+%% This is a "dirty" operation that doesn't rebalance the tree.
+make_cord(Left, Right) ->
+ #cord{size=cord_size(Left) + cord_size(Right),
+ left=Left,
+ right=Right,
+ dirty=true}.
+
+cord_size(Cord) when binary(Cord) ->
+ size(Cord);
+cord_size(Cord) when record(Cord, cord) ->
+ Cord#cord.size.
+
+max_depth(Cord) when binary(Cord) ->
+ 1;
+max_depth(Cord) ->
+ 1 + max(max_depth(Cord#cord.left),
+ max_depth(Cord#cord.right)).
+
+nr_nodes(Cord) when binary(Cord) ->
+ 1;
+nr_nodes(Cord) ->
+ 1 + nr_nodes(Cord#cord.left) + nr_nodes(Cord#cord.right).
+
+mean_leaf_size(Cord) ->
+ Sizes = leaf_sizes(Cord),
+ Sum = lists:foldr(fun(X, Acc) -> X + Acc end,
+ 0,
+ Sizes),
+ round(Sum / length(Sizes)).
+
+leaf_sizes(Cord) when binary(Cord) ->
+ [size(Cord)];
+leaf_sizes(Cord) ->
+ leaf_sizes(Cord#cord.left) ++ leaf_sizes(Cord#cord.right).
+
+max(X, Y) when X > Y -> X;
+max(X, Y) -> Y.
+
+insert(Cord, New, Point) ->
+ replace(Cord, New, Point, 0).
+
+delete(Cord, Point, Length) ->
+ replace(Cord, [], Point, Length).
+
+%% replace/4: Replace a region of the cord. `New' is the text to
+%% replace the region with, and can be either a list, binary, or cord.
+replace(Cord, New, Start, Length) when list(New) ->
+ replace(Cord, list_to_binary(New), Start, Length);
+replace(Cord, New, Start, Length) ->
+ %% Replace is done by copying the areas on the left and right of
+ %% the region, and joining them together with the new cord in the
+ %% middle.
+ {A, B} = split(Cord, Start-1),
+ {C, D} = split(B, Length),
+ fix_cord(make_cord(make_cord(A, New), D)).
+
+split(Cord, 0) when binary(Cord) ->
+ {<<>>, Cord};
+split(Cord, Pos) when binary(Cord) ->
+ ?assert(Pos =< cord_size(Cord)),
+ <<Left:Pos/binary, Right/binary>> = Cord,
+ {Left, Right};
+
+split(Cord, Pos) when record(Cord, cord) ->
+ ?assert(Pos =< cord_size(Cord)),
+ LeftSz = cord_size(Cord#cord.left),
+ RightSz = cord_size(Cord#cord.right),
+ %%io:format("Split - left:~p right:~p~n", [LeftSz, RightSz]),
+ if LeftSz == Pos ->
+ {Cord#cord.left, Cord#cord.right};
+ LeftSz > Pos ->
+ {SplitLeft, SplitRight} = split(Cord#cord.left, Pos),
+ {SplitLeft, make_cord(SplitRight, Cord#cord.right)};
+ LeftSz < Pos ->
+ {SplitLeft, SplitRight} = split(Cord#cord.right, Pos-LeftSz),
+ {make_cord(Cord#cord.left, SplitLeft), SplitRight}
+ end.
+
+%% join two cords together and rebalance.
+join(Left, Right) when binary(Left) ->
+ fix_cord(make_cord(Left, Right)).
+
+%% fix_cord/1
+%%
+%% "Fix" a cord so that it's reasonably balanced, and it's leaves are
+%% reasonable sizes.
+
+%% Leaf (binary) - break it up if it's too big
+fix_cord(Bin) when binary(Bin) ->
+ if size(Bin) > ?MAX_SIZE ->
+ {Left, Right} = split(Bin, round(size(Bin) / 2)),
+ fix_cord(make_cord(Left, Right));
+ true ->
+ Bin
+ end;
+fix_cord(Cord) when Cord#cord.dirty == false ->
+ Cord;
+%% Branch (cord) - merge its children if they're too small, balance it
+%% if it's too unbalanced.
+fix_cord(Cord) when record(Cord, cord) ->
+ Sz = cord_size(Cord),
+ Left = Cord#cord.left,
+ Right = Cord#cord.right,
+ LeftSz = cord_size(Left),
+ RightSz = cord_size(Right),
+ SzDiff = abs(LeftSz - RightSz),
+ if Sz < ?MIN_SIZE ->
+ %% Too small - make it into a binary
+ to_binary(Cord);
+ SzDiff > (Sz/3) ->
+ %% needs rebalancing
+ if LeftSz > RightSz ->
+ if binary(Left) ->
+ fix_cord(to_binary(Cord));
+ true ->
+ balance_from_left(Cord)
+ end;
+ LeftSz =< RightSz ->
+ if binary(Right) ->
+ fix_cord(to_binary(Cord));
+ true ->
+ balance_from_right(Cord)
+ end
+ end;
+ true ->
+ %% this cord is ok, fix the children
+ Cord#cord{left=fix_cord(Left),
+ right=fix_cord(Right),
+ dirty=false}
+ end.
+
+%% Balance by taking from the left side.
+%% Left must be a #cord, right can be a binary.
+balance_from_left(#cord{left=Left, right=Right}) when record(Left, cord) ->
+ LLSz = cord_size(Left#cord.left),
+ LRSz = cord_size(Left#cord.right),
+ if
+ LRSz < LLSz ->
+ %% single rotate
+ fix_cord(make_cord(Left#cord.left,
+ make_cord(Left#cord.right,
+ Right)));
+ record(Left#cord.right, cord) ->
+ %% double rotate
+ LeftRight = Left#cord.right,
+ fix_cord(make_cord(make_cord(Left#cord.left,
+ LeftRight#cord.left),
+ make_cord(LeftRight#cord.right,
+ Right)));
+ true ->
+ fix_cord(to_binary(make_cord(Left, Right)))
+ end.
+
+%% oh, pain, duplication. never have been good at taking redundancy
+%% out of symmetric functions. -luke
+balance_from_right(#cord{left=Left, right=Right}) when record(Right, cord) ->
+ RLSz = cord_size(Right#cord.left),
+ RRSz = cord_size(Right#cord.right),
+ if
+ RLSz < RRSz ->
+ %% single rotate
+ fix_cord(make_cord(make_cord(Left,
+ Right#cord.left),
+ Right#cord.right));
+ record(Right#cord.left, cord) ->
+ %% double rotate
+ RightLeft = Right#cord.left,
+ fix_cord(make_cord(make_cord(Left,
+ RightLeft#cord.left),
+ make_cord(RightLeft#cord.right,
+ Right#cord.right)));
+ true ->
+ fix_cord(to_binary(make_cord(Left, Right)))
+ end.
+
+%% Return: cord()
+region(Cord, Start, Length) ->
+ {A, B} = split(Cord, Start-1),
+ {C, D} = split(B, Length),
+ C.
+
+%% Return: binary()
+region_binary(Cord, Start, Length) ->
+ to_binary(region(Cord, Start, Length)).
+
+%% Return: list()
+region_list(Cord, Start, Length) ->
+ binary_to_list(region_binary(Cord, Start, Length)).
+
+to_binary(Cord) when binary(Cord) ->
+ Cord;
+to_binary(Cord) ->
+ list_to_binary(to_binary1(Cord)).
+
+to_binary1(Cord) when binary(Cord) ->
+ Cord;
+to_binary1(Cord) when record(Cord, cord) ->
+ [to_binary1(Cord#cord.left),to_binary1(Cord#cord.right)].
+
+to_list(Cord) ->
+ binary_to_list(to_binary(Cord)).
+
+to_iolist(Cord) when binary(Cord) ->
+ [Cord];
+to_iolist(Cord) ->
+ [to_iolist(Cord#cord.left) | to_iolist(Cord#cord.right)].
+
+%% Walk backwards along a cord, character by character.
+%% F = fun(X) -> {more, F2} | {result, R}
+%% X = char() | finish
+walk(Cord, Pos, Direction, F) ->
+ %% Make this simple: extract the region we want to walk along.
+ Region = case Direction of
+ backward ->
+ {A, B} = split(Cord, Pos),
+ A;
+ forward ->
+ {A, B} = split(Cord, Pos-1),
+ B
+ end,
+ case walk1(Region, Direction, F) of
+ {result, R} ->
+ R;
+ {more, FNext} ->
+ {result, R} = FNext(finish),
+ R
+ end.
+
+walk1(<<>>, Direction, F) ->
+ {more, F};
+walk1(Bin, Direction, F) when binary(Bin) ->
+ {Chunk, Char} = case Direction of
+ backward ->
+ Sz = size(Bin) - 1,
+ <<Front:Sz/binary, Back>> = Bin,
+ {Front, Back};
+ forward ->
+ <<Front, Back/binary>> = Bin,
+ {Back, Front}
+ end,
+ case F(Char) of
+ {more, F2} ->
+ walk1(Chunk, Direction, F2);
+ {result, R} ->
+ {result, R}
+ end;
+walk1(Cord, Direction, F) when record(Cord, cord) ->
+ {First, Second} = case Direction of
+ backward ->
+ {Cord#cord.right, Cord#cord.left};
+ forward ->
+ {Cord#cord.left, Cord#cord.right}
+ end,
+ case walk1(First, Direction, F) of
+ {more, F2} ->
+ walk1(Second, Direction, F2);
+ {result, R} ->
+ {result, R}
+ end.
+
+walker(Cord) ->
+ walker(Cord, forward).
+
+walker(Cord, Direction) ->
+ walker(Cord, Direction, <<>>).
+
+walker(Cord, Direction, More) when binary(Cord) ->
+ {Cord, Direction, More};
+walker(Cord, forward, More) ->
+ walker(Cord#cord.left, forward, make_cord(Cord#cord.right, More));
+walker(Cord, backward, More) ->
+ walker(Cord#cord.right, backward, make_cord(More, Cord#cord.left)).
+
+walker_at_end({walked_to_end, _}) ->
+ true;
+walker_at_end({C,_,Rest}) ->
+ cord_size(C) + cord_size(Rest) == 0.
+
+walker_direction({walked_to_end, Direction}) -> Direction;
+walker_direction({_, Direction, _}) -> Direction.
+
+walker_next({walked_to_end, Direction}) ->
+ {done, {walked_to_end, Direction}};
+walker_next({<<>>, Direction, More}) ->
+ case cord_size(More) of
+ 0 ->
+ {done, {walked_to_end, Direction}};
+ _ ->
+ walker_next(walker(More, Direction))
+ end;
+walker_next({<<A, Chunk/binary>>, forward, More}) ->
+ {A, {Chunk, forward, More}};
+walker_next({Bin, backward, More}) ->
+ ChunkSz = size(Bin) - 1,
+ <<Chunk:ChunkSz/binary, A>> = Bin,
+ {A, {Chunk, backward, More}}.
+
+walker_push(done, Walker) ->
+ Walker;
+walker_push(X, {walked_to_end, Dir}) ->
+ {<<X>>, Dir, <<>>};
+walker_push(X, {Bin, forward, More}) ->
+ {<<X>>, forward, make_cord(Bin, More)};
+walker_push(X, {Bin, backward, More}) ->
+ {<<X>>, backward, make_cord(Bin, More)}.
+
+walker_test() ->
+ Cord = make_cord(make_cord(<<1,2>>, <<3>>),
+ make_cord(<<4,5,6>>, <<7,8,9>>)),
+ W = walker(Cord, forward),
+ walker_test_loop(W).
+
+walker_test_loop(W) ->
+ case walker_next(W) of
+ {done, _} ->
+ [];
+ {Ch, W2} ->
+ [Ch|walker_test_loop(W2)]
+ end.
+
+test() ->
+ %% Test of binary cords
+ BinCord = <<1, 2, 3, 4, 5>> ,
+ {BinCord, <<>>} = split(BinCord, size(BinCord)),
+ {<<>>, BinCord} = split(BinCord, 0),
+ {<<1, 2>>, <<3, 4, 5>>} = split(BinCord, 2),
+ %% Test of a simple cord
+ Cord1 = make_cord(<<1, 2>>, <<3, 4, 5>>),
+ {<<>>, <<1, 2, 3, 4, 5>>} = binsplit(Cord1, 0),
+ {<<1, 2>>, <<3, 4, 5>>} = binsplit(Cord1, 2),
+ {<<1, 2, 3>>, <<4, 5>>} = binsplit(Cord1, 3),
+ %% A less trivial cord
+ %% (spaces before commas are to workaround an erlang-mode indent problem)
+ A = <<1, 2>> ,
+ B = <<3, 4>> ,
+ C = <<5, 6>> ,
+ D = <<7, 8>> ,
+ E = <<9>> ,
+ Cord2 = make_cord(make_cord(A, B), make_cord(C, make_cord(D, E))),
+ {<<>>, <<1, 2, 3, 4, 5, 6, 7, 8, 9>>} = binsplit(Cord2, 0),
+ {<<1, 2, 3, 4, 5>>, <<6, 7, 8, 9>>} = binsplit(Cord2, 5),
+ %% Joining
+ BinCord2 = to_binary(Cord2),
+ %% Why does = fail but == work?
+ true = <<BinCord/binary, BinCord2/binary>> == binjoin(BinCord, Cord2),
+ true = <<BinCord2/binary, BinCord/binary>> == binjoin(Cord2, Cord1),
+ true = <<BinCord2/binary, BinCord2/binary>> == binjoin(Cord2, Cord2),
+ %% Test "walking"
+ [1,2,3,4,5] = walk(Cord2, 5, backward, walk_test([])),
+ [9, 8, 7, 6, 5] = walk(Cord2, 5, forward, walk_test([])),
+ %% Test some operations
+ <<1, 2, 3, 4>> = delete(Cord2, 5, 5),
+ <<1, 2, 3,
+ 1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 4, 5, 6, 7, 8, 9>> = cord:insert(Cord2, Cord2, 4),
+ ok.
+
+walk_test(Acc) ->
+ fun(finish) ->
+ {result, Acc};
+ (X) ->
+ {more, walk_test([X|Acc])}
+ end.
+
+test2() ->
+ A = <<1, 2, 3, 4>> ,
+ B = <<5, 6>> ,
+ C = <<>> ,
+ D = <<7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17>> ,
+ join(A, join(B, join(C, D))).
+
+binsplit(Cord, Pos) ->
+ {Left, Right} = split(Cord, Pos),
+ {to_binary(Left), to_binary(Right)}.
+
+binjoin(Left, Right) ->
+ to_binary(make_cord(Left, Right)).
+
+benchmark(File) ->
+ {ok, Cord} = cord:new_from_file(File),
+ Sz = cord_size(Cord),
+ random:seed(),
+ Randoms = [random:uniform(Sz) || _ <- lists:seq(1, 100)],
+ timer:tc(?MODULE, split_with_each, [Cord, Randoms, 10]).
+
+split_with_each(Cord, L, N) ->
+ split_with_each(Cord, L, L, N).
+
+split_with_each(Cord, _, _, 0) ->
+ ok;
+split_with_each(Cord, [H|T], L, N) ->
+ split(Cord, H),
+ split_with_each(Cord, T, L, N);
+split_with_each(Cord, [], L, N) ->
+ split_with_each(Cord, L, L, N-1).
+
+
diff --git a/src/cord_regexp.erl b/src/cord_regexp.erl
new file mode 100644
index 0000000..1d6bb4d
--- /dev/null
+++ b/src/cord_regexp.erl
@@ -0,0 +1,296 @@
+%%%----------------------------------------------------------------------
+%%% File : cord_regexp.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Regexp ops on cords
+%%% Created : 10 Mar 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(cord_regexp).
+-author('luke@bluetail.com').
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+first_match(RE, Cord) ->
+ first_match(RE, Cord, 1).
+
+first_match(RE, Cord, Pos) ->
+ first_match(RE, Cord, Pos, forward).
+
+%% Find the first match of RE in Cord starting from Pos.
+first_match(RE, Cord, Pos, Dir) ->
+ case first_match_c(RE, Cord, Pos, Dir) of
+ {{match, Start, Len}, WalkerOut} ->
+ {match, Start, Len};
+ X ->
+ X
+ end.
+
+first_match_c(RE, Cord) ->
+ first_match_c(RE, Cord, 1).
+
+first_match_c(RE, Cord, Pos) ->
+ first_match_c(RE, Cord, Pos, forward).
+
+first_match_c(RE, Cord, Pos, forward) ->
+ {_, Region} = cord:split(Cord, Pos-1),
+ first_match1(RE, cord:walker(Region, forward), Pos);
+
+first_match_c(RE, Cord, Pos, backward) ->
+ case regexp:parse(RE) of
+ {ok, REP} ->
+ {Region,_} = cord:split(Cord, Pos),
+ RREP = reverse(REP),
+ case first_match1(RREP, cord:walker(Region, backward), Pos) of
+ {{match, S, L}, C} ->
+ {{match, S, L}, C};
+ X ->
+ X
+ end;
+ {error, Rsn} ->
+ {error, Rsn}
+ end.
+
+continue_match(RE, {W, N}) ->
+ case first_match1(RE, W, N) of
+ {{match, S, L}, C} ->
+ {{match, S, L}, {C, S+L}};
+ X ->
+ X
+ end.
+
+first_match1(RE, W, Pos) when list(RE) ->
+ case regexp:parse(RE) of
+ {ok, REP} ->
+ first_match1(optimise(REP), W, Pos);
+ X = {error, Rsn} ->
+ X
+ end;
+first_match1(RE, Walker, Pos) ->
+ case cord:walker_at_end(Walker) of
+ true ->
+ nomatch;
+ false ->
+ Start = case cord:walker_direction(Walker) of
+ forward -> 1;
+ backward -> Pos
+ end,
+ case apply_regexp(RE, Start, Walker) of
+ nomatch ->
+ %% it would be more efficient to use Walker to get
+ %% a new continuation to recurse with
+ {_, W1} = cord:walker_next(Walker),
+ first_match1(RE, W1, advance(Pos, W1));
+ {match, EndPos, WalkerOut} ->
+ Len = case cord:walker_direction(Walker) of
+ forward -> EndPos;
+ backward -> Pos - EndPos - 1
+ end,
+ {{match, Pos, Len}, WalkerOut}
+ end
+ end.
+
+apply_regexp(RE, Pos, W) ->
+ {Ch, W1} = cord:walker_next(W),
+ re_apply(RE, [], Ch, Pos, W1).
+
+%% re_apply(RE, More, ThisChar, Pos, InputCont) => {match, Len} | nomatch
+
+%% FIXME: Handling of bos (^) and eos ($) need thinking about when it
+%% comes to backwards-searching. Right now, ^ will match the end when
+%% going backwards.
+
+re_apply(epsilon, More, Ch, P, C) ->
+ re_apply_more(More, P, push(Ch, C));
+re_apply(eos, More, done, P, C) ->
+ case cord:walker_direction(C) of
+ forward ->
+ re_apply_more(More, P, C);
+ backward ->
+ nomatch
+ end;
+re_apply(eos, More, $\n, P, C) ->
+ re_apply_more(More, P, push($\n, C)); % \n isn't consumed
+re_apply(eos, More, done, P, C) ->
+ case cord:walker_direction(C) of
+ forward -> re_apply_more(More, P, C);
+ backward -> nomatch
+ end;
+re_apply(bos, More, done, P, C) ->
+ case cord:walker_direction(C) of
+ forward -> nomatch;
+ backward -> re_apply_more(More, P, C)
+ end;
+re_apply(eos, _, done, _, _) ->
+ true;
+re_apply(eos, _, done, _, _) ->
+ true;
+re_apply({'or', RE1, RE2}, More, Ch, P, C) ->
+ re_apply_or({apply, RE1, More, Ch, P, C},
+ {apply, RE2, More, Ch, P, C});
+re_apply({concat, RE1, RE2}, More, Ch, P, C) ->
+ re_apply(RE1, [RE2|More], Ch, P, C);
+re_apply({kclosure, CE}, More, Ch, P, C) ->
+ re_apply_or({apply_more, More, P, push(Ch, C)},
+ {apply, CE, [{kclosure, CE}|More], Ch, P, C});
+re_apply({pclosure, CE}, More, Ch, P, C) ->
+ re_apply(CE, [{kclosure, CE}|More], Ch, P, C);
+re_apply({optional, CE}, More, Ch, P, C) ->
+ re_apply_or({apply_more, More, P, push(Ch, C)},
+ {apply, CE, More, Ch, P, C});
+re_apply(bos, More, Ch, 1, C) ->
+ case cord:walker_direction(C) of
+ forward -> re_apply_more(More, 1, push(Ch, C));
+ backward -> nomatch
+ end;
+re_apply(bos, More, $\n, P, C) ->
+ re_apply_more(More, 1, push($\n, C)); % \n isn't consumed
+re_apply({char_class, Cc}, More, Ch, P, C) ->
+ case in_char_class(Ch, Cc) of
+ true -> re_apply_more(More, advance(P, C), C);
+ false -> nomatch
+ end;
+re_apply({comp_class, Cc}, More, Ch, P, C) ->
+ case in_char_class(Ch, Cc) of
+ true -> nomatch;
+ false -> re_apply_more(More, advance(P, C), C)
+ end;
+re_apply(Ch, More, Ch, P, C) when integer(Ch) ->
+ re_apply_more(More, advance(P, C), C);
+re_apply(_, _, _, _, _) ->
+ nomatch.
+
+advance(Pos, Walker) ->
+ case cord:walker_direction(Walker) of
+ forward -> Pos + 1;
+ backward -> Pos - 1
+ end.
+
+re_apply_more([H|T], P, C) ->
+ {Next, C1} = cord:walker_next(C),
+ re_apply(H, T, Next, P, C1);
+re_apply_more([], P, C) ->
+ %% -1 isn't used in 'regexp' module, not sure what's different..
+ {match, P-1, C}.
+
+re_apply_or(A, B) ->
+ case re_apply_or_operand(A) of
+ nomatch ->
+ re_apply_or_operand(B);
+ X ->
+ X
+ end.
+
+re_apply_or_operand({apply_more, More, P, C}) ->
+ re_apply_more(More, P, C);
+re_apply_or_operand({apply, RE, More, Ch, P, C}) ->
+ re_apply(RE, More, Ch, P, C).
+
+
+% re_apply_or({match,P1}, {match,P2}) when P1 >= P2 -> {match,P1};
+% re_apply_or({match,P1}, {match,P2}) -> {match,P2};
+% re_apply_or(nomatch, R2) -> R2;
+% re_apply_or(R1, nomatch) -> R1.
+
+in_char_class(C, [{C1,C2}|Cc]) when C >= C1, C =< C2 -> true;
+in_char_class(C, [C|Cc]) -> true;
+in_char_class(C, [_|Cc]) -> in_char_class(C, Cc);
+in_char_class(C, []) -> false.
+
+push(Ch, Cont) ->
+ cord:walker_push(Ch, Cont).
+
+%% reverse(RE)
+%% Returns an equivalent regexp which takes its input in reverse order.
+%% RE should come from regexp:parse/1
+reverse({'or', A, B}) ->
+ {'or', reverse(B), reverse(A)};
+reverse({concat, A, B}) ->
+ {concat, reverse(B), reverse(A)};
+reverse({optional, RE}) ->
+ {optional, reverse(RE)};
+reverse({kclosure, RE}) ->
+ {kclosure, reverse(RE)};
+reverse({pclosure, RE}) ->
+ {pclosure, reverse(RE)};
+reverse(Other) ->
+ Other.
+
+%% Just trivial stuff for starters.
+%%
+%% Rearrange concat
+optimise({concat, {concat, A, B}, C}) ->
+ optimise({concat, A, {concat, B, C}});
+%% recurse
+optimise({'or', A, B}) ->
+ {'or', optimise(A), optimise(B)};
+optimise({kclosure, A}) ->
+ {kclosure, optimise(A)};
+optimise({pclosure, A}) ->
+ {pclosure, optimise(A)};
+optimise({optional, A}) ->
+ {optional, optimise(A)};
+optimise(X) ->
+ X.
+
+test_exprs() ->
+ ["foo",
+ "a|b",
+ "(foo)|(bar)",
+ "a+"].
+
+test_inputs() ->
+ ["foo", "a", "b", "c", "foo", "bar", "baz", "aaaaa"].
+
+test() ->
+ case [{Inp, RE} || RE <- test_exprs(), Inp <- test_inputs(),
+ not same(Inp, RE)] of
+ [] ->
+ ok;
+ %% BadOnes is where the match result was different. BUT!
+ %% sometimes that is OK. At time of writing, the code seems
+ %% correct but has some BadOnes.
+ BadOnes ->
+ lists:foreach(fun complain/1, BadOnes)
+ end.
+
+same(Inp, REStr) ->
+ forwards(Inp, REStr) == backwards(Inp, REStr).
+
+forwards(Inp, RE) ->
+ regexp:match(Inp, RE).
+
+backwards(Inp, REStr) ->
+ {ok, RE} = regexp:parse(REStr),
+ BackwardsResult = regexp:match(lists:reverse(Inp), reverse(RE)).
+
+complain({Inp, REStr}) ->
+ {ok, RE} = regexp:parse(REStr),
+ ForwardsResult = regexp:match(Inp, RE),
+ BackwardsResult = regexp:match(lists:reverse(Inp), reverse(RE)),
+ io:format("match(~p, ~p):~n Fwds = ~p~n Bwds = ~p~n",
+ [Inp, REStr, ForwardsResult, BackwardsResult]).
+
+%% Compare speed of this module on a cord to the speed of the 'regexp'
+%% module on a string.
+bench(Regexp, Cord) ->
+ List = cord:to_list(Cord),
+ {CordSpeed,_} = timer:tc(?MODULE, first_match, [Regexp, Cord]),
+ {ListSpeed, _} = timer:tc(regexp, first_match, [List, Regexp]),
+ io:format("Cord takes ~p% time of List (~pms vs ~pms).~n",
+ [round(CordSpeed * 100 / ListSpeed),
+ round(CordSpeed/1000),
+ round(ListSpeed/1000)]).
+
+escape([H|T]) ->
+ case lists:member(H, specials()) of
+ true ->
+ [$\\,H|escape(T)];
+ false ->
+ [H|escape(T)]
+ end;
+escape([]) ->
+ [].
+
+specials() -> "()^$[]*+?.\\|".
+
diff --git a/src/edit.erl b/src/edit.erl
new file mode 100644
index 0000000..a1e0cbf
--- /dev/null
+++ b/src/edit.erl
@@ -0,0 +1,264 @@
+%%%----------------------------------------------------------------------
+%%% File : edit.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Main editor process.
+%%%
+%%% Grown out of Tobbe's 'edit' program, and slowly rewritten.
+%%%----------------------------------------------------------------------
+-module(edit).
+-author('luke@bluetail.com').
+-export([start/0]).
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+
+%% Command-line entry function. Starts the editor.
+start() ->
+ init().
+
+%% Another command-line entry function. Starts the editor and loads
+%% some files.
+start(Args) ->
+ %% Easy/hacky way - asynchronously ask that all the files be
+ %% loaded. When this process is initialised it'll see the
+ %% messages.
+ Filenames = lists:map(fun(X) -> atom_to_list(X) end,
+ Args),
+ lists:foreach(fun(Filename) ->
+ self() ! {invoke, {edit_file, find_file, [Filename]}}
+ end,
+ Filenames),
+ start().
+
+%% Another command-line entry function. Starts the editor with some
+%% modules loaded for debugging.
+debug() ->
+ lists:foreach(fun(Mod) -> i:ii(Mod) end, debug_modules()),
+ i:im(),
+ sleep(1000),
+ proc_lib:start_link(?MODULE, start, []).
+
+debug_modules() ->
+ [edit_display, edit_lib, ?EDIT_TERMINAL, edit_keymap, edit_buf,
+ edit_extended, edit_file, cord, edit_eval, edit_util, edit_text].
+
+%% ----------------------------------------------------------------------
+%% API program
+
+invoke_async(M, F, A) ->
+ edit ! {invoke, {M, F, A}}.
+
+invoke_async(Fn) ->
+ edit ! {invoke, Fn}.
+
+invoke_extended_async(M, F, A) ->
+ edit ! {invoke_extended, {M, F, A}}.
+
+get_key() ->
+ edit ! {want_key, self()},
+ receive {key_input, Ch} -> Ch end.
+
+%% ----------------------------------------------------------------------
+%% Turn this process into a lean, mean, editing machine
+init() ->
+ register(?MODULE, self()),
+ init_io_traps(),
+ process_flag(trap_exit, true),
+ %% Initialize editor
+ edit_keymap:start_link_server(),
+ edit_globalmap:init(),
+ ?EDIT_TERMINAL:setup(),
+ edit_input:start_link(self()),
+ init_vars(),
+ init_buffers(),
+ init_mods(),
+ State = init_windows(#state{}),
+ State1 = load_dot_ermacs(State),
+ State2 = redraw(State1),
+ io:format("INIT: ~p~n", [edit_mod:require(em_stdlib)]),
+ %%profile(State2),
+ loop(State2).
+
+init_io_traps() ->
+ {ok, Leader} = file_gl:start_link("/tmp/edit.out"),
+ group_leader(Leader, self()),
+ error_logger:tty(false).
+
+%% Setup initial buffers (scratch and minibuffer)
+init_buffers() ->
+ edit_buf:new('*scratch*'),
+ edit_buf:new(minibuffer),
+ MBMode = #mode{name="Minibuffer",
+ id=minibuffer,
+ keymaps=[minibuffer_map]},
+ edit_buf:set_mode(minibuffer, MBMode).
+
+%% Setup initial windows
+init_windows(State) ->
+ Width = ?EDIT_TERMINAL:width(),
+ Height = ?EDIT_TERMINAL:height(),
+ ScratchWin = edit_window:make_window('*scratch*',
+ 0,
+ Width,
+ Height - 1),
+ MiniWin1 = edit_window:make_window(minibuffer,
+ Height - 1,
+ Width,
+ 1),
+ MiniWin2 = MiniWin1#window{active=false,
+ minibuffer=true},
+ State#state{curwin=ScratchWin,
+ buffers=['*scratch*'],
+ windows=[MiniWin2]}.
+
+init_minibuffer(State) ->
+ edit_buf:new(minibuffer),
+ State.
+
+init_vars() ->
+ edit_var:start_link(),
+ edit_var:set(killring, []).
+
+init_mods() ->
+ edit_mod:init(),
+ %% special-case explicit initialisations for the core stuffs
+ edit_file:mod_init(),
+ ok.
+
+load_dot_ermacs(State) ->
+ Filename = filename:join(os:getenv("HOME"), ".ermacs"),
+ case file:read_file_info(Filename) of
+ {error, _} ->
+ edit_util:status_msg(State, "No ~~/.ermacs to read.");
+ {ok, _} ->
+ case catch file:eval(Filename) of
+ ok ->
+ State;
+ {error, Rsn} ->
+ edit_util:status_msg(State, "~/.ermacs failed: ~p", [Rsn])
+ end
+ end.
+
+%% -------------
+%% The Main Loop
+
+loop(S) ->
+ State = S#state{curwin=edit_display:draw_window(S#state.curwin)},
+ NewState = dispatch(State),
+ ?MODULE:loop(redraw(NewState)).
+
+redraw(State) ->
+ Wins = [edit_display:draw_window(W) || W <- State#state.windows],
+ lists:foreach(fun(W) -> edit_display:draw_window(W) end,
+ State#state.windows),
+ Cur = edit_display:draw_window(State#state.curwin),
+ ?EDIT_TERMINAL:refresh(),
+ State#state{curwin=Cur,
+ windows=Wins}.
+
+%% Dispatch a command, based on the next message we receive.
+dispatch(State) ->
+ Buf = (State#state.curwin)#window.buffer,
+ Keymaps = (edit_buf:get_mode(Buf))#mode.keymaps ++ [global_map],
+ receive
+ {invoke, {M, F, A}} ->
+ dispatch_proc(State, fun() -> apply(M, F, [State | A]) end);
+ {invoke, Fun} when function(Fun) ->
+ dispatch_proc(State, fun() -> Fun(State) end);
+ {invoke_extended, {Mod, Func, Args}} ->
+ dispatch_extended(State, Mod, Func, Args);
+ {key_input, Ch} ->
+ case find_cmd(State, Keymaps, Ch) of
+ unbound ->
+ edit_util:status_msg(State, "Unbound key");
+ {Mod, Func, Args} ->
+ dispatch_extended(State, Mod, Func, Args);
+ Other ->
+ edit_util:status_msg(State,"Bad binding: ~p~n",[Other])
+ end;
+ {'EXIT', _Someone, _SomeReason} ->
+ dispatch(State);
+ Other ->
+ io:format("Unexpected message: ~p~n", [Other])
+ end.
+
+dispatch_extended(State, Mod, Func, Args) ->
+ F = fun() -> edit_extended:extended_command(State, Mod, Func, Args) end,
+ dispatch_proc(State, F).
+
+%% ----------------------------------------------------------------------
+%% Dispatch a command in a new process. The process gets aborted if
+%% the user presses C-g.
+dispatch_proc(State, CommandFun) ->
+ Self = self(),
+ F = fun() ->
+ Result = CommandFun(),
+ Self ! {result, self(), Result}
+ end,
+ Pid = spawn_link(F),
+ dispatch_loop(State, Pid, false).
+
+dispatch_loop(State, Pid, WantKey) ->
+ receive
+ {result, Pid, Result} ->
+ Result;
+ {key_input, $\^G} ->
+ exit(Pid, user_abort),
+ edit_util:status_msg(State, "Abort");
+ {key_input, Ch} when WantKey == true ->
+ Pid ! {key_input, Ch},
+ dispatch_loop(State, Pid, false);
+ {want_key, Pid} when WantKey == false ->
+ dispatch_loop(State, Pid, true);
+ {'EXIT', Pid, Reason} ->
+ io:format("DE: ~p~n", [Reason]),
+ edit_util:status_msg(State,"Dispatch error: ~p~n",[Reason])
+ end.
+
+
+%% ----------------------------------------------------------------------
+%% Keymap lookup
+
+find_cmd(State, Keymaps) ->
+ Ch = get_char(),
+ find_cmd(State, Keymaps, Ch).
+
+find_cmd(State, [], Ch) ->
+ unbound;
+find_cmd(State, [Keymap|Keymaps], Ch) ->
+ case edit_keymap:lookup(Keymap, Ch) of
+ {ok, {keymap, NewMap}} ->
+ find_cmd(State, [NewMap]);
+ {ok, Cmd} ->
+ Cmd;
+ unbound ->
+ find_cmd(State, Keymaps, Ch)
+ end.
+
+get_char() ->
+ receive {key_input, C} -> C end.
+
+sleep(T) -> receive after T -> true end.
+
+
+%% ----------------------------------------------------------------------
+%% Profiling
+
+profile(State) ->
+ receive after 100 -> ok end,
+ Procs = [edit|State#state.buffers],
+ timer:start_link(),
+ spawn_link(fun() ->
+ analyse_loop(Procs)
+ end).
+
+analyse_loop(Procs) ->
+ eprof:start(),
+ profiling = eprof:profile(Procs),
+ receive after 15000 ->
+ eprof:total_analyse()
+ end,
+ analyse_loop(Procs).
+
+
diff --git a/src/edit_bench.erl b/src/edit_bench.erl
new file mode 100644
index 0000000..1c40925
--- /dev/null
+++ b/src/edit_bench.erl
@@ -0,0 +1,32 @@
+%%%-------------------------------------------------------------------
+%%% File : edit_bench.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Random benchmarking
+%%%
+%%% Created : 29 Sep 2001 by Luke Gorrie <luke@bluetail.com>
+%%%-------------------------------------------------------------------
+-module(edit_bench).
+
+-compile(export_all).
+
+%% Testing the speed of sending (large) cords in messages
+
+cord_bench(Filename, N) ->
+ {ok, Cord} = cord:new_from_file(Filename),
+ Pid = spawn_link(?MODULE, cord_receiver, []),
+ timer:tc(?MODULE, cord_bench_loop, [Cord, Pid, N]).
+
+cord_bench_loop(Cord, Pid, 0) ->
+ exit(Pid, kill),
+ ok;
+cord_bench_loop(Cord, Pid, N) when N > 0 ->
+ Pid ! {cord, self(), Cord},
+ receive ack -> ok end,
+ cord_bench_loop(Cord, Pid, N-1).
+
+cord_receiver() ->
+ receive {cord, Who, Cord} -> Who ! ack end,
+ cord_receiver().
+
+
+
diff --git a/src/edit_buf.erl b/src/edit_buf.erl
new file mode 100644
index 0000000..7d89741
--- /dev/null
+++ b/src/edit_buf.erl
@@ -0,0 +1,320 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_buf.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Buffer process
+%%% Created : 14 Sep 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_buf).
+-author('luke@bluetail.com').
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+-record(state, {name,
+ filename, % optional
+ text, % text()
+ mode,
+ borrower=nobody % for a lock pid()
+ }).
+
+%% ----------------------------------------------------------------------
+%% API
+%% ----------------------------------------------------------------------
+
+new(Name) ->
+ case start_link(Name) of
+ {ok, B} ->
+ add_mark(Name, point, 1, forward),
+ add_mark(Name, mark, 1, backward),
+ {ok, Name};
+ Err ->
+ Err
+ end.
+
+start_link(Name) when atom(Name) ->
+ case whereis(Name) of
+ undefined ->
+ Pid = proc_lib:spawn_link(?MODULE, init, [Name]),
+ register(Name, Pid),
+ {ok, Pid};
+ Pid ->
+ {error, {already_started, Pid}}
+ end.
+
+kill(Buf) ->
+ call(Buf, kill).
+
+set_filename(Buf, Filename) ->
+ call(Buf, {set_filename, Filename}).
+
+get_filename(Buf) ->
+ call(Buf, get_filename).
+
+set_mode(Buf, Mode) ->
+ call(Buf, {set_mode, Mode}).
+
+get_mode(Buf) ->
+ call(Buf, get_mode).
+
+get_text(Buf) ->
+ call(Buf, get_text).
+
+set_text(Buf, Text) ->
+ call(Buf, {set_text, Text}).
+
+get_cord(Buf) ->
+ call(Buf, get_cord).
+
+get_size(Buf) ->
+ call(Buf, get_size).
+
+replace(Buf, New, Start, End) when Start > End ->
+ replace(Buf, New, End, Start);
+replace(Buf, New, Start, End) ->
+ call(Buf, {replace, New, Start, End}).
+
+get_region(Buf, Start, End) ->
+ cord:to_list(get_region_cord(Buf, Start, End)).
+
+get_region_cord(Buf, Start, End) when Start > End ->
+ get_region_cord(Buf, End, Start);
+get_region_cord(Buf, Start, End) ->
+ call(Buf, {get_region_cord, Start, End}).
+
+undo(Buf, Continuing) ->
+ call(Buf, {undo, Continuing}).
+
+delete(Buf, Start, End) ->
+ replace(Buf, "", Start, End).
+
+insert(Buf, New, Position) ->
+ replace(Buf, New, Position, Position).
+
+add_mark(Buf, Name, Pos, Direction) ->
+ call(Buf, {add_mark, Name, Pos, Direction}).
+
+move_mark(Buf, Name, Pos) ->
+ call(Buf, {move_mark, Name, Pos}).
+
+mark_pos(Buf, Name) ->
+ call(Buf, {mark_pos, Name}).
+
+point_max(Buf) ->
+ call(Buf, point_max).
+
+point_min(Buf) ->
+ 1.
+
+walk_backward(Buf, Fun, Pos) ->
+ call(Buf, {walk_backward, Fun, Pos}).
+
+walk_forward(Buf, Fun, Pos) ->
+ call(Buf, {walk_forward, Fun, Pos}).
+
+walk(Buf, Direction, Fun, Pos) ->
+ call(Buf, {walk, Direction, Fun, Pos}).
+
+wait_available(Buf) ->
+ call(Buf, wait_available).
+
+borrow(Buf) ->
+ call(Buf, {borrow, self()}).
+
+return(Buf) ->
+ call(Buf, {return, self()}).
+
+revoke(Buf) ->
+ call(Buf, revoke).
+
+regexp_search(Buf, RE, Pos, Direction) ->
+ call(Buf, {regexp_search, RE, Pos, Direction}).
+
+%% Asynchronous locking
+
+%% Returns: ref()
+async_borrow(Buf) ->
+ Ref = make_ref(),
+ cast(Buf, {borrow, self()}).
+
+async_return(Buf) ->
+ cast(Buf, {return, self()}).
+
+%% Internals
+
+call(Buf, Msg) ->
+ Buf ! {call, self(), Msg},
+ receive {reply, R} -> R end.
+
+cast(Buf, Msg) ->
+ Buf ! {cast, Msg},
+ ok.
+
+init(Name) ->
+ {'EXIT', Reason} = (loop(#state{name=Name,
+ mode=edit_lib:fundamental_mode_rec(),
+ text=edit_text:new()})),
+ io:format("Buffer ~p crashed: ~p~n", [Name, Reason]),
+ exit(Reason).
+
+loop(State) ->
+ receive
+ %% get_text
+ {call, From, get_text} ->
+ From ! {reply, cord:to_list(edit_text:cord(State#state.text))},
+ edit_buf:loop(State);
+ %% set_text
+ {call, From, {set_text, Text}} ->
+ Cord = edit_text:cord(State#state.text),
+ NewCmd = {replace, Text, 1, cord:cord_size(Cord) + 1},
+ self() ! {call, From, NewCmd},
+ edit_buf:loop(State);
+ %% get_cord
+ {call, From, get_cord} ->
+ From ! {reply, edit_text:cord(State#state.text)},
+ edit_buf:loop(State);
+ %% get_size
+ {call, From, get_size} ->
+ From ! {reply, cord:cord_size(edit_text:cord(State#state.text))},
+ edit_buf:loop(State);
+ %% set_filename
+ {call, From, {set_filename, Filename}} ->
+ NewState = State#state{filename=Filename},
+ From ! {reply, ok},
+ edit_buf:loop(NewState);
+ %% get_filename
+ {call, From, get_filename} ->
+ From ! {reply, State#state.filename},
+ edit_buf:loop(State);
+ %% set_mode
+ {call, From, {set_mode, Mode}} ->
+ From ! {reply, ok},
+ edit_buf:loop(State#state{mode=Mode});
+ %% get_mode
+ {call, From, get_mode} ->
+ From ! {reply, State#state.mode},
+ edit_buf:loop(State);
+ %% replace
+ {call, From, {replace, Text, Start, End}} ->
+ NewText = edit_text:replace(State#state.text,Text,Start,End-Start),
+ From ! {reply, ok},
+ edit_buf:loop(State#state{text=NewText});
+
+% NewText = cord:replace(State#state.text, Text, Start, End-Start),
+% Marks1 = update_marks(State#state.marks, Text, Start, End),
+% State2 = State#state{text=NewText, marks=Marks1},
+% Undo = [{State#state.text, State#state.marks}|State2#state.undo],
+% State3 = State2#state{undo=Undo},
+% From ! {reply, ok},
+% edit_buf:loop(State3);
+ %% undo
+ {call, From, {undo, Continuing}} ->
+ NewText = edit_text:undo(State#state.text, Continuing),
+ From ! {reply, ok},
+ edit_buf:loop(State#state{text=NewText});
+
+% NewState = do_undo(State, Continuing),
+% From ! {reply, ok},
+% edit_buf:loop(NewState);
+ %% get_region
+ {call, From, {get_region_cord, Start, End}} ->
+ Cord = edit_text:cord(State#state.text),
+ Text = cord:region(Cord, Start, End-Start),
+ From ! {reply, Text},
+ edit_buf:loop(State);
+ %% add_mark
+ {call, From, {add_mark, Name, Pos, Direction}} ->
+ NewText = edit_text:add_mark(State#state.text,Name,Pos,Direction),
+ From ! {reply, ok},
+ edit_buf:loop(State#state{text=NewText});
+ %% move_mark
+ {call, From, {move_mark, Name, Pos}} ->
+ NewText = edit_text:move_mark(State#state.text, Name, Pos),
+ From ! {reply, ok},
+ edit_buf:loop(State#state{text=NewText});
+ %% mark_pos
+ {call, From, {mark_pos, Name}} ->
+ From ! {reply, edit_text:mark_pos(State#state.text, Name)},
+ edit_buf:loop(State);
+ %% point_max
+ {call, From, point_max} ->
+ Cord = edit_text:cord(State#state.text),
+ From ! {reply, cord:cord_size(Cord) + 1},
+ edit_buf:loop(State);
+ %% walk_backward
+ {call, From, {walk_backward, Fun, Pos}} ->
+ From ! {reply, edit_text:walk_backward(State#state.text,Fun,Pos)},
+ edit_buf:loop(State);
+ %% walk_forward
+ {call, From, {walk_forward, Fun, Pos}} ->
+ From ! {reply, edit_text:walk_forward(State#state.text,Fun,Pos)},
+ edit_buf:loop(State);
+ %% walk
+ {call, From, {walk, Direction, Fun, Pos}} ->
+ Result = case Direction of
+ forward ->
+ edit_text:walk_forward(State#state.text,Fun,Pos);
+ backward ->
+ edit_text:walk_backward(State#state.text,Fun,Pos)
+ end,
+ From ! {reply, Result},
+ edit_buf:loop(State);
+ %% wait_available
+ {call, From, wait_available} when State#state.borrower == nobody ->
+ From ! {reply, true},
+ edit_buf:loop(State);
+ %% borrow
+ {call, From, {borrow, Borrower}} when State#state.borrower == nobody ->
+ From ! {reply, true},
+ io:format("Borrowing buffer for ~p at ~s~n", [Borrower,
+ os:cmd("date")]),
+ %% monitor the borrower so we know when to release
+ erlang:monitor(process, Borrower),
+ edit_buf:loop(State#state{borrower=Borrower});
+ %% return
+ {call, From, {return, Who}} when Who == State#state.borrower ->
+ edit_buf:loop(State#state{borrower=nobody});
+ {'DOWN', _, process, Who, _} when Who == State#state.borrower ->
+ edit_buf:loop(State#state{borrower=nobody});
+ %% revoke
+ {call, From, revoke} ->
+ case State#state.borrower of
+ nobody ->
+ From ! {reply, false},
+ true;
+ Who ->
+ From ! {reply, true},
+ exit(Who, buffer_revoked)
+ end,
+ edit_buf:loop(State#state{borrower=nobody});
+ %% regexp_search
+ {call, From, {regexp_search, RE, Pos, Direction}} ->
+ Cord = edit_text:cord(State#state.text),
+ From ! {reply, cord_regexp:first_match(RE, Cord, Pos, Direction)},
+ edit_buf:loop(State);
+ %% kill
+ {call, From, kill} ->
+ From ! {reply, ok};
+ %% ---------------------------------------------------------------
+ %% Casts
+
+ %% {borrow, Who} --> {loan, Buffer}
+ {cast, {borrow, Who}} when State#state.borrower == nobody ->
+ Who ! {loan, State#state.name},
+ erlang:monitor(process, Who),
+ loop(State#state{borrower=Who});
+ %% {return, ByWho} --> void
+ {cast, {return, Who}} when Who == State#state.borrower ->
+ loop(State#state{borrower=nobody})
+ end.
+
+%% Debug
+
+log(FmtString, Args) ->
+ log(io_lib:format(FmtString, Args)).
+
+log(String) ->
+ {ok, Fd} = file:open("edit_buf.log", [append, raw]),
+ file:write(Fd, String),
+ file:close(Fd).
+
diff --git a/src/edit_complete.erl b/src/edit_complete.erl
new file mode 100644
index 0000000..b955574
--- /dev/null
+++ b/src/edit_complete.erl
@@ -0,0 +1,170 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_complete.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Minibuffer completion.
+%%% Created : 26 Mar 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_complete).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+-import(edit_lib, [buffer/1]).
+
+-define(completions_buffer, '*Completions*').
+
+%% Variables:
+%% completion_state = initial | ambiguous | showing | nomatch
+%% completion_type = file | boring_atom()
+%% completion_prev_path = string()
+completion_init(Type) ->
+ edit_var:set(completion_state, fresh),
+ edit_var:set(completion_prev_path, ""),
+ edit_var:set(completion_type, Type),
+ ok.
+
+complete(State) ->
+ complete(State, edit_var:lookup(completion_type)).
+
+complete(State, file) ->
+ MBuf = buffer(State),
+ Path = edit_buf:get_text(MBuf),
+ FSMState = case edit_var:lookup(completion_prev_path) of
+ Path ->
+ edit_var:lookup(completion_state);
+ _ ->
+ %% Different path, reset fsm
+ initial
+ end,
+ Result = filename_complete(Path),
+ {State1, NewFSMState, NewPath} =
+ do_complete(State, FSMState, Path, Result),
+ edit_buf:set_text(MBuf, NewPath),
+ edit_var:set(completion_state, NewFSMState),
+ edit_var:set(completion_prev_path, NewPath),
+ State1;
+complete(State, _) ->
+ State.
+
+do_complete(EditState, FSMState, Path, Result) ->
+ {NextFSMState, Action} = complete_fsm(FSMState, Result),
+ {EditState1, NewPath} = action(EditState, Path, Action),
+ {EditState1, NextFSMState, NewPath}.
+
+%% complete_fsm(State, CompletionResult) => {NextState, Action}
+%%
+%% State = initial | ambiguous | showing | nomatch
+%% CompletionResult = {unique, Path} | {completions, Path, List} | nomatch
+%% Action = nop | scroll | {show, List} | {rewrite, Path}
+
+%% FIXME: Add transitions to allow for files being created/deleted
+%% between inputs.
+
+complete_fsm(initial, {unique, New}) ->
+ {initial, {rewrite, New}};
+complete_fsm(initial, {completions, New, List}) ->
+ {ambiguous, {rewrite, New}};
+complete_fsm(initial, nomatch) ->
+ {nomatch, nop};
+
+complete_fsm(ambiguous, {completions, New, List}) ->
+ {showing, {show, List}};
+
+complete_fsm(showing, {completions, New, List}) ->
+ {showing, scroll};
+
+complete_fsm(nomatch, nomatch) ->
+ {nomatch, nop};
+
+%% catch all for unexpected cases
+complete_fsm(_, _) ->
+ {nomatch, nop}.
+
+%% action(State, Path, Action) => {State', Path'}
+action(State, Path, nop) ->
+ {State, Path};
+action(State, Path, scroll) ->
+ {edit_util:window_map(State, fun maybe_complete_scroll/1), Path};
+action(State, Path, {show, List}) ->
+ {NewState, Buf} = make_completion_buffer(State, List),
+ {edit_util:popup_buffer(NewState, Buf), Path};
+action(State, Path, {rewrite, NewPath}) ->
+ {State, NewPath}.
+
+make_completion_buffer(St, AbsStrings) ->
+ BaseStrings = lists:map(fun(X) -> filename:basename(X) end,
+ AbsStrings),
+ Strings = lists:sort(BaseStrings),
+ Name = ?completions_buffer,
+ edit_buf:new(Name),
+ edit_buf:set_text(Name, ""),
+ lists:foreach(fun(String) ->
+ P = edit_buf:mark_pos(Name, point),
+ edit_buf:insert(Name, String ++ "\n", P)
+ end,
+ Strings),
+ edit_buf:move_mark(Name, point, 1),
+ St1 = case lists:member(Name, St#state.buffers) of
+ true ->
+ St;
+ false ->
+ St#state{buffers=[Name|St#state.buffers]}
+ end,
+ {St1, Name}.
+
+maybe_complete_scroll(Win) when Win#window.buffer == ?completions_buffer ->
+ edit_lib:scroll_down_wrap(Win);
+maybe_complete_scroll(Win) ->
+ Win.
+
+%% Returns: {unique, Complete}
+%% | {completions, LongestPrefix, [string()]}
+%% | nomatch
+filename_complete(Path) ->
+ Dir = filename:dirname(Path),
+ case file:list_dir(Dir) of
+ {ok, BaseNames} ->
+ Names1 = lists:map(fun(X) -> filename:join(Dir, X) end,
+ BaseNames),
+ Names2 = lists:map(fun dirify/1, Names1),
+ string_complete(Path, Names2);
+ Err ->
+ nomatch
+ end.
+
+%% Append a "/" to directory names if they don't already have them.
+dirify(Path) ->
+ case file:read_file_info(Path) of
+ {ok, Inf} when Inf#file_info.type == directory ->
+ case lists:last(Path) of
+ $/ ->
+ Path;
+ _ ->
+ Path ++ "/"
+ end;
+ _ ->
+ Path
+ end.
+
+string_complete(In, L) ->
+ case lists:filter(fun(X) -> lists:prefix(In, X) end, L) of
+ [C] ->
+ {unique, C};
+ [] ->
+ nomatch;
+ Completions ->
+ {completions, longest_prefix(Completions), Completions}
+ end.
+
+longest_prefix([]) -> [];
+longest_prefix([A, B | T]) -> longest_prefix([longest_prefix1(A, B) | T]);
+longest_prefix([X]) -> X.
+
+longest_prefix1([X|T1], [X|T2]) -> [X|longest_prefix1(T1, T2)];
+longest_prefix1(_, _) -> [].
+
diff --git a/src/edit_display.erl b/src/edit_display.erl
new file mode 100644
index 0000000..e732c62
--- /dev/null
+++ b/src/edit_display.erl
@@ -0,0 +1,154 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_display.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Editor display process: talks to curses
+%%% Created : 16 Sep 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_display).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+draw_window(Window) when Window#window.minibuffer == true,
+ Window#window.status_text /= undefined ->
+ ?EDIT_TERMINAL:move_to(0, Window#window.y),
+ draw_line(Window#window.status_text),
+ Window#window{status_text=undefined};
+draw_window(Window) ->
+ try_update(Window).
+
+try_update(Window) ->
+ Buf = Window#window.buffer,
+ PointMax = edit_buf:point_max(Buf),
+ DStart = edit_buf:mark_pos(Buf, Window#window.start_mark),
+ Scan = edit_lib:beginning_of_line_pos(Buf, DStart),
+ Point = edit_buf:mark_pos(Buf, point),
+ Chars = (?EDIT_TERMINAL:width() * ?EDIT_TERMINAL:height()) * 4, %% FIXME
+ Text = edit_buf:get_region(Buf, Scan, min(PointMax, Scan + Chars)),
+ ?EDIT_TERMINAL:move_to(0, Window#window.y),
+ Rows = edit_window:text_lines(Window),
+ Prefix = Window#window.prefix,
+ PLen = length(Prefix),
+ PAcc = lists:reverse(Prefix),
+ case try_update_loop(Text,Rows,Scan,PLen,0,Point,undefined,PAcc) of
+ {X, Y} ->
+ %% draw mode line
+ draw_modeline(Window),
+ TrimX = edit_lib:min(X, Window#window.width - 1),
+ ?EDIT_TERMINAL:move_to(TrimX, Y + Window#window.y),
+ Window;
+ undefined ->
+ %% The point wasn't inside the area we drew, so we
+ %% recenter the display with the point in the middle and
+ %% then draw again.
+ try_update(recenter_window(Window))
+ end.
+
+%% Returns the location of the point in a tuple {X, Y}, or undefined
+%% if it wasn't in the area drawn.
+
+try_update_loop(Text, NRows, Scan, Col, Row, Point, PointXY, Acc)
+ when Scan == Point,
+ PointXY == undefined ->
+ try_update_loop(Text,NRows,Scan,Col,Row,Point,{Col, Row},Acc);
+try_update_loop([$\n|T], NRows, Scan, Col, Row, Point, PointXY, Acc) ->
+ draw_line(lists:reverse(Acc)),
+ ?EDIT_TERMINAL:newline(),
+ NextRow = Row+1,
+ if NextRow == NRows ->
+ PointXY;
+ true ->
+ try_update_loop(T,NRows,Scan+1,0,Row+1,Point,PointXY, [])
+ end;
+try_update_loop([$\t|T], NRows, Scan, Col, Row, Point, PointXY, Acc) ->
+ Size = 8 - (Col rem 8),
+ Tab = lists:duplicate(Size, $ ),
+ try_update_loop(T,NRows,Scan+1,Col+Size,Row,Point,PointXY,Tab++Acc);
+try_update_loop([H|T], NRows, Scan, Col, Row, Point, PointXY, Acc) ->
+ try_update_loop(T,NRows,Scan+1,Col+1,Row,Point,PointXY,[H|Acc]);
+try_update_loop([], NRows, Scan, Col, Row, Point, PointXY, Acc) ->
+ draw_line(lists:reverse(Acc)),
+ RemainingRows = NRows - Row,
+ %% draw empty lines until the end
+ dotimes(fun() -> draw_line([]),
+ ?EDIT_TERMINAL:newline()
+ end,
+ RemainingRows),
+ PointXY.
+
+draw_line(L) ->
+ Wth = ?EDIT_TERMINAL:width(),
+ Str = trunc_line(L, Wth),
+ ?EDIT_TERMINAL:put_string(L),
+ ?EDIT_TERMINAL:erase_to_eol().
+
+trunc_line([H], 1) -> [H];
+trunc_line(_, 1) -> [$$];
+trunc_line([H|T], N) -> [H|trunc_line(T, N-1)];
+trunc_line([], _) -> [].
+
+draw_modeline(Window) when Window#window.minibuffer == true ->
+ ok;
+draw_modeline(Window) ->
+ Buffer = Window#window.buffer,
+ Where = modeline_where(Window, Buffer),
+ Text = lists:flatten(
+ io_lib:format("--:?? ~s (~s) ~s",
+ [atom_to_list(Buffer),
+ (edit_buf:get_mode(Buffer))#mode.name,
+ Where])),
+ ?EDIT_TERMINAL:font_reverse(),
+ ?EDIT_TERMINAL:move_to(0, Window#window.y +
+ edit_window:physical_lines(Window) - 1),
+ draw_line(Text),
+ ?EDIT_TERMINAL:font_normal().
+
+modeline_where(Window, Buffer) ->
+ case edit_buf:get_size(Buffer) of
+ 0 ->
+ "ALL";
+ BSize ->
+ Start = edit_buf:mark_pos(Buffer, Window#window.start_mark),
+ Percentage = trunc(Start * 100 / BSize),
+ io_lib:format("~p%", [Percentage])
+ end.
+
+%% Update the display_start of a window so that it presents the point
+%% in the middle of the screen.
+recenter_window(Window) ->
+ Buf = Window#window.buffer,
+ Height = edit_window:text_lines(Window),
+ Pos = backward_lines(Buf, trunc(Height / 2)),
+ edit_buf:move_mark(Buf, Window#window.start_mark, Pos),
+ Window.
+
+backward_lines(Buf, N) ->
+ StartPos = edit_lib:beginning_of_line_pos(Buf),
+ edit_buf:walk_backward(Buf,
+ fun(X) -> back_lines(X, N, StartPos) end,
+ StartPos).
+
+back_lines(finish, N, Pos) ->
+ {result, 1};
+back_lines($\n, N, Pos) ->
+ if
+ N == 1 ->
+ {result, Pos};
+ true ->
+ {more, fun(New) -> back_lines(New, N-1, Pos-1) end}
+ end;
+back_lines(_, N, Pos) ->
+ {more, fun(New) -> back_lines(New, N, Pos-1) end}.
+
+dotimes(Fun, 0) ->
+ true;
+dotimes(Fun, N) when integer(N), N > 0 ->
+ Fun(),
+ dotimes(Fun, N-1).
+
+min(X, Y) when X < Y -> X;
+min(X, Y) -> Y.
diff --git a/src/edit_display.erl.slow b/src/edit_display.erl.slow
new file mode 100644
index 0000000..54a1d01
--- /dev/null
+++ b/src/edit_display.erl.slow
@@ -0,0 +1,160 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_display.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Editor display process: talks to curses
+%%% Created : 16 Sep 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_display).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+draw_window(Window) when Window#window.minibuffer == true,
+ Window#window.status_text /= undefined ->
+ ?EDIT_TERMINAL:move_to(0, Window#window.y),
+ draw_line(Window#window.status_text),
+ Window#window{status_text=undefined};
+draw_window(Window) ->
+ try_update(Window).
+
+try_update(Window) ->
+ Buf = Window#window.buffer,
+ PointMax = edit_buf:point_max(Buf),
+ DStart = edit_buf:mark_pos(Buf, Window#window.start_mark),
+ Scan = edit_lib:beginning_of_line_pos(Buf, DStart),
+ Point = edit_buf:mark_pos(Buf, point),
+ Cord = edit_buf:get_cord(Buf),
+ {_, CordInFront} = cord:split(Cord, Scan-1),
+ Walker = cord:walker(CordInFront, forward),
+ ?EDIT_TERMINAL:move_to(0, Window#window.y),
+ Rows = edit_window:text_lines(Window),
+ Prefix = Window#window.prefix,
+ PLen = length(Prefix),
+ PAcc = lists:reverse(Prefix),
+ case try_update_loop(Walker,Rows,Scan,PLen,0,Point,undefined,PAcc) of
+ {X, Y} ->
+ %% draw mode line
+ draw_modeline(Window),
+ TrimX = edit_lib:min(X, Window#window.width - 1),
+ ?EDIT_TERMINAL:move_to(TrimX, Y + Window#window.y),
+ Window;
+ undefined ->
+ %% The point wasn't inside the area we drew, so we
+ %% recenter the display with the point in the middle and
+ %% then draw again.
+ try_update(recenter_window(Window))
+ end.
+
+%% Returns the location of the point in a tuple {X, Y}, or undefined
+%% if it wasn't in the area drawn.
+
+try_update_loop(W0, NRows, Scan, Col, Row, Point, PointXY, Acc)
+ when Scan == Point, PointXY == undefined ->
+ try_update_loop(W0, NRows, Scan, Col, Row, Point, {Col,Row}, Acc);
+try_update_loop(W0, NRows, Scan, Col, Row, Point, PointXY, Acc) ->
+ {Ch, W1} = cord:walker_next(W0),
+ case Ch of
+ done ->
+ draw_line(lists:reverse(Acc)),
+ RemainingRows = NRows - Row,
+ %% draw empty lines until the end
+ dotimes(fun() -> draw_line([]),
+ ?EDIT_TERMINAL:newline()
+ end,
+ RemainingRows),
+ PointXY;
+ $\n ->
+ draw_line(lists:reverse(Acc)),
+ ?EDIT_TERMINAL:newline(),
+ NextRow = Row+1,
+ if NextRow == NRows ->
+ PointXY;
+ true ->
+ try_update_loop(W1,NRows,Scan+1,0,Row+1,Point,PointXY,[])
+ end;
+ $\t ->
+ Size = 8 - (Col rem 8),
+ Tab = lists:duplicate(Size, $ ),
+ Acc1 = Tab++Acc,
+ try_update_loop(W1,NRows,Scan+1,Col+Size,Row,Point,PointXY,Acc1);
+ Ch ->
+ Acc1 = [Ch|Acc],
+ try_update_loop(W1,NRows,Scan+1,Col+1,Row,Point,PointXY,Acc1)
+ end.
+
+draw_line(L) ->
+ Wth = ?EDIT_TERMINAL:width(),
+ Str = trunc_line(L, Wth),
+ ?EDIT_TERMINAL:put_string(L),
+ ?EDIT_TERMINAL:erase_to_eol().
+
+trunc_line([H], 1) -> [H];
+trunc_line(_, 1) -> [$$];
+trunc_line([H|T], N) -> [H|trunc_line(T, N-1)];
+trunc_line([], _) -> [].
+
+draw_modeline(Window) when Window#window.minibuffer == true ->
+ ok;
+draw_modeline(Window) ->
+ Buffer = Window#window.buffer,
+ Where = modeline_where(Window, Buffer),
+ Text = lists:flatten(
+ io_lib:format("--:?? ~s (~s) ~s",
+ [atom_to_list(Buffer),
+ (edit_buf:get_mode(Buffer))#mode.name,
+ Where])),
+ ?EDIT_TERMINAL:font_reverse(),
+ ?EDIT_TERMINAL:move_to(0, Window#window.y +
+ edit_window:physical_lines(Window) - 1),
+ draw_line(Text),
+ ?EDIT_TERMINAL:font_normal().
+
+modeline_where(Window, Buffer) ->
+ case edit_buf:get_size(Buffer) of
+ 0 ->
+ "ALL";
+ BSize ->
+ Start = edit_buf:mark_pos(Buffer, Window#window.start_mark),
+ Percentage = trunc(Start * 100 / BSize),
+ io_lib:format("~p%", [Percentage])
+ end.
+
+%% Update the display_start of a window so that it presents the point
+%% in the middle of the screen.
+recenter_window(Window) ->
+ Buf = Window#window.buffer,
+ Height = edit_window:text_lines(Window),
+ Pos = backward_lines(Buf, trunc(Height / 2)),
+ edit_buf:move_mark(Buf, Window#window.start_mark, Pos),
+ Window.
+
+backward_lines(Buf, N) ->
+ StartPos = edit_lib:beginning_of_line_pos(Buf),
+ edit_buf:walk_backward(Buf,
+ fun(X) -> back_lines(X, N, StartPos) end,
+ StartPos).
+
+back_lines(finish, N, Pos) ->
+ {result, 1};
+back_lines($\n, N, Pos) ->
+ if
+ N == 1 ->
+ {result, Pos};
+ true ->
+ {more, fun(New) -> back_lines(New, N-1, Pos-1) end}
+ end;
+back_lines(_, N, Pos) ->
+ {more, fun(New) -> back_lines(New, N, Pos-1) end}.
+
+dotimes(Fun, 0) ->
+ true;
+dotimes(Fun, N) when integer(N), N > 0 ->
+ Fun(),
+ dotimes(Fun, N-1).
+
+min(X, Y) when X < Y -> X;
+min(X, Y) -> Y.
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}.
+
+
diff --git a/src/edit_extended.erl b/src/edit_extended.erl
new file mode 100644
index 0000000..31920ca
--- /dev/null
+++ b/src/edit_extended.erl
@@ -0,0 +1,149 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_extended.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : execute-extended-command (emacs lingo)
+%%% Created : 14 Jan 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_extended).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+-compile({parse_transform, edit_transform}).
+
+extended_command(State, Mod, Func, Args) ->
+ {Params, _Doc} = find_cmd_info(Mod, Func),
+ NewState = execute(State, Mod, Func, Args, Params),
+ NewState#state{lastcmd={Mod, Func, Args}}.
+
+execute(State, Mod, Func, Args, []) ->
+ case catch apply(Mod, Func, [State | Args]) of
+ S when record(S, state) ->
+ S;
+ {'EXIT', Rsn} ->
+ io:format("** Crash: ~p~n", [Rsn]),
+ edit_util:popup_message(State, '*Error*', "** Crash: ~p", [Rsn]);
+ Other ->
+ State
+ end;
+execute(State, Mod, Func, Args, Params)
+ when State#state.pending_cmd /= undefined ->
+ edit_util:status_msg(State, "Minibuffer busy!");
+execute(State, Mod, Func, Args, Params) ->
+ Cont = make_execute_continuation(Mod, Func, Args, Params),
+ {Type, Prompt} = hd(Params),
+ edit_var:set(extended_arg_type, Type),
+ edit_complete:completion_init(Type),
+ default_init(State, Type),
+ State1 = activate_continuation(State, Cont, Prompt ++ " "),
+ State2 = edit_util:select_window(State1,
+ fun(W) ->
+ W#window.minibuffer
+ end),
+ case State#state.pending_win of
+ undefined ->
+ State2#state{pending_win=(State#state.curwin)#window.id};
+ _ ->
+ State2
+ end.
+
+default_init(State, file) ->
+ edit_buf:set_text(minibuffer, edit_util:pwd(State));
+default_init(State, _) ->
+ ok.
+
+make_execute_continuation(Mod, Func, Args, Params) ->
+ fun(State, NewArg) ->
+ State1 = if length(Params) == 1 ->
+ %% last argument, we're done!
+ deactivate_continuation(State);
+ true ->
+ State
+ end,
+ execute(State1, Mod, Func, Args ++ [NewArg], tl(Params))
+ end.
+
+activate_continuation(State, C, Prompt) ->
+ State1 = State#state{pending_cmd=C},
+ Enabler = fun(Win) -> Win#window{active=true, prefix=Prompt} end,
+ edit_util:update_minibuffer_window(State1, Enabler).
+
+deactivate_continuation(State) ->
+ edit_buf:set_text(minibuffer, ""),
+ State1 = State#state{pending_cmd=undefined},
+ Disabler = fun(Win) -> Win#window{active=false, prefix=""}
+ end,
+ %% disable minibuffer
+ State2 = edit_util:update_minibuffer_window(State1, Disabler),
+ %% reselect the window that the command is being done in
+ State3 =
+ edit_util:select_window(State2,
+ fun(W) ->
+ W#window.id == State2#state.pending_win
+ end),
+ State4 = State3#state{pending_win=undefined}.
+
+
+find_cmd_info(Mod, Func) ->
+ case catch Mod:command_info() of
+ {'EXIT', _} ->
+ {[], ""};
+ L when list(L) ->
+ case lists:keysearch(Func, 1, L) of
+ {value, {_, Params, Doc}} ->
+ {Params, Doc};
+ _ ->
+ {[], ""}
+ end
+ end.
+
+%% Callbacks from key bindings
+
+take_argument(State) when State#state.pending_cmd /= undefined ->
+ Text = edit_buf:get_text(minibuffer),
+ edit_history:add(history_var_name(), Text),
+ Cont = State#state.pending_cmd,
+ Cont(State, Text).
+
+abort(State) ->
+ io:format("Aborted!~n"),
+ State1 = deactivate_continuation(State).
+
+%% M-x
+
+
+-command({execute_extended_command,
+ [{mod_func, "M-x"}],
+ "Like M-x in emacs."}).
+
+execute_extended_command(State, MF) ->
+ case string:tokens(MF, " :") of
+ [ModStr, FunStr] ->
+ Mod = list_to_atom(ModStr),
+ Fun = list_to_atom(FunStr),
+ edit:invoke_extended_async(Mod, Fun, []),
+ State;
+ _ ->
+ edit_util:status_msg(State, "Bad string; must be \"Mod:Fun\"")
+ end.
+
+%% History
+
+history_move(State, Dir) ->
+ edit_history:move(State, history_var_name(), history_region_fun(), Dir).
+
+-command({history_search, [{history_regexp, "History regexp:"}]}).
+%% FIXME: This function fails when it tries to activate the minibuffer to
+%% search regexp, but the minibuffer is already active.
+history_search(State, RE) ->
+ edit_history:search(State, history_var_name(), history_region_fun(), RE).
+
+history_var_name() ->
+ TypeString = atom_to_list(edit_var:lookup(extended_arg_type)),
+ list_to_atom("extended_history:" ++ TypeString).
+
+history_region_fun() ->
+ fun(Buf) -> {1, edit_buf:point_max(Buf)} end.
+
diff --git a/src/edit_file.erl b/src/edit_file.erl
new file mode 100644
index 0000000..5c26de6
--- /dev/null
+++ b/src/edit_file.erl
@@ -0,0 +1,95 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_file.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : file-related editor commands
+%%% Created : 14 Jan 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_file).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+-compile({parse_transform, edit_transform}).
+
+-import(edit_lib, [buffer/1]).
+
+-command({find_file, [{file, "Filename:"}], "Open a file in a buffer."}).
+
+mod_init() ->
+ edit_var:set(auto_mode_alist, []),
+ ok.
+
+find_file(State, "") ->
+ State;
+find_file(State, Filename) ->
+ BufferName = list_to_atom(lists:last(string:tokens(Filename, "/"))),
+ NewState = find_file1(State, Filename, BufferName, 0),
+ auto_set_mode(NewState).
+
+%% Open Filename in the buffer Name. If Name exists and is visiting a
+%% different file, then try Name<1>, etc. If we hit a buffer that's
+%% already visiting this file, just attach to that.
+find_file1(State, Filename, Name, N) ->
+ BufferName = make_buffer_name(Name, N),
+ case whereis(BufferName) of
+ undefined ->
+ {ok, Buf} = edit_buf:new(BufferName),
+ edit_buf:set_filename(Buf, filename:absname(Filename)),
+ State1 = edit_util:set_buffer(State, BufferName),
+ case cord:new_from_file(Filename) of
+ {ok, Cord} ->
+ edit_buf:set_text(Buf, Cord),
+ edit_buf:move_mark(Buf, point, 1),
+ State1;
+ {error, Reason} ->
+ edit_util:status_msg(State1, "(New file)")
+ end;
+ Pid ->
+ case edit_buf:get_filename(BufferName) of
+ Filename ->
+ edit_util:set_buffer(State, BufferName);
+ _ ->
+ find_file1(State, Filename, Name, N+1)
+ end
+ end.
+
+make_buffer_name(Atom, 0) ->
+ Atom;
+make_buffer_name(Atom, N) ->
+ Name = io_lib:format("~s<~p>", [atom_to_list(Atom), N]),
+ list_to_atom(lists:flatten(Name)).
+
+save_file(State) ->
+ case edit_buf:get_filename(buffer(State)) of
+ undefined ->
+ edit_util:status_msg(State, "No file associated with buffer");
+ Filename ->
+ Cord = edit_buf:get_cord(buffer(State)),
+ IO = cord:to_iolist(Cord),
+ ok = file:write_file(Filename, IO),
+ edit_util:status_msg(State, "Wrote ~s", [Filename])
+ end.
+
+auto_set_mode(State) ->
+ AutoModeList = edit_var:lookup(auto_mode_alist),
+ Buf = buffer(State),
+ case edit_buf:get_filename(Buf) of
+ undefined ->
+ State;
+ Filename ->
+ auto_set_mode(State, Filename, AutoModeList)
+ end.
+
+auto_set_mode(State, Filename, []) ->
+ State;
+auto_set_mode(State, Filename, [{RE, {Mod, Fun}}|T]) ->
+ case regexp:match(Filename, RE) of
+ nomatch ->
+ auto_set_mode(State, Filename, T);
+ {match, _, _} ->
+ Mod:Fun(State)
+ end.
+
+
diff --git a/src/edit_genserv.erl b/src/edit_genserv.erl
new file mode 100644
index 0000000..0ec5557
--- /dev/null
+++ b/src/edit_genserv.erl
@@ -0,0 +1,50 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_genserv.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Generic utility functions for servers.
+%%% Created : 28 Apr 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+%% This is a library for writing servers. The idea is to have a less
+%% abstract gen_server, in which the protocols are exposed and the
+%% servers still use 'receive' to get requests with pattern matching.
+
+-module(edit_genserv).
+-author('luke@bluetail.com').
+
+-export([start_link/4, call/2, call/3, reply/2, cast/2]).
+
+%% Start a named process, if it's not already running.
+%%
+%% Returns: {ok, Pid} | {error, Reason}
+start_link(Name, M, F, A) ->
+ case whereis(Name) of
+ Pid when pid(Pid) ->
+ {error, {already_started, Pid}};
+ undefined ->
+ Pid = spawn_link(M, F, A),
+ register(Name, Pid),
+ {ok, Pid}
+ end.
+
+%% Protocol for calls (from 'gen'):
+%% -> {call, {SenderPid, Ref}, Req}
+%% <- {Ref, Reply}
+
+%% Synchronous call.
+call(Pid, Req) ->
+ call(Pid, Req, 10000).
+
+call(Pid, Req, Timeout) ->
+ {ok, Reply} = gen:call(Pid, call, Req, Timeout),
+ Reply.
+
+reply(To, Reply) ->
+ gen:reply(To, Reply).
+
+%% Protocol for casts:
+%% -> {cast, Req}
+
+cast(Pid, Req) ->
+ Pid ! {cast, Req}.
+
diff --git a/src/edit_globalmap.erl b/src/edit_globalmap.erl
new file mode 100644
index 0000000..8751f95
--- /dev/null
+++ b/src/edit_globalmap.erl
@@ -0,0 +1,126 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_globalmap.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Global keymap
+%%% Created : 23 Sep 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_globalmap).
+-author('luke@bluetail.com').
+
+-export([init/0]).
+
+%% Initialise global keymap
+init() ->
+ Map = edit_keymap:new(global_map),
+ edit_keymap:bind_each(Map, bindings()),
+ edit_keymap:new(global_cx_map),
+ edit_keymap:bind_each(global_cx_map, cx_bindings()),
+ edit_keymap:new(minibuffer_map),
+ edit_keymap:bind_each(minibuffer_map, minibuffer_bindings()),
+ edit_keymap:new(term_escape_map),
+ edit_keymap:bind_each(term_escape_map, term_escape_bindings()),
+ edit_keymap:new(help_map),
+ edit_keymap:bind_each(help_map, help_bindings()),
+ edit_keymap:new(empty_keymap),
+ global_map.
+
+bindings() ->
+ [
+ %% Motion
+ {"C-a", {edit_lib, beginning_of_line, []}},
+ {"C-b", {edit_lib, backward_char, []}},
+ {"C-e", {edit_lib, end_of_line, []}},
+ {"C-f", {edit_lib, forward_char, []}},
+ {"M-b", {edit_lib, backward_word, []}},
+ {"M-f", {edit_lib, forward_word, []}},
+ {"M-<", {edit_lib, start_of_buffer, []}},
+ {"M->", {edit_lib, end_of_buffer, []}},
+ {"C-n", {edit_lib, next_line, []}},
+ {"C-p", {edit_lib, previous_line, []}},
+ {"C-M-a", {edit_erlang, beginning_of_function, []}},
+ {"C-v", {edit_lib, scroll_down, []}},
+ {"M-v", {edit_lib, scroll_up, []}},
+ {"C-l", {edit_lib, recenter, []}},
+ %% Editing
+ {"C-h", {edit_lib, delete_char_backward, []}},
+ {"C-?", {edit_lib, delete_char_backward, []}},
+ {"C-d", {edit_lib, delete_char_forward, []}},
+ {"M-d", {edit_lib, delete_word_forward, []}},
+ {"C-k", {edit_lib, kill_line, []}},
+ {"C-w", {edit_lib, kill_region, [true]}},
+ {"M-w", {edit_lib, kill_region, [false]}},
+ {"C-y", {edit_lib, yank, []}},
+ {"C-o", {edit_lib, open_line, []}},
+ {"C-_", {edit_lib, undo, []}},
+ {"C-/", {edit_lib, undo, []}},
+ {"C-u", {edit_lib, undo, []}},
+ %% Searching
+ {"C-s", {edit_lib, search, [forward]}},
+ {"C-r", {edit_lib, search, [backward]}},
+ {"C-M-s", {edit_lib, regexp_search, [forward]}},
+ {"C-M-r", {edit_lib, regexp_search, [backward]}},
+ %% Misc
+ {"C-@", {edit_lib, set_mark, []}},
+ {"M-!", {edit_lib, unix_command, []}},
+ {"C-t", {edit_lib, printf, []}},
+ {"M-:", {edit_eval, eval_expression, []}},
+ {"M-x", {edit_extended, execute_extended_command, []}},
+ {"C-g", {edit_lib, abort, []}},
+ %% Keymaps
+ {"M-[", {keymap, term_escape_map}},
+ {"C-x", {keymap, global_cx_map}},
+ %% Help
+ {"M-h", {keymap, help_map}},
+ %% self insert commands (\r becomes \n)
+ {$\r, {edit_lib, self_insert_command, [$\n]}}
+ | [{Ch, {edit_lib, self_insert_command, [Ch]}} || Ch <- self_inserts()]
+ ].
+
+%% ESC [ <key> - for arrows etc
+term_escape_bindings() ->
+ [{"A", {edit_lib, previous_line, []}},
+ {"B", {edit_lib, next_line, []}},
+ {"C", {edit_lib, forward_char, []}},
+ {"D", {edit_lib, backward_char, []}}].
+
+%% Bindings after C-x prefix
+cx_bindings() ->
+ [{"C-x", {edit_lib, exchange_point_and_mark, []}},
+ {"C-c", {edit_lib, quit, []}},
+ {"C-f", {edit_file, find_file, []}},
+ {"C-s", {edit_file, save_file, []}},
+ {"C-m", {edit_eval, interactive_eval, []}},
+ {"i", {edit_eval, erlang_interaction_mode, []}},
+ {"f", {edit_lib, fundamental_mode, []}},
+ {"r", {edit_eval, back_to_start, []}},
+ {"b", {edit_lib, switch_to_buffer, []}},
+ {"k", {edit_lib, kill_buffer, []}},
+ {"h", {edit_lib, mark_whole_buffer, []}},
+ {"o", {edit_lib, next_window, []}},
+ {"0", {edit_lib, delete_window, []}},
+ {"1", {edit_lib, delete_other_windows, []}},
+ {"2", {edit_lib, split_window_vertically, []}},
+% {"e", {edit_erlang, erlang_mode, []}},
+ {"d", {edit_lib, buffer_cord_info, []}}
+ ].
+
+minibuffer_bindings() ->
+ [{"C-m", {edit_extended, take_argument, []}},
+ {"C-g", {edit_extended, abort, []}},
+ {"C-i", {edit_complete, complete, []}},
+ %% generic history
+ {"M-p", {edit_extended, history_move, [up]}},
+ {"M-n", {edit_extended, history_move, [down]}},
+ {"M-r", {edit_extended, history_search, []}}
+ ].
+
+help_bindings() ->
+ [{"k", {edit_help, describe_key, []}},
+ {"s", {edit_help, find_source, []}}].
+
+self_inserts() ->
+ %% HACK
+ "\n\t" ++ lists:seq(32, 126).
+ %%"\r\n\t" ++ (lists:seq(32, 126) -- [127]).
+
diff --git a/src/edit_help.erl b/src/edit_help.erl
new file mode 100644
index 0000000..dc904cf
--- /dev/null
+++ b/src/edit_help.erl
@@ -0,0 +1,97 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_help.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Help-related functions
+%%% Created : 2 Feb 2002 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_help).
+
+-include_lib("ermacs/include/edit.hrl").
+
+-import(edit_lib, [buffer/1]).
+
+-compile(export_all).
+-compile({parse_transform, edit_transform}).
+
+describe_key(S) ->
+ edit_util:popup_message(S, '*Help*', description(lookup_key(S))).
+
+description({Mod, Fun, Args}) ->
+ case edit_extended:find_cmd_info(Mod, Fun) of
+ {[], ""} ->
+ io_lib:format("Command: ~s:~s", [Mod, Fun]);
+ {Params, Doc} ->
+ io_lib:format("Command: ~s:~s ~s\n~s",
+ [Mod,
+ Fun,
+ [io_lib:format("~p ", [Param]) ||
+ {Param,_} <- Params],
+ Doc])
+ end;
+description(X) ->
+ lists:flatten(io_lib:format("~p", [X])).
+
+find_source(S) ->
+ case lookup_key(S) of
+ {Mod, Fun, Args} ->
+ find_source(S, Mod, Fun);
+ _ ->
+ edit_lib:status_msg(S, "Not bound to a function")
+ end.
+
+find_source(S0, Mod, Fun) ->
+ case guess_source_file(code:which(Mod)) of
+ {ok, Filename} ->
+ S1 = edit_file:find_file(S0, Filename),
+ Buf = buffer(S1),
+ edit_buf:move_mark(Buf, point, 1),
+ edit_lib:regexp_search(S1, forward, [$\n]++atom_to_list(Fun)),
+ edit_lib:beginning_of_line(S1)
+ end.
+
+guess_source_file(S0) ->
+ case regexp:sub(S0, "ebin", "src") of
+ {ok, S1, _} ->
+ case regexp:sub(S1, "beam", "erl") of
+ {ok, S2, _} ->
+ case file:read_file_info(S2) of
+ {ok, _} ->
+ {ok, S2};
+ _ ->
+ error
+ end;
+ _ ->
+ error
+ end;
+ _ ->
+ error
+ end.
+
+%% ----------------------------------------------------------------------
+%% Keymap lookup - cut&paste from edit.erl
+
+lookup_key(State) ->
+ io:format("here.~n"),
+ Buf = buffer(State),
+ Keymaps = (edit_buf:get_mode(Buf))#mode.keymaps ++ [global_map],
+ lookup_key(State, Keymaps).
+
+lookup_key(State, Keymaps) ->
+ Ch = edit:get_key(),
+ io:format("Snarfed key: ~p ~p~n", [Ch, Keymaps]),
+ lookup_key(State, Keymaps, Ch).
+
+lookup_key(State, [], Ch) ->
+ unbound;
+lookup_key(State, [Keymap|Keymaps], Ch) ->
+ case edit_keymap:lookup(Keymap, Ch) of
+ {ok, {keymap, NewMap}} ->
+ io:format("Retrying with ~p~n", [NewMap]),
+ lookup_key(State, [NewMap]);
+ {ok, Cmd} ->
+ Cmd;
+ unbound ->
+ lookup_key(State, Keymaps, Ch)
+ end.
+
diff --git a/src/edit_history.erl b/src/edit_history.erl
new file mode 100644
index 0000000..750ac35
--- /dev/null
+++ b/src/edit_history.erl
@@ -0,0 +1,126 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_history.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Command history
+%%% Created : 24 Mar 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_history).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+-compile({parse_transform, edit_transform}).
+-import(edit_lib, [buffer/1]).
+
+-define(HISTORY_MAX_LENGTH, 500).
+
+%% API
+-export([bindings/2, add/2]).
+
+%% Commands
+-export([move/4, search/4]).
+
+%% BaseVar = atom() name of edit_var variable to put history
+%% RegionFn = fun(Buffer) -> {Start, End} of history region
+%%
+%% Returns a set of (standard) keybindings.
+bindings(BaseVar, RegionFn) ->
+ [{"M-p", {?MODULE, move, [BaseVar, RegionFn, up]}},
+ {"M-n", {?MODULE, move, [BaseVar, RegionFn, down]}},
+ {"M-r", {?MODULE, search, [BaseVar, RegionFn]}}
+ ].
+
+add(BaseVar, "") ->
+ %% No blanks in history.
+ skipped;
+add(BaseVar, Item) ->
+ Hist = edit_var:lookup(BaseVar, []),
+ if
+ hd(Hist) == Item ->
+ %% duplicate, leave history alone
+ ok;
+ true ->
+ edit_var:set(BaseVar, trim_history([Item|Hist]))
+ end.
+
+move(State, BaseVar, RegionFn, Direction) ->
+ IdxVar = index_var(BaseVar),
+ HVar = list_var(BaseVar),
+ History = ["" | edit_var:lookup(HVar, [])],
+ Idx = case continuing_p(State) of
+ true ->
+ Prev = edit_var:lookup(IdxVar, 1),
+ case Direction of
+ up ->
+ Prev + 1;
+ down ->
+ Prev - 1
+ end;
+ false ->
+ 2 % 1 is a new empty string
+ end,
+ if
+ Idx >= 1, length(History) >= Idx ->
+ Buf = buffer(State),
+ New = lists:nth(Idx, History),
+ kill_old(Buf, RegionFn),
+ edit_buf:insert(Buf, New, edit_buf:mark_pos(Buf, point)),
+ edit_var:set(IdxVar, Idx),
+ State;
+ true ->
+ edit_util:status_msg(State, "No more history")
+ end.
+
+-command({search, [{string, "History regexp:"}]}).
+search(State, BaseVar, RegionFn, Regexp) ->
+ History = edit_var:lookup(BaseVar, []),
+ case find_match(History, Regexp) of
+ {match, Cmd} ->
+ Buf = buffer(State),
+ kill_old(Buf, RegionFn),
+ edit_buf:insert(Buf, Cmd, edit_buf:mark_pos(Buf, point)),
+ State;
+ {error, Err} ->
+ edit_util:status_msg(State, "Error: " ++ regexp:format_error(Err));
+ nomatch ->
+ edit_util:status_msg(State, "Not found")
+ end.
+
+continuing_p(State) ->
+ case State#state.lastcmd of
+ {?MODULE, move, _} ->
+ true;
+ {?MODULE, search, _} ->
+ true;
+ {_, history_move, _} ->
+ true;
+ {_, history_search, _} ->
+ true;
+ _ ->
+ false
+ end.
+
+kill_old(Buf, RegionFn) ->
+ {Start, End} = RegionFn(Buf),
+ edit_buf:delete(Buf, Start, End).
+
+trim_history(Hist) when length(Hist) > ?HISTORY_MAX_LENGTH ->
+ lists:sublist(Hist, ?HISTORY_MAX_LENGTH);
+trim_history(Hist) ->
+ Hist.
+
+find_match([], Regexp) ->
+ nomatch;
+find_match([H|T], Regexp) ->
+ case regexp:match(H, Regexp) of
+ nomatch ->
+ find_match(T, Regexp);
+ {match, _Start, _Length} ->
+ {match, H};
+ {error, Err} ->
+ {error, Err}
+ end.
+
+list_var(Base) -> Base.
+index_var(Base) -> list_to_atom(atom_to_list(Base) ++ "_idx").
+
diff --git a/src/edit_input.erl b/src/edit_input.erl
new file mode 100644
index 0000000..2f0990a
--- /dev/null
+++ b/src/edit_input.erl
@@ -0,0 +1,34 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_input.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Keyboard input server
+%%% Created : 22 Jan 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_input).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-export([start_link/1, loop/1]).
+
+%% Receiver will be sent {key_input, Char} each time a key is pressed.
+start_link(Receiver) ->
+ Pid = spawn_link(edit_input, loop, [Receiver]),
+ register(?MODULE, Pid),
+ Pid.
+
+loop(Receiver) ->
+ Ch = case ?EDIT_TERMINAL:read() of
+ $\n ->
+ $\r;
+ 145 -> % C-M-q is reserved for panic
+ panic();
+ X ->
+ X
+ end,
+ Receiver ! {key_input, Ch},
+ loop(Receiver).
+
+panic() ->
+ halt().
diff --git a/src/edit_keymap.erl b/src/edit_keymap.erl
new file mode 100644
index 0000000..9ed0ce6
--- /dev/null
+++ b/src/edit_keymap.erl
@@ -0,0 +1,109 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_keymap.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Keymap implemented as an ETS table
+%%% Created : 23 Sep 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_keymap).
+-author('luke@bluetail.com').
+
+-export([start_link_server/0, new/1, keymap_exists/1,
+ global_set_key/2, set_key/3,
+ bind/3, bind_each/2, delete/1, lookup/2, test/0]).
+
+-export([server/0]).
+
+start_link_server() ->
+ case whereis(?MODULE) of
+ Pid when pid(Pid) ->
+ {error, {already_started, Pid}};
+ undefined ->
+ Pid = spawn_link(?MODULE, server, []),
+ register(?MODULE, Pid),
+ {ok, Pid}
+ end.
+
+server() ->
+ receive
+ {request, From, {create, Name}} ->
+ Reply = case keymap_exists(Name) of
+ true ->
+ {error, {already_exists, Name}};
+ false ->
+ ets:new(Name, [named_table, set, public]),
+ Name
+ end,
+ From ! {reply, Reply};
+ _ ->
+ true
+ end,
+ server().
+
+new(Name) ->
+ ?MODULE ! {request, self(), {create, Name}},
+ receive {reply, R} -> R end.
+
+keymap_exists(Name) ->
+ case ets:info(Name) of
+ undefined ->
+ false;
+ _ ->
+ true
+ end.
+
+global_set_key(KeySeq, Binding) ->
+ set_key(global_map, KeySeq, Binding).
+
+%% e.g. set_key(edit_globalmap, "C-x C-f", {edit_file, find_file, []})
+set_key(KeyMap, KeySeq, Binding) ->
+ set_key_traverse(KeyMap, string:tokens(KeySeq, " "), Binding).
+
+set_key_traverse(KeyMap, [Key], Binding) ->
+ bind(KeyMap, Key, Binding),
+ ok;
+set_key_traverse(KeyMap, [MapKey | KeySeq], Binding) ->
+ case lookup(KeyMap, MapKey) of
+ {_, {keymap, SubMap}} ->
+ set_key_traverse(SubMap, KeySeq, Binding);
+ _ ->
+ error
+ end.
+
+bind(Keymap, Key, Value) ->
+ ets:insert(Keymap, {Key, Value}).
+
+%% Bind each item of a key-value list.
+bind_each(Name, KVList) ->
+ lists:foreach(fun({Key, Value}) -> bind(Name, Key, Value) end,
+ KVList).
+
+lookup(Name, Key) ->
+ %% First lookup by key-code number
+ case ets:lookup(Name, Key) of
+ [{Key, Value}] ->
+ {ok, Value};
+ [] ->
+ %% Next lookup by stringified name
+ String = edit_util:keyname(Key),
+ case ets:lookup(Name, String) of
+ [{String, Value}] ->
+ {ok, Value};
+ [] ->
+ unbound
+ end
+ end.
+
+delete(Name) ->
+ ets:delete(Name).
+
+test() ->
+ Map = new(test),
+ unbound = lookup(Map, key),
+ bind(Map, key, value),
+ {ok, value} = lookup(Map, key),
+ bind(Map, key, newvalue),
+ {ok, newvalue} = lookup(Map, key),
+ delete(Map),
+ ok.
+
diff --git a/src/edit_lex.erl b/src/edit_lex.erl
new file mode 100644
index 0000000..4d6e6ef
--- /dev/null
+++ b/src/edit_lex.erl
@@ -0,0 +1,14 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_lex.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Lexer
+%%% Created : 8 Apr 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_lex).
+-author('luke@bluetail.com').
+
+%%-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+
diff --git a/src/edit_lib.erl b/src/edit_lib.erl
new file mode 100644
index 0000000..73372db
--- /dev/null
+++ b/src/edit_lib.erl
@@ -0,0 +1,801 @@
+-module(edit_lib).
+-author('tobbe@serc.rmit.edu.au').
+%%----------------------------------------------------------------------
+%% Created : 15 Jun 1998 by tobbe@serc.rmit.edu.au
+%% Function: Core library routines for the Edit editor.
+%%----------------------------------------------------------------------
+-vc('$Id$ ').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+-compile({parse_transform, edit_transform}).
+
+self_insert_command(S,Ch) when record(S,state) ->
+ insert_char(S,Ch).
+
+delete_char_backward(S) ->
+ B = buffer(S),
+ case edit_buf:mark_pos(B, point) of
+ 1 ->
+ S;
+ N ->
+ edit_buf:delete(B, N-1, N)
+ end.
+
+delete_char_forward(S) ->
+ B = buffer(S),
+ Max = edit_buf:point_max(B),
+ case edit_buf:mark_pos(B, point) of
+ N when N < Max ->
+ edit_buf:delete(B, N, N+1);
+ _ ->
+ true
+ end,
+ S.
+
+%% Note: delete_word, not kill_word. Should single words really go on
+%% the kill ring, like they do in emacs?
+delete_word_forward(S) ->
+ Buf = buffer(S),
+ Point = edit_buf:mark_pos(Buf, point),
+ P1 = find_char_forward(Buf, alphanumeric_P(), Point, Point),
+ P2 = find_char_forward(Buf, not_P(alphanumeric_P()), P1, P1),
+ edit_buf:delete(Buf, Point, P2).
+
+insert_char(S, Ch) ->
+ Buf = buffer(S),
+ edit_buf:insert(Buf, [Ch], edit_buf:mark_pos(Buf, point)).
+
+%% "open_line" is the emacs name for what C-o does
+open_line(S) ->
+ Buf = buffer(S),
+ OrigPoint = edit_buf:mark_pos(Buf, point),
+ insert_char(S, $\n),
+ edit_buf:move_mark(Buf, point, OrigPoint).
+
+%% Abort - interpreted to abort any asynchronous command which is
+%% borrowing the current buffer (or nothing if there isn't one).
+abort(S) ->
+ case edit_buf:revoke((S#state.curwin)#window.buffer) of
+ false ->
+ S;
+ true ->
+ edit_util:status_msg(S, "Buffer revoked")
+ end.
+
+%% -----------------------------------
+%% Move cursor vertically down 1 line.
+
+next_line(S) when record(S,state) ->
+ Buf = buffer(S),
+ NewWindow = update_goal(S, Buf),
+ Goal = NewWindow#window.goal_column,
+ Max = edit_buf:point_max(Buf),
+ case end_of_line_pos(Buf) of
+ X when X == Max ->
+ ok;
+ X ->
+ F = fun(Ch) -> walk_goal(Ch, Goal, X+1) end,
+ NewP = edit_buf:walk_forward(Buf, F, X+1),
+ edit_buf:move_mark(Buf, point, NewP),
+ S#state{curwin=NewWindow}
+ end.
+
+previous_line(S) when record(S,state) ->
+ Buf = buffer(S),
+ NewWindow = update_goal(S, Buf),
+ Goal = NewWindow#window.goal_column,
+ case beginning_of_line_pos(Buf) of
+ 1 ->
+ move_to(Buf, 1);
+ X ->
+ From = beginning_of_line_pos(Buf, X-1),
+ F = fun(Ch) -> walk_goal(Ch, Goal, From) end,
+ NewP = edit_buf:walk_forward(Buf,
+ F,
+ beginning_of_line_pos(Buf, From)),
+ edit_buf:move_mark(Buf, point, NewP),
+ S#state{curwin=NewWindow}
+ end.
+
+update_goal(State, Buf) ->
+ Win = State#state.curwin,
+ case goal_preserving(State#state.lastcmd) of
+ true ->
+ Win;
+ false ->
+ Region = edit_buf:get_region(Buf,
+ point(Buf),
+ beginning_of_line_pos(Buf)),
+ Win#window{goal_column=goal_value(Region)}
+ end.
+
+goal_value(Region) ->
+ lists:foldl(fun($\t, Acc) -> Acc + 8;
+ (_, Acc) -> Acc + 1
+ end,
+ 0,
+ Region).
+
+goal_preserving({?MODULE, next_line, []}) -> true;
+goal_preserving({?MODULE, previous_line, []}) -> true;
+goal_preserving(_) -> false.
+
+%% Go forward until we reach the goal column, or the end of file
+walk_goal(C, Z, P) when Z =< 0 ->
+ {result, P};
+walk_goal(C, Remaining, P) when C == finish;
+ C == $\n ->
+ {result, P};
+walk_goal($\t, Remaining, P) when Remaining < 8 ->
+ {result, P};
+walk_goal($\t, Remaining, P) ->
+ {more, fun(C) -> walk_goal(C, Remaining-8, P+1) end};
+walk_goal(_, Remaining, P) ->
+ {more, fun(C) -> walk_goal(C, Remaining-1, P+1) end}.
+
+point(Buf) ->
+ edit_buf:mark_pos(Buf, point).
+
+%% ----------------------------
+%% Move point left 1 character.
+
+backward_char(S) when record(S,state) ->
+ Buf = buffer(S),
+ Pos = edit_buf:mark_pos(Buf, point) - 1,
+ case Pos < 1 of
+ true ->
+ S;
+ false ->
+ edit_buf:move_mark(Buf, point, Pos)
+ end.
+
+%% -----------------------------
+%% Move point right 1 character.
+%%
+
+forward_char(S) when record(S,state) ->
+ Buf = buffer(S),
+ Pos = edit_buf:mark_pos(Buf, point) + 1,
+ Max = edit_buf:point_max(Buf),
+ case Pos > Max of
+ true ->
+ S;
+ false ->
+ edit_buf:move_mark(Buf, point, Pos)
+ end.
+
+%% ----------------------------------------
+%% Move point to beginning of current line.
+
+beginning_of_line(S) ->
+ Pos = beginning_of_line_pos(buffer(S)),
+ edit_buf:move_mark(buffer(S), point, Pos),
+ S.
+
+%% ugly function
+backward_word(S) ->
+ Buf = buffer(S),
+ Point = edit_buf:mark_pos(Buf, point),
+ if Point == 1 ->
+ S;
+ true ->
+ X = find_char_backward(Buf, alphanumeric_P(), Point-1, 1),
+ if X == 1 ->
+ move_to(Buf, 1),
+ S;
+ true ->
+ NewP = 1 + find_char_backward(Buf,
+ not_P(alphanumeric_P()),
+ X,
+ 0),
+ move_to(Buf, NewP),
+ S
+ end
+ end.
+
+beginning_of_line_pos(Buf) ->
+ Pos = edit_buf:mark_pos(Buf, point),
+ beginning_of_line_pos(Buf, Pos).
+beginning_of_line_pos(Buf, Pos) ->
+ %% We start before the point, incase we're on a newline
+ case find_char_backward(Buf, const_P($\n), Pos - 1) of
+ not_found -> 1; % point_min
+ N when integer(N) -> N + 1 % we want to be just after the newline
+ end.
+
+move_to_char_backward(Buf, Pred) ->
+ Pos = edit_buf:mark_pos(Buf, point),
+ edit_buf:move_mark(Buf, point, find_char_backward(Buf, Pred, Pos, 1)).
+
+%% Returns CharPos | not_found
+find_char_backward(Buf, Pred) ->
+ find_char_backward(Buf, Pred, edit_buf:mark_pos(Buf, point)).
+find_char_backward(Buf, Pred, Pos) ->
+ find_char_backward(Buf, Pred, Pos, not_found).
+find_char_backward(Buf, Pred, Pos, Default) ->
+ case edit_buf:walk_backward(Buf,
+ fun(C) -> find_char_backward1(C,Pred,Pos) end,
+ Pos) of
+ not_found ->
+ Default;
+ P when integer(P) ->
+ P
+ end.
+
+%% This clause is a kludge; see edit_buf.erl for details
+find_char_backward1(not_there, Pred, Pos) ->
+ {more, fun(New) -> find_char_backward1(New, Pred, Pos-1) end};
+find_char_backward1(finish, Pred, Pos) ->
+ {result, not_found};
+find_char_backward1(C, Pred, Pos) ->
+ %%edit_buf:log("Char: ~p~n", [C]),
+ case Pred(C) of
+ true ->
+ {result, Pos};
+ false ->
+ {more, fun(New) -> find_char_backward1(New, Pred, Pos-1) end}
+ end.
+
+%% ----------------------------------
+%% Move point to end of current line.
+
+end_of_line(S) ->
+ move_to_char_forward(buffer(S), fun(C) -> C == $\n end),
+ S.
+
+forward_word(S) ->
+ move_to_char_forward(buffer(S), alphanumeric_P()),
+ move_to_char_forward(buffer(S), not_P(alphanumeric_P())),
+ S.
+
+end_of_line_pos(Buf) ->
+ Pos = edit_buf:mark_pos(Buf, point),
+ end_of_line_pos(Buf, Pos).
+end_of_line_pos(Buf, Pos) ->
+ P = case find_char_forward(Buf, fun(C) -> C == $\n end, Pos) of
+ not_found ->
+ edit_buf:point_max(Buf);
+ N when integer(N) ->
+ N
+ end.
+
+move_to_char_forward(Buf, Pred) ->
+ Pos = edit_buf:mark_pos(Buf, point),
+ NewPos = find_char_forward(Buf, Pred, Pos, edit_buf:point_max(Buf)),
+ edit_buf:move_mark(Buf, point, NewPos).
+
+%% Returns CharPos | not_found
+find_char_forward(Buf, Pred, Pos) ->
+ find_char_forward(Buf, Pred, Pos, not_found).
+%% Returns CharPos | Default (if not found)
+find_char_forward(Buf, Pred, Pos, Default) ->
+ case edit_buf:walk_forward(Buf,
+ fun(C) -> find_char_forward1(C, Pred, Pos) end,
+ Pos) of
+ not_found ->
+ Default;
+ P when integer(P) ->
+ P
+ end.
+
+find_char_forward1(finish, Pred, Pos) ->
+ {result, not_found};
+find_char_forward1(C, Pred, Pos) ->
+ case Pred(C) of
+ true ->
+ {result, Pos};
+ false ->
+ {more, fun(New) -> find_char_forward1(New, Pred, Pos+1) end}
+ end.
+
+recenter(State) ->
+ Buf = buffer(State),
+ Win = State#state.curwin,
+ Lines = trunc(Win#window.height/2),
+ Point = edit_buf:mark_pos(Buf, point),
+ NewDStart = case find_nth(Buf, backward, Point, $\n, Lines) of
+ not_found ->
+ 1;
+ Pos ->
+ Pos + 1
+ end,
+ edit_buf:move_mark(Buf, Win#window.start_mark, NewDStart),
+ ?EDIT_TERMINAL:invalidate(),
+ State.
+
+%% These scrolling functions are ugly
+
+scroll_up(State) ->
+ Win = State#state.curwin,
+ Height = edit_window:text_lines(Win),
+ Buf = buffer(State),
+ DStart = edit_buf:mark_pos(Buf, Win#window.start_mark),
+ NewDStart =
+ case find_nth(Buf, backward, DStart, $\n, Height - 1) of
+ not_found ->
+ 1;
+ X when integer(X) ->
+ X + 1
+ end,
+ DEnd = case find_nth(Buf, forward, NewDStart, $\n, Height) of
+ not_found ->
+ edit_buf:point_max(Buf);
+ N ->
+ N
+ end,
+ Point = edit_buf:mark_pos(Buf, point),
+ if Point > DEnd ->
+ move_to(Buf, beginning_of_line_pos(Buf, DEnd));
+ true ->
+ ok
+ end,
+ edit_buf:move_mark(Buf, Win#window.start_mark, NewDStart),
+ State.
+
+scroll_down(State) ->
+ Win = State#state.curwin,
+ Buf = buffer(State),
+ DStart = edit_buf:mark_pos(Buf, Win#window.start_mark),
+ PMax = edit_buf:point_max(Buf),
+ case find_nth(Buf, forward, DStart, $\n, edit_window:text_lines(Win)-2) of
+ X when integer(X),
+ X < PMax ->
+ %% We want to be just after the newline - i.e. start of next line
+ Pos = X + 1,
+ Point = edit_buf:mark_pos(Buf, point),
+ if Point < Pos ->
+ %% Scrolled the point off the screen, move the
+ %% point to the old display start (up near the
+ %% top).
+ edit_buf:move_mark(Buf, point, Pos);
+ true ->
+ ok
+ end,
+ edit_buf:move_mark(Buf, Win#window.start_mark, Pos),
+ State;
+ _ ->
+ State
+ end.
+
+scroll_down_wrap(Win) ->
+ Buf = Win#window.buffer,
+ DStart = edit_buf:mark_pos(Buf, Win#window.start_mark),
+ PMax = edit_buf:point_max(Buf),
+ Lines = edit_window:text_lines(Win),
+ Pos = case find_nth(Buf, forward, DStart, $\n, Lines) of
+ X when integer(X),
+ X < PMax ->
+ %% We want to be just after the newline - i.e. start
+ %% of next line
+ X + 1;
+ _ ->
+ 1
+ end,
+ edit_buf:move_mark(Buf, Win#window.start_mark, Pos),
+ edit_buf:move_mark(Buf, point, Pos),
+ Win.
+
+start_of_buffer(State) ->
+ B = buffer(State),
+ edit_buf:move_mark(B, mark, edit_buf:mark_pos(B, point)),
+ edit_buf:move_mark(B, point, 1),
+ State.
+
+end_of_buffer(State) ->
+ B = buffer(State),
+ edit_buf:move_mark(B, mark, edit_buf:mark_pos(B, point)),
+ edit_buf:move_mark(B, point, edit_buf:point_max(B)),
+ State.
+
+mark_whole_buffer(State) ->
+ Buf = buffer(State),
+ edit_buf:move_mark(Buf, point, 1),
+ edit_buf:move_mark(Buf, mark, edit_buf:point_max(Buf)),
+ State.
+
+find_nth(Buf, Direction, Pos, Char, N) ->
+ case edit_buf:walk(Buf, Direction, find_nth_fun(Char, N), Pos) of
+ not_found ->
+ not_found;
+ Offset when Direction == forward ->
+ Pos + Offset;
+ Offset when Direction == backward ->
+ Pos - Offset
+ end.
+
+find_nth_fun(Char, N) -> find_nth_fun(Char, N, 0).
+find_nth_fun(Char, N, Offset) -> find_nth_fun(Char, N, Offset, not_found).
+
+find_nth_fun(Char, N, Offset, Default) ->
+ fun(X) when X == Char ->
+ case N of
+ 1 ->
+ {result, Offset};
+ _ ->
+ {more, find_nth_fun(Char, N-1, Offset+1, Default)}
+ end;
+ (finish) ->
+ {result, Default};
+ (_) ->
+ {more, find_nth_fun(Char, N, Offset+1, Default)}
+ end.
+
+set_mark(State) ->
+ Buf = buffer(State),
+ Point = edit_buf:mark_pos(Buf, point),
+ edit_buf:move_mark(Buf, mark, Point),
+ State.
+
+exchange_point_and_mark(State) ->
+ Buf = buffer(State),
+ Point = edit_buf:mark_pos(Buf, point),
+ Mark = edit_buf:mark_pos(Buf, mark),
+ edit_buf:move_mark(Buf, point, Mark),
+ edit_buf:move_mark(Buf, mark, Point),
+ State.
+
+kill_region(State, KillFlag) ->
+ Buf = buffer(State),
+ Point = edit_buf:mark_pos(Buf, point),
+ Mark = edit_buf:mark_pos(Buf, mark),
+ Text = edit_buf:get_region_cord(Buf, Point, Mark),
+ State1 = case KillFlag of
+ true ->
+ edit_buf:delete(Buf, Point, Mark),
+ State;
+ false ->
+ edit_util:status_msg(State, "Copied")
+ end,
+ KR = edit_var:lookup(killring),
+ edit_var:set(killring, [Text|KR]).
+%%State1#state{killring=[Text|State#state.killring]}.
+
+kill_line(State) ->
+ Buf = buffer(State),
+ P = edit_buf:mark_pos(Buf, point),
+ EOL = end_of_line_pos(Buf),
+ End = if P == EOL ->
+ min(P + 1, edit_buf:point_max(Buf));
+ true ->
+ EOL
+ end,
+ Text = edit_buf:get_region_cord(Buf, P, End),
+ edit_buf:delete(Buf, P, End),
+ %%OldKillring = State#state.killring,
+ OldKillring = edit_var:lookup(killring),
+ NewKillring = case State#state.lastcmd of
+ %% Multiple kill_line's accumulate at the front
+ %% of the kill ring
+ {?MODULE, kill_line, _} ->
+ [cord:join(hd(OldKillring), Text) |
+ tl(OldKillring)];
+ _ ->
+ [Text|OldKillring]
+ end,
+ edit_var:set(killring, NewKillring).
+ %%State#state{killring=NewKillring}.
+
+yank(State) ->
+ case edit_var:lookup(killring) of
+ [] ->
+ State;
+ [Text|_] ->
+ Buf = buffer(State),
+ Point = edit_buf:mark_pos(Buf, point),
+ edit_buf:insert(Buf, Text, Point)
+ end.
+
+undo(State) ->
+ Continuing = case State#state.lastcmd of
+ {?MODULE, undo, []} ->
+ true;
+ _ ->
+ false
+ end,
+ edit_buf:undo(buffer(State), Continuing).
+
+move_to(Buf, Pos) ->
+ edit_buf:move_mark(Buf, point, Pos).
+
+next_window(State) when State#state.windows == [] ->
+ State;
+next_window(State) ->
+ next_window1(State).
+
+next_window1(State) ->
+ [Next | Rest] = State#state.windows,
+ NewState = State#state{curwin=Next,
+ windows = Rest ++ [State#state.curwin]},
+ if (NewState#state.curwin)#window.active == false ->
+ next_window1(NewState);
+ true ->
+ NewState
+ end.
+
+split_window_vertically(State) ->
+ case edit_window:physical_lines(State#state.curwin) of
+ N when N < 4 -> % too small to split
+ State;
+ N ->
+ split_window_vertically1(State)
+ end.
+
+split_window_vertically1(State) ->
+ Orig = State#state.curwin,
+ OrigHeight = edit_window:physical_lines(Orig),
+ TopHeight = trunc(OrigHeight / 2),
+ BottomHeight = OrigHeight - TopHeight,
+ BottomY = Orig#window.y + TopHeight,
+ BottomW = edit_window:make_window(buffer(State),
+ BottomY,
+ Orig#window.width,
+ BottomHeight),
+ State#state{curwin=Orig#window{height=TopHeight},
+ windows=[BottomW | State#state.windows]}.
+
+delete_other_windows(State)
+ when (State#state.curwin)#window.minibuffer == true ->
+ edit_util:status_msg(State, "Minibuffer can't expand to full frame");
+delete_other_windows(State) ->
+ Others = lists:filter(fun(W) -> not W#window.minibuffer end,
+ State#state.windows),
+ ExtraSpace = lists:foldl(fun(W, Sum) -> Sum + W#window.height end,
+ 0,
+ Others),
+ [Minibuffer] = State#state.windows -- Others,
+ OldWin = State#state.curwin,
+ NewWin = OldWin#window{y=0, height = OldWin#window.height + ExtraSpace},
+ State#state{curwin=NewWin,
+ windows=[Minibuffer]}.
+
+delete_window(State) when (State#state.curwin)#window.minibuffer == true ->
+ State;
+delete_window(State) when length(State#state.windows) =< 1 ->
+ State;
+delete_window(State) ->
+ Cur = State#state.curwin,
+ Wins = State#state.windows,
+ [Prev | OthersReversed] = lists:reverse(Wins),
+ Others = lists:reverse(OthersReversed),
+ io:format("Prev.y = ~p~nCur.y = ~p~n", [Prev#window.y, Cur#window.y]),
+ if Prev#window.y > Cur#window.y ->
+ %% Topmost window is being deleted. In this special case,
+ %% its size is allocated to the next (rather than
+ %% previous, i.e. bottom) window.
+ Kin = hd(Wins),
+ Kin1 = Kin#window{height=Kin#window.height + Cur#window.height,
+ y=Cur#window.y},
+ State#state{curwin=Kin1,
+ windows=tl(Wins)};
+ true ->
+ Kin = Prev,
+ Kin1 = Kin#window{height=Kin#window.height + Cur#window.height},
+ Others1 = Others ++ [Kin1],
+ case Others1 of
+ [Next = #window{minibuffer=false} | Rest] ->
+ State#state{curwin=Next,
+ windows=Rest};
+ [MB = #window{minibuffer=true}, Next | Rest] ->
+ State#state{curwin=Next,
+ windows=(Rest ++ [MB])}
+ end
+ end.
+
+-command({quit, [], "Exit the editor process"}).
+quit(State) ->
+ ?EDIT_TERMINAL:teardown(),
+ halt().
+
+-command({printf, [{string, "String:"}],
+ "Print a string to standard output (the file edit.out)"}).
+
+printf(State, String) ->
+ io:format("*** PRINTF: ~s~n", [String]),
+ State.
+
+-command({switch_to_buffer, [{string, "Switch to buffer:"}]}).
+
+switch_to_buffer(State, "") ->
+ VisibleBuffers = [W#window.buffer || W <- edit_util:windows(State)],
+ case State#state.buffers -- VisibleBuffers of
+ [] ->
+ State;
+ [Next|_] ->
+ switch_to_buffer(State, atom_to_list(Next))
+ end;
+switch_to_buffer(State, Name) ->
+ Buffer = list_to_atom(Name),
+ case whereis(Buffer) of % FIXME: how do I know its a buffer?
+ undefined ->
+ edit_util:status_msg(State, "No such buffer: ~s", [Name]);
+ Pid ->
+ Buffers = State#state.buffers,
+ NewBuffers = [Buffer | (Buffers -- [Buffer])],
+ Win = edit_window:attach(State#state.curwin, Buffer),
+ State#state{curwin=Win, buffers=NewBuffers}
+ end.
+
+-command({kill_buffer, [{string, "Kill buffer:"}]}).
+
+kill_buffer(State, "") ->
+ kill_buffer(State, atom_to_list((State#state.curwin)#window.buffer));
+kill_buffer(State, Name) ->
+ BufferName = list_to_atom(Name),
+ case State#state.buffers of
+ [OnlyBuffer] ->
+ State;
+ Buffers ->
+ case whereis(BufferName) of
+ Pid when pid(Pid) ->
+ edit_buf:kill(BufferName),
+ NewBuffers = Buffers -- [BufferName],
+ F = fun(Win) ->
+ case Win#window.buffer of
+ BufferName ->
+ edit_window:attach(Win,
+ hd(NewBuffers));
+ _ ->
+ Win
+ end
+ end,
+ State1 = edit_util:window_map(State, F),
+ io:format("Killing ~p~n", [BufferName]),
+ State1#state{buffers=NewBuffers};
+ undefined ->
+ edit_util:status_msg(State, "No such buffer: ~s", [Name])
+ end
+ end.
+
+min(X,Y) when X<Y -> X;
+min(_,Y) -> Y.
+
+max(X,Y) when X>Y -> X;
+max(_,Y) -> Y.
+
+%% Get the buffer from state - blocks if it's being borrowed by someone else.
+buffer(State) ->
+ Buf = (State#state.curwin)#window.buffer,
+ edit_buf:wait_available(Buf),
+ Buf.
+
+find(Pred, []) ->
+ not_found;
+find(Pred, [H|T]) ->
+ case Pred(H) of
+ true ->
+ H;
+ false ->
+ find(Pred, T)
+ end.
+
+%% FIXME: Expects the *end* of the string to be before the point
+find_string_backward(Buf, String) ->
+ find_string_backward(Buf, String, edit_buf:mark_pos(Buf, point)).
+find_string_backward(Buf, String, Point) ->
+ find_string_backward(Buf, String, Point, not_found).
+find_string_backward(Buf, String, Point, Default) ->
+ find_string_backward1(Buf, String, Point, Default).
+
+find_string_backward1(Buf, String, Point, Default) ->
+ PointMax = edit_buf:point_max(Buf),
+ P = fun(C) -> C == hd(String) end,
+ case min(Point, PointMax - length(String)) of
+ Start when Start =< 0 ->
+ Default;
+ Start ->
+ case find_char_backward(Buf, P, Start) of
+ not_found ->
+ Default;
+ Pos ->
+ case edit_buf:get_region(Buf, Pos, Pos + length(String)) of
+ String ->
+ Pos;
+ _Other when Pos == 1 ->
+ Default;
+ _Other ->
+ find_string_backward1(Buf, String, Pos-1, Default)
+ end
+ end
+ end.
+
+%% Predicate functions on characters.
+%% Functions ending in _P return predicate fun's.
+whitespace_P() -> fun(C) -> lists:member(C, "\n\t ") end.
+alphabet_P() -> fun(C) when C >= $A, C =< $Z -> true;
+ (C) when C >= $a, C =< $z -> true;
+ (_) -> false
+ end.
+numeric_P() -> fun(C) -> (C >= $0) and (C =< $9) end.
+not_whitespace_P() -> not_P(whitespace_P()).
+
+either_P(A, B) -> fun(C) -> A(C) or B(C) end.
+const_P(Char) -> fun(C) -> C == Char end.
+not_P(Pred) -> fun(C) -> not Pred(C) end.
+
+alphanumeric_P() -> either_P(alphabet_P(), numeric_P()).
+
+fundamental_mode(State) ->
+ Buf = buffer(State),
+ edit_buf:set_mode(Buf, fundamental_mode_rec()).
+
+fundamental_mode_rec() ->
+ #mode{name="Fundamental",
+ id=fundamental,
+ keymaps=[]}.
+
+-command({unix_command, [{shell_command, "Shell command:"}]}).
+
+unix_command(State, Cmd) ->
+ %% FIXME: This looks suspiciously system dependent.
+ %% Infact running the command as a port would be much nicer
+ Text = os:cmd("sh -c '(cd "++edit_util:pwd(State)++" ; "++Cmd++")'"),
+ edit_util:popup_message(State, '*Shell Command Output*', Text).
+
+lines(Text) ->
+ length(lists:filter(fun(C) -> C == $\n end, Text)).
+
+-command({search, [{search_string, "Search:"}]}).
+
+search(State, Dir, "") ->
+ Str = edit_var:lookup(prev_search_string, ""),
+ regexp_search1(State, Dir, cord_regexp:escape(Str));
+search(State, Dir, Str) ->
+ edit_var:set(prev_search_string, Str),
+ regexp_search1(State, Dir, cord_regexp:escape(Str)).
+
+-command({regexp_search, [{search_regexp, "Regexp:"}]}).
+
+regexp_search(State, Dir, "") ->
+ %% Blank search string - reuse previous
+ RE = edit_var:lookup(prev_search_regexp, ""),
+ regexp_search1(State, Dir, RE);
+regexp_search(State, Dir, RE) ->
+ edit_var:set(prev_search_regexp, RE),
+ regexp_search1(State, Dir, RE).
+
+regexp_search1(State, Direction, RE) ->
+ B = buffer(State),
+ P = edit_buf:mark_pos(B, point),
+ case edit_buf:regexp_search(B, RE, clip(B, P), Direction) of
+ nomatch ->
+ edit_util:status_msg(State, "Not found: " ++ RE);
+ {match, Start, Len} ->
+ NewPos = case Direction of
+ forward ->
+ Start + Len;
+ backward ->
+ Start - (Len - 1)
+ end,
+ ?debug("Start = ~p Len = ~p~n", [Start, Len]),
+ mark_to_point(B),
+ edit_buf:move_mark(B, point, NewPos)
+ end.
+
+%% Returns: Point' < point_max
+clip(Buf, Point) ->
+ max(1, min(Point, edit_buf:point_max(Buf) - 1)).
+
+mark_to_point(Buf) ->
+ edit_buf:move_mark(Buf, mark, edit_buf:mark_pos(Buf, point)).
+
+%% Null command. Invoked by async processes to get the editor to
+%% redraw.
+nop(State) ->
+ State.
+
+%% ----------------------------------------------------------------------
+%% Debug info
+
+buffer_cord_info(State) ->
+ B = buffer(State),
+ C = edit_buf:get_cord(B),
+ Fmt = "Byte size: ~p ; Max depth: ~p ; Mean leaf size: ~p ; # nodes: ~p",
+ edit_util:status_msg(State,
+ Fmt,
+ [cord:cord_size(C),
+ cord:max_depth(C),
+ cord:mean_leaf_size(C),
+ cord:nr_nodes(C)]).
+
diff --git a/src/edit_make.erl b/src/edit_make.erl
new file mode 100644
index 0000000..20f6cc2
--- /dev/null
+++ b/src/edit_make.erl
@@ -0,0 +1,14 @@
+-module(edit_make).
+-export([start/0]).
+
+start() ->
+ spawn(fun() -> doit() end).
+
+doit() ->
+ sleep(1),
+ ig:gen(edit_ig),
+ c:c(edit_ig),
+ halt().
+
+sleep(Sec) ->
+ receive after Sec*1000 -> true end.
diff --git a/src/edit_mod.erl b/src/edit_mod.erl
new file mode 100644
index 0000000..ae5be54
--- /dev/null
+++ b/src/edit_mod.erl
@@ -0,0 +1,42 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_mod.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Module loader
+%%% Created : 28 Apr 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_mod).
+-author('luke@bluetail.com').
+
+-export([init/0, require/1, load/1]).
+
+init() ->
+ ets:new(?MODULE, [set, named_table, public]),
+ ok.
+
+require(Mod) ->
+ case ets:lookup(?MODULE, Mod) of
+ [] -> % not initialised
+ case load(Mod) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ {error, Reason}
+ end;
+ [_] -> % already initialised
+ ok
+ end.
+
+load(Mod) ->
+ case catch Mod:mod_init() of
+ ok ->
+ ets:insert(?MODULE, {Mod}),
+ ok;
+ {'EXIT', {undef, _}} ->
+ {error, {missing, Mod, mod_init, 0}};
+ {error, Reason} ->
+ {error, Reason};
+ Unexpected ->
+ {error, {unexpected, Unexpected}}
+ end.
+
diff --git a/src/edit_prof.erl b/src/edit_prof.erl
new file mode 100644
index 0000000..08aea4d
--- /dev/null
+++ b/src/edit_prof.erl
@@ -0,0 +1,36 @@
+-module(edit_prof).
+-compile(export_all).
+
+start(Filename) ->
+ Edit = spawn_link(fun() -> edit:start(Filename) end),
+ timer:start_link(),
+ eprof:start(),
+ profiling = eprof:profile([Edit, scratch]),
+ ok = eprof:analyse(),
+ analyse_loop().
+
+analyse_loop() ->
+ receive after 5000 ->
+ eprof:total_analyse()
+ end,
+ analyse_loop().
+
+leader() ->
+ {ok, ProfLog} = file:open("prof.log", [write]),
+ spawn_link(?MODULE, leader_proc, [ProfLog]).
+
+leader_proc(Fd) ->
+ receive
+ {io_request, From, ReplyAs, {put_chars, C}} ->
+ file:write(Fd, C),
+ From ! {io_reply, ReplyAs, ok};
+ {io_request, From, ReplyAs, {put_chars, M, F, A}} ->
+ file:write(Fd, apply(M, F, A)),
+ From ! {io_reply, ReplyAs, ok};
+ {io_request, From, ReplyAs, {get_until, _, _, _}} ->
+ From ! {io_reply, ReplyAs, eof};
+ X ->
+ file:write(Fd, io_lib:format("Unexpected: ~p~n", [X]))
+ end,
+ leader_proc(Fd).
+
diff --git a/src/edit_terminal.erl b/src/edit_terminal.erl
new file mode 100644
index 0000000..b18cafb
--- /dev/null
+++ b/src/edit_terminal.erl
@@ -0,0 +1,77 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_terminal.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : ncurses terminal implementation
+%%% Created : 16 Sep 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_terminal).
+-author('luke@bluetail.com').
+
+-include_lib("slang/include/slang.hrl").
+
+-define(ESC, 27).
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+setup() ->
+ slang:tt_get_terminfo(),
+ slang:kp_init(),
+ slang:init_tty(0, 1, 1),
+ slang:set_abort_signal(null),
+ slang:smg_init_smg (),
+ slang:smg_normal_video(),
+ slang:setvar(newline_behaviour, ?NEWLINE_MOVES),
+ refresh(),
+ ok.
+
+teardown() ->
+ slang:smg_reset_smg(),
+ slang:reset_tty(),
+ ok.
+
+newline() ->
+ put_char($\n).
+
+put_char(C) ->
+ slang:smg_write_char(C).
+
+put_string(S) ->
+ slang:smg_write_string(S).
+
+format(Fmt, Args) ->
+ slang:smg_printf(Fmt, Args).
+
+erase_to_eol() ->
+ slang:smg_erase_eol().
+
+move_to(X, Y) ->
+ slang:smg_gotorc(Y, X).
+
+refresh() ->
+ slang:smg_refresh().
+
+invalidate() ->
+ slang:smg_touch_screen().
+
+width() ->
+ slang:getvar(screen_cols).
+
+height() ->
+ slang:getvar(screen_rows).
+
+read() ->
+ case slang:getkey() of
+ ?ESC ->
+ read() bor 2#10000000;
+ N ->
+ N
+ end.
+
+font_reverse() ->
+ slang:smg_reverse_video().
+
+font_normal() ->
+ slang:smg_normal_video().
+
diff --git a/src/edit_terminal_gterm.erl b/src/edit_terminal_gterm.erl
new file mode 100644
index 0000000..0d6b678
--- /dev/null
+++ b/src/edit_terminal_gterm.erl
@@ -0,0 +1,42 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_terminal_gterm.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : edit_terminal implementation for gterm (Tony's GTK terminal
+%%% emulator)
+%%% Created : 14 Mar 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_terminal_gterm).
+-author('luke@bluetail.com').
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+%% name of terminal process
+-define(TERM, ?MODULE).
+
+setup() ->
+ Term = gterm:run(),
+ register(?TERM, Term),
+% gterm_api:set_local_echo(Term, false),
+% gterm_api:set_auto_scroll(Term, false),
+ Term.
+
+teardown() ->
+ gterm_api:quit().
+
+newline() -> gterm_api:newline(?TERM).
+put_char(C) -> gterm_api:put_char(?TERM, C).
+put_string(S) -> gterm_api:put_string(?TERM, S).
+format(Fmt, Args) -> gterm_api:format(?TERM, Fmt, Args).
+erase_to_eol() -> gterm_api:erase_to_eol(?TERM).
+move_to(X, Y) -> gterm_api:move_to(?TERM, X, Y).
+refresh() -> gterm_api:refresh(?TERM).
+invalidate() -> gterm_api:refresh(?TERM).
+width() -> gterm_api:width(?TERM).
+height() -> gterm_api:height(?TERM).
+read() -> gterm_api:read(?TERM).
+font_reverse() -> gterm_api:font_reverse(?TERM).
+font_normal() -> gterm_api:font_normal(?TERM).
+
+
diff --git a/src/edit_text.erl b/src/edit_text.erl
new file mode 100644
index 0000000..3bcc7d9
--- /dev/null
+++ b/src/edit_text.erl
@@ -0,0 +1,148 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_text.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Text data structure with markers and undo
+%%% Created : 2 Oct 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_text).
+-author('luke@bluetail.com').
+
+-export([new/0, new/1, replace/4, add_mark/4, move_mark/3, mark_pos/2,
+ undo/2, cord/1, walk_backward/3, walk_forward/3]).
+
+-record(text, {cord, marks=[], undo=[], running_undo=[]}).
+
+-record(mark, {name, pos, direction}).
+
+%% ----------------------------------------------------------------------
+%% new
+
+new() ->
+ new(<<>>).
+
+new(BCS) when list(BCS) ->
+ new(cord:new(BCS));
+new(Cord) ->
+ #text{cord=Cord}.
+
+%% ----------------------------------------------------------------------
+%% replace (CBS means Cord, Binary, or String)
+
+replace(Text0 = #text{cord=Cord0,marks=Marks0,undo=Undo0}, CBS, Start, Len) ->
+ Cord1 = cord:replace(Cord0, CBS, Start, Len),
+ Marks1 = update_marks(Marks0, Start, Start+Len, cbs_length(CBS)),
+ Undo1 = [{Cord0, Marks0} | Undo0],
+ %% FIXME: maybe better to chain undo through states (i.e. just
+ %% know about previous state, which knows about previous state,
+ %% ...) - if that plays nicely with "running undo"
+ Text0#text{cord=Cord1, marks=Marks1, undo=Undo1, running_undo=[]}.
+
+cbs_length(L) when list(L) -> length(L);
+cbs_length(B) when binary(B) -> size(B);
+cbs_length(C) -> cord:cord_size(C).
+
+update_marks(Marks, Start, End, Len) ->
+ [update_mark(Mark,Start,End,Len) || Mark <- Marks].
+
+%% Inside the replaced region (includes being on an inserted character)
+update_mark(Mark, Start, End, Len)
+ when Mark#mark.pos >= Start,
+ Mark#mark.pos < End;
+ Mark#mark.pos == Start ->
+ case Mark#mark.direction of
+ forward ->
+ Mark#mark{pos=Start+Len};
+ backward ->
+ Mark#mark{pos=Start}
+ end;
+%% After the replaced region
+update_mark(Mark, Start, End, Len) when Mark#mark.pos >= End ->
+ %% We move forward by the length-delta
+ Mark#mark{pos = Mark#mark.pos - (End - Start) + Len};
+%% Before the replaced region
+update_mark(Mark, _Start, _End, _Len) ->
+ Mark.
+
+%% ----------------------------------------------------------------------
+%% add_mark
+
+add_mark(Text = #text{marks=Marks0}, Name, Pos, Direction) ->
+ Mark = #mark{name=Name, pos=Pos, direction=Direction},
+ Text#text{marks=[Mark|Marks0]}.
+
+%% ----------------------------------------------------------------------
+%% move_mark
+
+move_mark(Text0, Name, Pos) ->
+ {Front, [M | Back]} =
+ lists:splitwith(fun(X) -> X#mark.name /= Name end,
+ Text0#text.marks),
+ NewMarks = Front ++ [M#mark{pos=Pos} | Back],
+ Text0#text{marks=NewMarks}.
+
+%% ----------------------------------------------------------------------
+%% mark_pos
+
+mark_pos(#text{marks=Marks}, Name) ->
+ {found, Mark} = find(fun(M) -> M#mark.name == Name end, Marks),
+ Mark#mark.pos.
+
+find(Pred, []) ->
+ not_found;
+find(Pred, [H|T]) ->
+ case Pred(H) of
+ true ->
+ {found, H};
+ false ->
+ find(Pred, T)
+ end.
+
+%% ----------------------------------------------------------------------
+%% walk_backward
+
+walk_backward(Text = #text{cord=Cord}, Fun, Pos) ->
+ Size = cord:cord_size(Cord),
+ if Pos == Size + 1 ->
+ case Fun(not_there) of
+ {more, FNext} ->
+ walk_backward(Text, FNext, Pos-1);
+ {result, R} ->
+ R
+ end;
+ true ->
+ cord:walk(Cord, Pos, backward, Fun)
+ end.
+
+%% ----------------------------------------------------------------------
+%% walk_forward
+
+walk_forward(#text{cord=Cord}, Fun, Pos) ->
+ cord:walk(Cord, Pos, forward, Fun).
+
+%% ----------------------------------------------------------------------
+%% undo
+
+undo(Text0 = #text{undo=Undo, running_undo=Running}, Continuing) ->
+ Text1 = case Continuing of
+ true ->
+ Text0;
+ false ->
+ Text0#text{running_undo=Undo}
+ end,
+ case Running of
+ %% Nothing to undo - recycle redo list
+ [] ->
+ Text1#text{running_undo=Undo};
+ [{UndoCord, UndoMarks}|Undos] ->
+ Text1#text{cord=UndoCord,
+ marks=UndoMarks,
+ undo=[{Text0#text.cord, Text0#text.marks} | Undo],
+ running_undo=Undos}
+ end.
+
+%% ----------------------------------------------------------------------
+%% cord
+
+cord(#text{cord=Cord}) -> Cord.
+
diff --git a/src/edit_transform.erl b/src/edit_transform.erl
new file mode 100644
index 0000000..52bd294
--- /dev/null
+++ b/src/edit_transform.erl
@@ -0,0 +1,69 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_transform.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Parse transform module for editor command modules
+%%% Created : 25 Oct 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+% -command({cmd1, [{name1, prompt1}], doc1}).
+% -command({cmd2, []}).
+
+-module(edit_transform).
+-author('luke@bluetail.com').
+
+-export([parse_transform/2]).
+
+%% Collect "command" attributes to generate a command_info function. These
+%% like (interactive ...) declarations in ELisp. See edit_lib for examples.
+%%
+%% command grammar:
+%% -command({Fun, [Arg]}) | -command({Fun, [Arg], DocString})
+%% Arg = {TypeAtom, PromptString}
+%%
+%% command_info() returns [{Fun, [Arg], DocString}]
+%%
+parse_transform(Form, Opts) ->
+ {Head, Rest} = split(Form),
+ {Body, EOF} = splitlast(Rest),
+ Line = element(2, hd(Body)),
+ {Cmds, NewBody} = separate(Body),
+ ListSrc = lists:flatten(io_lib:format("~p", [Cmds])),
+ Export = scan_and_parse("-export([command_info/0]).", Line),
+ Fun = scan_and_parse("command_info() -> " ++ ListSrc ++ ".", Line),
+ NewForm = Head ++ [Export] ++ NewBody ++ [Fun, EOF],
+ NewForm.
+
+%% => {Commands, FilteredBody}
+%% Commands = {Name, Params, DocString}
+separate(Body) ->
+ separate(Body, [], []).
+
+separate([{attribute, _Line, command, {Name, Params, Doc}}|T], Acc1, Acc2) ->
+ separate(T, [{Name, Params, Doc}|Acc1], Acc2);
+separate([{attribute, _Line, command, {Name, Params}}|T], Acc1, Acc2) ->
+ separate(T, [{Name, Params, ""}|Acc1], Acc2);
+separate([H|T], Acc1, Acc2) ->
+ separate(T, Acc1, [H|Acc2]);
+separate([], Acc1, Acc2) ->
+ {lists:reverse(Acc1), lists:reverse(Acc2)}.
+
+scan_and_parse(Source, Line) ->
+ {ok, FunTokens, _} = erl_scan:string(Source, Line),
+ {ok, FunParse} = erl_parse:parse_form(FunTokens),
+ FunParse.
+
+%% Split into {Head, Body}. Head is everything upto and including the
+%% 'module' attribute, Body is the rest.
+split(Form) ->
+ split(Form, []).
+split([X = {attribute, Line, module, Module}|T], Acc) ->
+ {lists:reverse([X|Acc]), T};
+split([H|T], Acc) ->
+ split(T, [H|Acc]).
+
+splitlast(L) ->
+ splitlast(L, []).
+splitlast([H], Acc) -> {lists:reverse(Acc), H};
+splitlast([H|T], Acc) -> splitlast(T, [H|Acc]).
+
+
diff --git a/src/edit_util.erl b/src/edit_util.erl
new file mode 100644
index 0000000..c539277
--- /dev/null
+++ b/src/edit_util.erl
@@ -0,0 +1,245 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_util.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Utility functions
+%%% Created : 15 Oct 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_util).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-export([keyname/1, status_msg/2, status_msg/3, update_minibuffer_window/2,
+ select_window/2, update/3]).
+
+-compile(export_all).
+
+keyname(263) -> "C-h";
+keyname(127) -> "C-h";
+keyname(272) -> "C-h";
+keyname(C) ->
+ case ctrl_p(C) of
+ true ->
+ "C-" ++ keyname(tolower(unctrl(C)));
+ false ->
+ case meta_p(C) of
+ true ->
+ "M-" ++ keyname(tolower(unmeta(C)));
+ false ->
+ [C]
+ end
+ end.
+
+%% Eighth bit set means meta
+meta_p(C) -> C band meta_bit() /= 0.
+unmeta(C) -> C band (bnot meta_bit()).
+
+meta_bit() -> 2#10000000.
+
+ctrl_p(C) -> Base = unmeta(C), ((Base band 2#01000000) == 0) and (Base < 32).
+unctrl(C) -> C + 64.
+
+tolower(C) when C >= $A, C =< $Z ->
+ C - $A + $a;
+tolower(C) ->
+ C.
+
+%% Show a one-line message in the status line (where the minibuffer is)
+status_msg(State, Fmt, Args) ->
+ status_msg(State, io_lib:format(Fmt, Args)).
+status_msg(State, String) ->
+ Text = lists:flatten(String),
+ AllWindows = [State#state.curwin | State#state.windows],
+ NewWindows = update(fun(Win) -> Win#window{status_text=Text} end,
+ fun(Win) -> Win#window.minibuffer end,
+ AllWindows),
+ State#state{curwin=hd(NewWindows),
+ windows=tl(NewWindows)}.
+
+kill_newlines([]) ->
+ [];
+kill_newlines([$\n|T]) ->
+ [$ |
+ kill_newlines(lists:dropwhile(fun(C) -> C == $ end, T))];
+kill_newlines([H|T]) ->
+ [H|kill_newlines(T)].
+
+%% Apply Fun to minibuffer window and put the result in the state.
+update_minibuffer_window(State, Fun) ->
+ AllWindows = [State#state.curwin | State#state.windows],
+ NewWindows = update(Fun,
+ fun(Win) -> Win#window.minibuffer end,
+ AllWindows),
+ State#state{curwin=hd(NewWindows),
+ windows=tl(NewWindows)}.
+
+%% Map over all windows in State with Fun.
+%% Fun is fun(Win) -> Win'
+window_map(State, Fun) ->
+ State#state{curwin=Fun(State#state.curwin),
+ windows=lists:map(Fun, State#state.windows)}.
+
+%% Return a list of all windows in State.
+windows(State) ->
+ [State#state.curwin | State#state.windows].
+
+%% Use F to update the first item of L which satisfies P.
+%% e.g.
+update(F, P, []) ->
+ [];
+update(F, P, [H|T]) ->
+ case P(H) of
+ true ->
+ [F(H)|T];
+ false ->
+ [H|update(F, P, T)]
+ end.
+
+%% Select the first window satisfying P, or crash if no match
+select_window(State, P) ->
+ case P(State#state.curwin) of
+ true ->
+ State;
+ false ->
+ select_window1(State, P, State#state.windows, [State#state.curwin])
+ end.
+
+select_window1(State, P, [], Acc) ->
+ exit(nomatch);
+select_window1(State, P, [H|T], Acc) ->
+ case P(H) of
+ true ->
+ State#state{curwin=H,
+ windows=T ++ lists:reverse(Acc)};
+ false ->
+ select_window1(State, P, T, [H|Acc])
+ end.
+
+set_buffer(State, Buffer) ->
+ Win = edit_window:attach(State#state.curwin, Buffer),
+ State#state{curwin = Win,
+ buffers = [Buffer | (State#state.buffers -- [Buffer])]}.
+
+%% Display the message `String'. If it's only one line, it's shown in
+%% the status line. Otherwise, a buffer called `Buf' is popped up to
+%% show it.
+popup_message(State, Buf, String) ->
+ popup_message(State, Buf, "~s", [String]).
+
+popup_message(State, Buf, Format, Args) ->
+ String = lists:flatten(io_lib:format(Format, Args)),
+ case monoline(String) of
+ {true, Line} ->
+ edit_util:status_msg(State, Line);
+ false ->
+ edit_buf:new(Buf), % might be already started, s'ok
+ edit_buf:set_text(Buf, String),
+ popup_buffer(State, Buf)
+ end.
+
+%% See if a string fits on one line.
+%% Returns: {true, Line} | false
+%% Where Line is the same as the input string, but with no newlines.
+monoline(S) -> monoline(S, []).
+
+monoline([$\n], Acc) -> {true, lists:reverse(Acc)};
+monoline([], Acc) -> {true, lists:reverse(Acc)};
+monoline([$\n|T], Acc) -> false;
+monoline([H|T], Acc) -> monoline(T, [H|Acc]).
+
+%% Make `Buffer' visible (but not selected) in a window - either by hijacking
+%% an existing window or creating a new one.
+popup_buffer(State, Buffer) ->
+ case lists:any(fun(W) -> W#window.buffer == Buffer end, windows(State)) of
+ true ->
+ %% already visible
+ State;
+ false ->
+ popup_buffer1(State, Buffer)
+ end.
+
+popup_buffer1(State = #state{windows=[MB]}, Buffer)
+ when MB#window.minibuffer == true ->
+ %% Clause: One normal window, which is curwin. Split it.
+ {A, B} = split_window(State#state.curwin),
+ popup_buffer1(State#state{curwin=A,
+ windows=[B,MB]},
+ Buffer);
+popup_buffer1(State = #state{windows=[Win]}, Buffer)
+ when (State#state.curwin)#window.minibuffer == true ->
+ %% Clause: One normal window, minibuffer is curwin. Split normal
+ %% window.
+ {A, B} = split_window(Win),
+ popup_buffer1(State#state{windows=[A,B]}, Buffer);
+popup_buffer1(State, Buffer) when State#state.windows > 1 ->
+ %% OK! The first non-minibuffer is used.
+ State#state{windows=popup_buffer_in(State#state.windows, Buffer)}.
+
+popup_buffer_in([W|Ws], Buffer) when W#window.minibuffer == false ->
+ [edit_window:attach(W, Buffer) | Ws];
+popup_buffer_in([MB|Ws], Buffer) ->
+ [MB | popup_buffer_in(Ws, Buffer)].
+
+split_window(W) ->
+ Height = W#window.height,
+ HeightA = trunc(Height / 2),
+ HeightB = Height - HeightA,
+ io:format("Height = ~p~nHeightA = ~p~nHeightB = ~p~n",
+ [Height, HeightA, HeightB]),
+ New = edit_window:make_window(W#window.buffer,
+ W#window.y + HeightA,
+ W#window.width,
+ HeightB),
+ {W#window{height=HeightA}, New}.
+
+%% spawn process with some borrowed buffers
+
+spawn_with(Buffer, M, F, A) ->
+ spawn_with(Buffer, {M, F, A}).
+
+spawn_with(Buffers, What) ->
+ Ref = make_ref(),
+ Pid = spawn_link(?MODULE, spawn_with_init, [self(), Ref, Buffers, What]),
+ receive {ready, Ref} -> ok end.
+
+spawn_with_init(Pid, Ref, Buffers, What) ->
+ lists:foreach(fun(Buf) -> edit_buf:borrow(Buf) end,
+ Buffers),
+ Pid ! {ready, Ref},
+ spawn_with_apply(What),
+ %% we miss this redraw if the command crashes. oops.
+ edit:invoke_later(?MODULE, redraw, []).
+
+spawn_with_apply({M, F, A}) ->
+ apply(M, F, A);
+spawn_with_apply(Fun) when function(Fun) ->
+ Fun().
+
+
+redraw(State) ->
+ %% Actually we needn't do anything, because the screen redraws
+ %% after each command. (What a pointless abstraction :-)
+ State.
+
+%% Get the current working directory for the state or buffer.
+pwd(State) when record(State, state) ->
+ pwd((State#state.curwin)#window.buffer);
+pwd(Buf) when atom(Buf) ->
+ case edit_buf:get_filename(Buf) of
+ undefined ->
+ case file:get_cwd() of
+ {ok, Path} ->
+ case lists:last(Path) of
+ $/ ->
+ Path;
+ _ ->
+ Path ++ "/"
+ end;
+ _ ->
+ undefined
+ end;
+ Filename ->
+ filename:dirname(Filename)++"/"
+ end.
+
diff --git a/src/edit_var.erl b/src/edit_var.erl
new file mode 100644
index 0000000..0af74b4
--- /dev/null
+++ b/src/edit_var.erl
@@ -0,0 +1,171 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_var.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Variable management server - transient and persistent
+%%% Created : 21 Jan 2001 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+%%% This module implements "setq"-like variables. But, this seems a bit
+%%% distasteful because of concurrent updates and so on. Maybe there is
+%%% better way to do variables in general (or just program-internal
+%%% variables).
+
+-module(edit_var).
+-author('luke@bluetail.com').
+
+%%-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+-behaviour(gen_server).
+
+%% External exports
+-export([start_link/0]).
+
+-export([lookup/1, lookup/2, set/2, permanent/2, add_to_list/2]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-record(state, {ets, dets}).
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+start_link() ->
+ gen_server:start_link({local, edit_var}, edit_var, [], []).
+
+%% Return: Value | undefined
+lookup(Name) ->
+ lookup(Name, undefined).
+
+lookup(Name, Default) ->
+ gen_server:call(?MODULE, {lookup, Name, Default}).
+
+set(Name, Value) ->
+ gen_server:call(?MODULE, {set, Name, Value}).
+
+%% permanent(Name, true | false)
+permanent(Name, Flag) ->
+ gen_server:call(?MODULE, {permanent, Name, Flag}).
+
+add_to_list(Name, Value) ->
+ List = lookup(Name, []),
+ edit_var:set(Name, include(List, Value)).
+
+include([], Value) -> [Value];
+include([Value|T], Value) -> [Value|T];
+include([H|T], Value) -> [H|include(T, Value)].
+
+%%%----------------------------------------------------------------------
+%%% Callback functions from gen_server
+%%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+%% Func: init/1
+%% Returns: {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%%----------------------------------------------------------------------
+init([]) ->
+ Filename = filename:join(os:getenv("HOME"), "edit_var.dets"),
+ Ets = ets:new(edit_mem_var, [set, public, named_table]),
+ {ok, Dets} = dets:open_file(edit_disk_var,
+ [{type, set},
+ {file, Filename}]),
+ load_file(Dets, Ets),
+ State = #state{ets=Ets,
+ dets=Dets},
+ {ok, State}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_call/3
+%% Returns: {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} | (terminate/2 is called)
+%% {stop, Reason, State} (terminate/2 is called)
+%%----------------------------------------------------------------------
+handle_call({lookup, Name, Default}, From, State) ->
+ {reply, do_lookup(State, Name, Default), State};
+
+handle_call({set, Name, Value}, From, State) ->
+ ets:insert(State#state.ets, {Name, Value}),
+ case is_permanent(State, Name) of
+ true ->
+ dets:insert(State#state.dets, {Name, Value});
+ false ->
+ ok
+ end,
+ {reply, ok, State};
+
+handle_call({permanent, Name, Flag}, From, State) ->
+ case Flag of
+ false ->
+ dets:delete(State#state.dets, Name);
+ true ->
+ dets:insert(State#state.dets, {Name, do_lookup(State, Name)})
+ end,
+ {reply, ok, State}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_cast/2
+%% Returns: {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State} (terminate/2 is called)
+%%----------------------------------------------------------------------
+handle_cast(Msg, State) ->
+ {noreply, State}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_info/2
+%% Returns: {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State} (terminate/2 is called)
+%%----------------------------------------------------------------------
+handle_info(Info, State) ->
+ {noreply, State}.
+
+%%----------------------------------------------------------------------
+%% Func: terminate/2
+%% Purpose: Shutdown the server
+%% Returns: any (ignored by gen_server)
+%%----------------------------------------------------------------------
+terminate(Reason, State) ->
+ ok.
+
+%%----------------------------------------------------------------------
+%% Func: code_change/3
+%% Purpose: Convert process state when code is changed
+%% Returns: {ok, NewState}
+%%----------------------------------------------------------------------
+code_change(OldVsn, State, Extra) ->
+ {ok, State}.
+
+%%%----------------------------------------------------------------------
+%%% Internal functions
+%%%----------------------------------------------------------------------
+
+load_file(Dets, Ets) ->
+ F = fun(X) -> ets:insert(Ets, X) end,
+ dets:traverse(Dets, F).
+
+do_lookup(State, Name) ->
+ do_lookup(State, Name, undefined).
+
+do_lookup(State, Name, Default) ->
+ case ets:lookup(State#state.ets, Name) of
+ [{_, Value}] ->
+ Value;
+ [] ->
+ Default
+ end.
+
+is_permanent(State, Name) ->
+ case dets:lookup(State#state.dets, Name) of
+ [] ->
+ false;
+ _ ->
+ true
+ end.
diff --git a/src/edit_window.erl b/src/edit_window.erl
new file mode 100644
index 0000000..a14c047
--- /dev/null
+++ b/src/edit_window.erl
@@ -0,0 +1,45 @@
+%%%----------------------------------------------------------------------
+%%% File : edit_window.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Window handling functions
+%%% Created : 14 Oct 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(edit_window).
+-author('luke@bluetail.com').
+
+-include_lib("ermacs/include/edit.hrl").
+
+-compile(export_all).
+%%-export([Function/Arity, ...]).
+
+%% NB: Height is the total height including modeline
+make_window(Buffer, Y, Width, Height) ->
+ Id = make_ref(),
+ W = #window{start_mark={start, Id},
+ y=Y,
+ width=Width,
+ height=Height,
+ id=Id},
+ attach(W, Buffer).
+
+%% Number of lines for viewing text - excludes modeline
+text_lines(W) when W#window.minibuffer == true ->
+ physical_lines(W);
+text_lines(W) ->
+ physical_lines(W) - 1.
+
+physical_lines(W) ->
+ W#window.height.
+
+width(W) ->
+ W#window.width.
+
+%% "Attach" a window to a buffer. Puts a mark in the buffer so that
+%% the window knows where it's up to.
+attach(Window, Buffer) ->
+ attach(Window, Buffer, 1).
+attach(Window, Buffer, Start) ->
+ edit_buf:add_mark(Buffer, Window#window.start_mark, 1, backward),
+ Window#window{buffer=Buffer}.
+
diff --git a/src/ermacs.in b/src/ermacs.in
new file mode 100644
index 0000000..77fa8c8
--- /dev/null
+++ b/src/ermacs.in
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+BASEDIR=%BASEDIR%
+
+SLANG_EBIN_DIR=${BASEDIR}/../slang/ebin
+
+erl -pa ${SLANG_EBIN_DIR} -pa ${BASEDIR}/ebin -pa ${BASEDIR}/mods/ebin \
+ -noshell -s edit start $*
+
+# Can I get bash to run this in response to SIGINT?
+stty sane
+
diff --git a/src/file_gl.erl b/src/file_gl.erl
new file mode 100644
index 0000000..832df73
--- /dev/null
+++ b/src/file_gl.erl
@@ -0,0 +1,62 @@
+%%%----------------------------------------------------------------------
+%%% File : file_gl.erl
+%%% Author : Luke Gorrie <luke@bluetail.com>
+%%% Purpose : Group leader server for writing to a file
+%%% Created : 22 Oct 2000 by Luke Gorrie <luke@bluetail.com>
+%%%----------------------------------------------------------------------
+
+-module(file_gl).
+-author('luke@bluetail.com').
+
+-behaviour(gen_server).
+
+%% External exports
+-export([start_link/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_info/2, terminate/2, code_change/3,
+ handle_call/3, handle_cast/2]).
+
+-record(state, {fd}).
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+start_link(Filename) ->
+ gen_server:start_link({local, file_gl}, file_gl, Filename, []).
+
+%%%----------------------------------------------------------------------
+%%% Callback functions from gen_server
+%%%----------------------------------------------------------------------
+
+init(Filename) ->
+ {ok, Fd} = file:open(Filename, [write]),
+ {ok, #state{fd=Fd}}.
+
+handle_info({io_request, From, ReplyAs, {put_chars, C}}, State) ->
+ file:write(State#state.fd, C),
+ From ! {io_reply, ReplyAs, ok},
+ {noreply, State};
+handle_info({io_request, From, ReplyAs, {put_chars, M, F, A}}, State) ->
+ file:write(State#state.fd, apply(M, F, A)),
+ From ! {io_reply, ReplyAs, ok},
+ {noreply, State};
+handle_info({io_request, From, ReplyAs, {get_until, _, _, _}}, State) ->
+ From ! {io_reply, ReplyAs, eof},
+ {noreply, State};
+handle_info(Info, State) ->
+ {noreply, State}.
+
+handle_call(_, _, _) ->
+ exit(no_such_callback).
+
+handle_cast(_, _) ->
+ exit(no_such_callback).
+
+terminate(Reason, State) ->
+ file:close(State#state.fd),
+ ok.
+
+code_change(OldVsn, State, Extra) ->
+ {ok, State}.
+