aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/edit_text.erl
blob: 2c8359477482cce6d3f4eef274c10c8cb0fed0ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
-module(edit_text).

-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 is_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 is_list(L)   -> length(L);
cbs_length(B) when is_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.