diff options
author | Federico Igne <undyamon@disroot.org> | 2024-01-11 19:31:31 +0100 |
---|---|---|
committer | Federico Igne <undyamon@disroot.org> | 2024-01-11 19:31:31 +0100 |
commit | 055c743c55bde27f4475d3434c26d8383c0c3ea1 (patch) | |
tree | aabf2173a9995f5795da86d5676181b62fee0e9e | |
parent | 416c56656af65d656f637dc8c8fdb62d0ba03e29 (diff) | |
download | sandy-055c743c55bde27f4475d3434c26d8383c0c3ea1.tar.gz sandy-055c743c55bde27f4475d3434c26d8383c0c3ea1.zip |
bulk: add PoC of vim-like modular editor
-rw-r--r-- | bin/main.ml | 9 | ||||
-rw-r--r-- | lib/command.ml | 115 | ||||
-rw-r--r-- | lib/config.ml | 9 | ||||
-rw-r--r-- | lib/editor.ml | 271 | ||||
-rw-r--r-- | lib/editorBuffer.ml | 72 | ||||
-rw-r--r-- | lib/key.ml | 72 | ||||
-rw-r--r-- | lib/modes.ml | 18 | ||||
-rw-r--r-- | lib/terminal.ml | 130 | ||||
-rw-r--r-- | lib/terminal.mli | 67 | ||||
-rw-r--r-- | lib/text.ml | 10 | ||||
-rw-r--r-- | lib/util.ml | 26 | ||||
-rw-r--r-- | lib/zipper.ml | 131 | ||||
-rw-r--r-- | lib/zipper.mli | 258 |
13 files changed, 1187 insertions, 1 deletions
diff --git a/bin/main.ml b/bin/main.ml index 7bf6048..75328c9 100644 --- a/bin/main.ml +++ b/bin/main.ml | |||
@@ -1 +1,8 @@ | |||
1 | let () = print_endline "Hello, World!" | 1 | open Sand |
2 | |||
3 | let () = | ||
4 | let open Editor in | ||
5 | let cli = Config.parse Sys.argv in | ||
6 | let editor = Editor.init cli in | ||
7 | let rec loop () = Action.(render *> handle_next_command2 >>= loop) in | ||
8 | Action.eval ~editor loop | ||
diff --git a/lib/command.ml b/lib/command.ml new file mode 100644 index 0000000..7dedc6c --- /dev/null +++ b/lib/command.ml | |||
@@ -0,0 +1,115 @@ | |||
1 | open Base | ||
2 | open Key | ||
3 | |||
4 | type register = char option | ||
5 | type count = int option | ||
6 | type operation = Noop | Yank | Paste | Delete | Change | ||
7 | type scope = Line | To_bol | To_eol | Down | Left | Right | Up | ||
8 | |||
9 | type command = | ||
10 | | Type of char | ||
11 | | Simple of Key.t | ||
12 | | Partial of Key.t | ||
13 | | Shortcut of register * count * operation * scope | ||
14 | | Chord of register * count * operation * count * scope | ||
15 | |||
16 | type t = command | ||
17 | |||
18 | let shortcut ?r ?n c s = Shortcut (r, n, c, s) | ||
19 | let chord ?r ?n1 c ?n2 m = Chord (r, n1, c, n2, m) | ||
20 | |||
21 | let i_stream = | ||
22 | let step s k = | ||
23 | let open Sequence.Step in | ||
24 | match (s, k) with | ||
25 | | `start, Key c -> Yield { value = Type c; state = `start } | ||
26 | | `start, _ -> Yield { value = Simple k; state = `start } | ||
27 | | _, _ -> Skip { state = `start } | ||
28 | in | ||
29 | Sequence.unfold_with ~init:`start ~f:step Key.stream | ||
30 | |||
31 | let simple_movements = | ||
32 | [ | ||
33 | Key 'h'; | ||
34 | Key 'j'; | ||
35 | Key 'k'; | ||
36 | Key 'l'; | ||
37 | Key ' '; | ||
38 | Arrow_up; | ||
39 | Arrow_down; | ||
40 | Arrow_left; | ||
41 | Arrow_right; | ||
42 | Backspace; | ||
43 | ] | ||
44 | |||
45 | let to_scope = function | ||
46 | | Key 'j' | Arrow_down -> Down | ||
47 | | Key 'h' | Arrow_left | Backspace -> Left | ||
48 | | Key 'l' | Key ' ' | Arrow_right -> Right | ||
49 | | Key 'k' | Arrow_up -> Up | ||
50 | | _ -> failwith "Invalid motion." | ||
51 | |||
52 | let n_stream = | ||
53 | let step s k = | ||
54 | let open Sequence.Step in | ||
55 | let is_chord_op c = String.contains "ydc" (Char.lowercase c) in | ||
56 | let is_simple_movement k = List.mem ~equal:Poly.equal simple_movements k in | ||
57 | let to_op c = | ||
58 | match Char.lowercase c with | ||
59 | | 'y' -> Yank | ||
60 | | 'p' -> Paste | ||
61 | | 'd' -> Delete | ||
62 | | 'c' -> Change | ||
63 | | _ -> failwith "Invalid operation in chord." | ||
64 | in | ||
65 | match (s, k) with | ||
66 | | `start, Key '"' -> Yield { value = Partial k; state = `chord_reg_pre } | ||
67 | | `chord_reg_pre, Key c -> Yield { value = Partial k; state = `chord_reg c } | ||
68 | | `chord_reg r, Key n when Char.('1' <= n && n <= '9') -> | ||
69 | let n = Char.to_int n - 48 in | ||
70 | Yield { value = Partial k; state = `chord_n (Some r, n) } | ||
71 | | `start, Key n when Char.('1' <= n && n <= '9') -> | ||
72 | let n = Char.to_int n - 48 in | ||
73 | Yield { value = Partial k; state = `chord_n (None, n) } | ||
74 | | `chord_n (r, m), Key n when Char.('0' <= n && n <= '9') -> | ||
75 | let n = (10 * m) + Char.to_int n - 48 in | ||
76 | Yield { value = Partial k; state = `chord_n (r, n) } | ||
77 | | `start, Key c when is_chord_op c -> | ||
78 | if Char.is_uppercase c then | ||
79 | Yield { value = shortcut (to_op c) To_eol; state = `start } | ||
80 | else | ||
81 | Yield { value = Partial k; state = `chord_cmd (None, None, to_op c) } | ||
82 | | `chord_reg r, Key c when is_chord_op c -> | ||
83 | if Char.is_uppercase c then | ||
84 | Yield { value = shortcut ~r (to_op c) To_eol; state = `start } | ||
85 | else | ||
86 | Yield | ||
87 | { value = Partial k; state = `chord_cmd (Some r, None, to_op c) } | ||
88 | | `chord_n (r, n), Key c when is_chord_op c -> | ||
89 | if Char.is_uppercase c then | ||
90 | Yield { value = shortcut ?r ~n (to_op c) To_eol; state = `start } | ||
91 | else | ||
92 | Yield { value = Partial k; state = `chord_cmd (r, Some n, to_op c) } | ||
93 | | `chord_cmd (r, n, c), Key ch when is_chord_op ch && Poly.(c = to_op ch) -> | ||
94 | if Char.is_uppercase ch then | ||
95 | Yield { value = shortcut ?r ?n c To_bol; state = `start } | ||
96 | else Yield { value = shortcut ?r ?n c Line; state = `start } | ||
97 | | (`start | `chord_reg _), k when is_simple_movement k -> | ||
98 | Yield { value = chord Noop (to_scope k); state = `start } | ||
99 | | `chord_n (_, n), k when is_simple_movement k -> | ||
100 | Yield { value = chord ~n1:n Noop (to_scope k); state = `start } | ||
101 | | `chord_cmd (r, n, c), k when is_simple_movement k -> | ||
102 | Yield { value = chord ?r ?n1:n c (to_scope k); state = `start } | ||
103 | | `chord_cmd (r, n1, c), Key n when Char.('1' <= n && n <= '9') -> | ||
104 | let n = Char.to_int n - 48 in | ||
105 | Yield { value = Partial k; state = `chord_mv_n (r, n1, c, n) } | ||
106 | | `chord_mv_n (r, n1, c, n2), Key n when Char.('0' <= n && n <= '9') -> | ||
107 | let n2 = (10 * n2) + Char.to_int n - 48 in | ||
108 | Yield { value = Partial k; state = `chord_mv_n (r, n1, c, n2) } | ||
109 | | `chord_mv_n (r, n1, c, n2), k when is_simple_movement k -> | ||
110 | Yield { value = chord ?r ?n1 c ~n2 (to_scope k); state = `start } | ||
111 | (* Catch-all rules *) | ||
112 | | `start, _ -> Yield { value = Simple k; state = `start } | ||
113 | | _, _ -> Skip { state = `start } | ||
114 | in | ||
115 | Sequence.unfold_with ~init:`start ~f:step Key.stream | ||
diff --git a/lib/config.ml b/lib/config.ml new file mode 100644 index 0000000..4ebf436 --- /dev/null +++ b/lib/config.ml | |||
@@ -0,0 +1,9 @@ | |||
1 | open Base | ||
2 | |||
3 | type config = { | ||
4 | files : string list; | ||
5 | } | ||
6 | type t = config | ||
7 | |||
8 | let parse args = | ||
9 | { files = List.(of_array args |> tl_exn) } | ||
diff --git a/lib/editor.ml b/lib/editor.ml new file mode 100644 index 0000000..c3e298e --- /dev/null +++ b/lib/editor.ml | |||
@@ -0,0 +1,271 @@ | |||
1 | open Base | ||
2 | module Buffer = EditorBuffer | ||
3 | open Util | ||
4 | |||
5 | type mode = Normal | Insert | ||
6 | type cursor = int * int | ||
7 | |||
8 | type editor = { | ||
9 | term : Terminal.state; | ||
10 | mode : mode; | ||
11 | offset : int * int; | ||
12 | cursor : cursor; | ||
13 | buffer : Buffer.t option; | ||
14 | pending : Key.t Sequence.t; | ||
15 | i_pending : Command.t Sequence.t; | ||
16 | n_pending : Command.t Sequence.t; | ||
17 | } | ||
18 | |||
19 | type t = editor | ||
20 | |||
21 | let init (c : Config.t) : editor = | ||
22 | { | ||
23 | term = Terminal.init (); | ||
24 | mode = Normal; | ||
25 | offset = (0, 0); | ||
26 | cursor = (1, 1); | ||
27 | buffer = List.hd c.files |> Option.map ~f:Buffer.from_file; | ||
28 | pending = Key.stream; | ||
29 | i_pending = Command.i_stream; | ||
30 | n_pending = Command.n_stream; | ||
31 | } | ||
32 | |||
33 | type 'a action = t -> 'a * t | ||
34 | |||
35 | module Action = struct | ||
36 | let run ~editor action = action () editor | ||
37 | let eval ~editor action = run ~editor action |> fst | ||
38 | let exec ~editor action = run ~editor action |> snd | ||
39 | |||
40 | include Applicative.Make (struct | ||
41 | type 'a t = 'a action | ||
42 | |||
43 | let return a s = (a, s) | ||
44 | |||
45 | let apply f a e = | ||
46 | let f, e' = f e in | ||
47 | let x, e'' = a e' in | ||
48 | (f x, e'') | ||
49 | |||
50 | let map = `Define_using_apply | ||
51 | end) | ||
52 | |||
53 | include Monad.Make (struct | ||
54 | type 'a t = 'a action | ||
55 | |||
56 | let return x s = (x, s) | ||
57 | |||
58 | let bind a ~f x = | ||
59 | let y, a' = a x in | ||
60 | f y a' | ||
61 | |||
62 | let map = `Define_using_bind | ||
63 | end) | ||
64 | |||
65 | let get e = (e, e) | ||
66 | let put e _ = ((), e) | ||
67 | let modify ~f e = ((), f e) | ||
68 | |||
69 | let update_cursor = | ||
70 | let aux e = | ||
71 | let dx, dy = e.offset and rs, cs = e.term.size in | ||
72 | match Option.map ~f:Buffer.cursor e.buffer with | ||
73 | | None -> { e with cursor = (1, 1); offset = (0, 0) } | ||
74 | | Some (cx, cy) -> | ||
75 | let dx' = Int.clamp_exn ~min:(cx - rs) ~max:cx dx | ||
76 | and dy' = Int.clamp_exn ~min:(cy - cs) ~max:cy dy in | ||
77 | { e with cursor = (cx - dx' + 1, cy - dy' + 1); offset = (dx', dy') } | ||
78 | in | ||
79 | modify ~f:aux | ||
80 | |||
81 | let get_mode s = (s.mode, s) | ||
82 | let set_mode m s = ((), { s with mode = m }) | ||
83 | let get_focused_buffer e = (e.buffer, e) | ||
84 | let set_focused_buffer b e = ((), { e with buffer = Some b }) | ||
85 | |||
86 | let on_focused_buffer f = | ||
87 | let f e = { e with buffer = Option.map ~f e.buffer } in | ||
88 | modify ~f *> update_cursor | ||
89 | |||
90 | let on_focused_buffer_or_new f = | ||
91 | (get_focused_buffer | ||
92 | >>| Option.value ~default:Buffer.empty | ||
93 | >>| f >>= set_focused_buffer) | ||
94 | *> update_cursor | ||
95 | |||
96 | let render = | ||
97 | let welcome (r, c) = | ||
98 | let open Text in | ||
99 | let hfill = ' ' and vfill = Sequence.empty in | ||
100 | "Welcome to the sand editor!" |> String.to_list |> Sequence.of_list | ||
101 | |> center ~fill:hfill c |> Sequence.singleton |> center ~fill:vfill r | ||
102 | in | ||
103 | let aux e = | ||
104 | let x, y = e.offset | ||
105 | and ((r, c) as size) = e.term.size | ||
106 | and fill = Sequence.singleton '~' in | ||
107 | let view = | ||
108 | Option.( | ||
109 | e.buffer >>| Buffer.view x y r c | ||
110 | |> value ~default:(welcome size) | ||
111 | |> Text.extend ~fill r) | ||
112 | in | ||
113 | Terminal.redraw view e.cursor | ||
114 | in | ||
115 | get >>| aux | ||
116 | |||
117 | (* TODO: save logic *) | ||
118 | let quit n = Stdlib.exit n | ||
119 | let noop = return () | ||
120 | end | ||
121 | |||
122 | let move ?(up = 0) ?(down = 0) ?(left = 0) ?(right = 0) (x, y) = | ||
123 | (x + down - up, y + right - left) | ||
124 | |||
125 | let move_to ?x ?y (sx, sy) = Option.(value x ~default:sx, value y ~default:sy) | ||
126 | |||
127 | (* let get_next_command s = *) | ||
128 | (* match Sequence.next s.pending with *) | ||
129 | (* | None -> (None, s) *) | ||
130 | (* | Some (h, t) -> (Some h, { s with pending = t }) *) | ||
131 | |||
132 | (* let handle_insert_key = *) | ||
133 | (* let open Action in *) | ||
134 | (* let open Key in *) | ||
135 | (* function *) | ||
136 | (* | Arrow_down -> Buffer.Action.down |> on_focused_buffer *) | ||
137 | (* | Arrow_left -> Buffer.Action.left |> on_focused_buffer *) | ||
138 | (* | Arrow_right -> Buffer.Action.right |> on_focused_buffer *) | ||
139 | (* | Arrow_up -> Buffer.Action.up |> on_focused_buffer *) | ||
140 | (* | Backspace -> Buffer.Action.delete_before |> on_focused_buffer *) | ||
141 | (* | Ctrl 'Q' -> quit 0 *) | ||
142 | (* | Delete -> Buffer.Action.delete_after |> on_focused_buffer *) | ||
143 | (* | Enter -> Buffer.Action.newline |> on_focused_buffer *) | ||
144 | (* | Esc -> (Buffer.Action.left |> on_focused_buffer) *> set_mode Normal *) | ||
145 | (* | Key k -> Buffer.Action.insert k |> on_focused_buffer *) | ||
146 | (* | _ -> noop *) | ||
147 | |||
148 | (* let handle_normal_key = *) | ||
149 | (* let open Action in *) | ||
150 | (* let open Key in *) | ||
151 | (* function *) | ||
152 | (* | Arrow_down | Key 'j' -> Buffer.Action.down |> on_focused_buffer *) | ||
153 | (* | Arrow_left | Backspace | Key 'h' -> Buffer.Action.left |> on_focused_buffer *) | ||
154 | (* | Arrow_right | Key ' ' | Key 'l' -> Buffer.Action.right |> on_focused_buffer *) | ||
155 | (* | Arrow_up | Key 'k' -> Buffer.Action.up |> on_focused_buffer *) | ||
156 | (* | Ctrl 'Q' -> quit 0 *) | ||
157 | (* | Key '0' -> Buffer.Action.bol |> on_focused_buffer_or_new *) | ||
158 | (* | Key 'A' -> *) | ||
159 | (* (Buffer.Action.eol |> on_focused_buffer_or_new) *> set_mode Insert *) | ||
160 | (* | Key 'a' -> *) | ||
161 | (* (Buffer.Action.right |> on_focused_buffer_or_new) *> set_mode Insert *) | ||
162 | (* | Key 'G' -> Buffer.Action.eof |> on_focused_buffer_or_new *) | ||
163 | (* | Key 'I' -> *) | ||
164 | (* noop *) | ||
165 | (* (1* (Buffer.Action.bol |> on_focused_buffer_or_new) *> set_mode Insert *1) *) | ||
166 | (* | Key 'i' -> (Fn.id |> on_focused_buffer_or_new) *> set_mode Insert *) | ||
167 | (* | Key 's' -> *) | ||
168 | (* (Buffer.Action.delete_after |> on_focused_buffer_or_new) *) | ||
169 | (* *> set_mode Insert *) | ||
170 | (* | Key 'x' -> Buffer.Action.delete_after |> on_focused_buffer_or_new *) | ||
171 | (* | Key 'X' -> Buffer.Action.delete_before |> on_focused_buffer_or_new *) | ||
172 | (* | Key '$' -> Buffer.Action.eol |> on_focused_buffer_or_new *) | ||
173 | (* | _ -> noop *) | ||
174 | |||
175 | (* let handle_next_command = *) | ||
176 | (* let f m = function *) | ||
177 | (* | None -> Action.return () *) | ||
178 | (* | Some k -> ( *) | ||
179 | (* match m with *) | ||
180 | (* | Insert -> handle_insert_key k *) | ||
181 | (* | Normal -> handle_normal_key k) *) | ||
182 | (* in *) | ||
183 | (* Action.(map2 ~f get_mode get_next_command |> join) *) | ||
184 | |||
185 | let handle_insert_command = | ||
186 | let open Command in | ||
187 | let open Action in | ||
188 | function | ||
189 | | Simple Arrow_down -> Buffer.Action.down |> on_focused_buffer | ||
190 | | Simple Arrow_left -> Buffer.Action.left |> on_focused_buffer | ||
191 | | Simple Arrow_right -> Buffer.Action.right |> on_focused_buffer | ||
192 | | Simple Arrow_up -> Buffer.Action.up |> on_focused_buffer | ||
193 | | Simple Backspace -> Buffer.Action.delete_before ~n:1 |> on_focused_buffer | ||
194 | | Simple (Ctrl 'Q') -> quit 0 | ||
195 | | Simple Delete -> Buffer.Action.delete_after ~n:1 |> on_focused_buffer | ||
196 | | Simple Enter -> Buffer.Action.newline |> on_focused_buffer | ||
197 | | Simple Esc -> (Buffer.Action.left |> on_focused_buffer) *> set_mode Normal | ||
198 | | Type k -> Buffer.Action.insert k |> on_focused_buffer | ||
199 | | _ -> noop | ||
200 | |||
201 | let handle_normal_command = | ||
202 | let open Command in | ||
203 | let open Action in | ||
204 | function | ||
205 | (* Movements *) | ||
206 | | Chord (_, n, Noop, _, Down) -> Buffer.Action.down ?n |> on_focused_buffer | ||
207 | | Chord (_, n, Noop, _, Left) -> Buffer.Action.left ?n |> on_focused_buffer | ||
208 | | Chord (_, n, Noop, _, Right) -> Buffer.Action.right ?n |> on_focused_buffer | ||
209 | | Chord (_, n, Noop, _, Up) -> Buffer.Action.up ?n |> on_focused_buffer | ||
210 | | Simple (Ctrl 'Q') -> quit 0 | ||
211 | (* | Key '0' -> Buffer.Action.bol |> on_focused_buffer_or_new *) | ||
212 | | Simple (Key 'A') -> | ||
213 | (Buffer.Action.eol |> on_focused_buffer_or_new) *> set_mode Insert | ||
214 | | Simple (Key 'a') -> | ||
215 | (Buffer.Action.right |> on_focused_buffer_or_new) *> set_mode Insert | ||
216 | (* | Key 'G' -> Buffer.Action.eof |> on_focused_buffer_or_new *) | ||
217 | (* | Key 'I' -> noop *) | ||
218 | | Simple (Key 'i') -> (Fn.id |> on_focused_buffer_or_new) *> set_mode Insert | ||
219 | (* | Key 's' -> *) | ||
220 | (* (Buffer.Action.delete_after |> on_focused_buffer_or_new) *) | ||
221 | (* *> set_mode Insert *) | ||
222 | (* | Key 'x' -> Buffer.Action.delete_after |> on_focused_buffer_or_new *) | ||
223 | (* | Key 'X' -> Buffer.Action.delete_before |> on_focused_buffer_or_new *) | ||
224 | (* | Key '$' -> Buffer.Action.eol |> on_focused_buffer_or_new *) | ||
225 | | Shortcut (_, n, Change, Line) -> | ||
226 | let n = Option.value ~default:1 n - 1 in | ||
227 | (Buffer.Action.(delete_lines ~n &> bol &> delete_to_eol) | ||
228 | |> on_focused_buffer_or_new) | ||
229 | *> set_mode Insert | ||
230 | | Shortcut (_, _, Change, To_eol) -> | ||
231 | (Buffer.Action.delete_to_eol |> on_focused_buffer_or_new) | ||
232 | *> set_mode Insert | ||
233 | | Shortcut (_, _, Change, To_bol) -> | ||
234 | (Buffer.Action.delete_to_bol |> on_focused_buffer_or_new) | ||
235 | *> set_mode Insert | ||
236 | (* Delete *) | ||
237 | | Chord (_, n1, Delete, n2, Down) -> | ||
238 | let n = Option.((value ~default:1 n1 * value ~default:1 n2) + 1) in | ||
239 | Buffer.Action.delete_lines ~n |> on_focused_buffer_or_new | ||
240 | | Chord (_, n1, Delete, n2, Left) -> | ||
241 | let n = Option.(value ~default:1 n1 * value ~default:1 n2) in | ||
242 | Buffer.Action.delete_before ~n |> on_focused_buffer_or_new | ||
243 | | Chord (_, n1, Delete, n2, Right) -> | ||
244 | let n = Option.(value ~default:1 n1 * value ~default:1 n2) in | ||
245 | Buffer.Action.delete_after ~n |> on_focused_buffer_or_new | ||
246 | | Chord (_, n1, Delete, n2, Up) -> | ||
247 | let n = Option.((value ~default:1 n1 * value ~default:1 n2) + 1) in | ||
248 | Buffer.Action.delete_lines_before ~n |> on_focused_buffer_or_new | ||
249 | | Shortcut (_, n, Delete, Line) -> | ||
250 | Buffer.Action.delete_lines ~n:Option.(value ~default:1 n) | ||
251 | |> on_focused_buffer_or_new | ||
252 | | Shortcut (_, _, Delete, To_eol) -> | ||
253 | Buffer.Action.delete_to_eol |> on_focused_buffer_or_new | ||
254 | | Shortcut (_, _, Delete, To_bol) -> | ||
255 | Buffer.Action.delete_to_bol |> on_focused_buffer_or_new | ||
256 | | _ -> noop | ||
257 | |||
258 | let handle_next_command2 m e = | ||
259 | match m with | ||
260 | | Insert -> ( | ||
261 | match Sequence.next e.i_pending with | ||
262 | | None -> ((), e) | ||
263 | | Some (h, t) -> handle_insert_command h { e with i_pending = t }) | ||
264 | | Normal -> ( | ||
265 | match Sequence.next e.n_pending with | ||
266 | | None -> ((), e) | ||
267 | | Some (h, t) -> handle_normal_command h { e with n_pending = t }) | ||
268 | |||
269 | let handle_next_command2 = | ||
270 | let open Action in | ||
271 | get_mode >>= handle_next_command2 | ||
diff --git a/lib/editorBuffer.ml b/lib/editorBuffer.ml new file mode 100644 index 0000000..5104549 --- /dev/null +++ b/lib/editorBuffer.ml | |||
@@ -0,0 +1,72 @@ | |||
1 | open Base | ||
2 | open Zipper | ||
3 | open Util | ||
4 | |||
5 | type kind = File of string | No_name | Scratch | ||
6 | type error = No_such_file | Other | ||
7 | type buffer = { kind : kind; content : (char zipper zipper, error) Result.t } | ||
8 | type t = buffer | ||
9 | |||
10 | let empty = | ||
11 | { kind = No_name; content = empty |> push_after empty |> Result.return } | ||
12 | |||
13 | module Action = struct | ||
14 | let on_content f b = { b with content = Result.map ~f b.content } | ||
15 | |||
16 | let up, down, left, right = | ||
17 | let vertical f ?(n = 1) = | ||
18 | on_content (fun z -> | ||
19 | let col = focus_or ~default:Zipper.empty z |> left_length in | ||
20 | Fn.apply_n_times ~n f z |> map_focus (goto col)) | ||
21 | and horizontal f ?(n = 1) = | ||
22 | Fn.apply_n_times ~n (map_focus f) |> on_content | ||
23 | in | ||
24 | (vertical left, vertical right, horizontal left, horizontal right) | ||
25 | |||
26 | let bol = map_focus far_left |> on_content | ||
27 | let eol = map_focus far_right |> on_content | ||
28 | let bof = far_left |> on_content | ||
29 | let eof = far_right |> on_content | ||
30 | let insert k = map_focus (push k) |> on_content | ||
31 | let delete_after ~n = Fn.apply_n_times ~n (map_focus pop_after) |> on_content | ||
32 | let delete_before ~n = Fn.apply_n_times ~n (map_focus pop) |> on_content | ||
33 | let delete_to_eol = map_focus (split &> fst) |> on_content | ||
34 | let delete_to_bol = map_focus (split &> snd) |> on_content | ||
35 | let delete_lines ~n = Fn.apply_n_times ~n pop_after |> on_content | ||
36 | |||
37 | let delete_lines_before ~n = | ||
38 | on_content (fun z -> pop_after z |> Fn.apply_n_times ~n:(n - 1) pop_before) | ||
39 | |||
40 | let newline = | ||
41 | let aux z = | ||
42 | let l1, l2 = focus_or ~default:Zipper.empty z |> split in | ||
43 | push_before l1 z |> map_focus (Fn.const l2) | ||
44 | in | ||
45 | on_content aux | ||
46 | |||
47 | (* let save_history_to ?(clear = true) r = () *) | ||
48 | end | ||
49 | |||
50 | let from_file f = | ||
51 | let lines = Stdio.In_channel.read_lines f in | ||
52 | let line_to_zipper l = String.to_list l |> Sequence.of_list |> of_seq in | ||
53 | let content = Sequence.(of_list lines |> map ~f:line_to_zipper) |> of_seq in | ||
54 | { kind = File f; content = Ok content } | ||
55 | |||
56 | let cursor b = | ||
57 | let open Option in | ||
58 | let x = Result.(map ~f:left_length b.content |> ok |> value ~default:0) | ||
59 | and y = | ||
60 | Result.(map ~f:focus b.content |> ok) | ||
61 | |> join |> map ~f:left_length |> value ~default:0 | ||
62 | in | ||
63 | (x, y) | ||
64 | |||
65 | let view x y h w b = | ||
66 | match b.content with | ||
67 | | Error _ -> Sequence.empty | ||
68 | | Ok z -> | ||
69 | let cx, _ = cursor b in | ||
70 | context ~b:(cx - x) ~a:(x + h - cx) z | ||
71 | |> to_seq | ||
72 | |> Sequence.map ~f:(window ~from:y ~len:w) | ||
diff --git a/lib/key.ml b/lib/key.ml new file mode 100644 index 0000000..85aa282 --- /dev/null +++ b/lib/key.ml | |||
@@ -0,0 +1,72 @@ | |||
1 | open Base | ||
2 | |||
3 | type key = | ||
4 | | Arrow_down | ||
5 | | Arrow_left | ||
6 | | Arrow_right | ||
7 | | Arrow_up | ||
8 | | Backspace | ||
9 | | Ctrl of char | ||
10 | | Delete | ||
11 | | End | ||
12 | | Enter | ||
13 | | Esc | ||
14 | | Home | ||
15 | | Key of char | ||
16 | | Nul | ||
17 | | Page_down | ||
18 | | Page_up | ||
19 | |||
20 | type t = key | ||
21 | |||
22 | let ctrl c = Ctrl c | ||
23 | let key c = Key c | ||
24 | |||
25 | let of_char = function | ||
26 | | '\000' -> Nul | ||
27 | | '\013' -> Enter | ||
28 | | '\027' -> Esc | ||
29 | | '\127' -> Backspace | ||
30 | | c when Char.(c < ' ') -> Char.to_int c + 64 |> Char.of_int_exn |> ctrl | ||
31 | | c -> Key c | ||
32 | |||
33 | let stream = | ||
34 | let step s c = | ||
35 | let open Sequence.Step in | ||
36 | let escaped = function | ||
37 | | 'A' -> Some Arrow_up | ||
38 | | 'B' -> Some Arrow_down | ||
39 | | 'C' -> Some Arrow_right | ||
40 | | 'D' -> Some Arrow_left | ||
41 | | 'F' -> Some End | ||
42 | | 'H' -> Some Home | ||
43 | | _ -> None | ||
44 | and tilda = function | ||
45 | | '1' -> Some Home | ||
46 | | '3' -> Some Delete | ||
47 | | '4' -> Some End | ||
48 | | '5' -> Some Page_up | ||
49 | | '6' -> Some Page_down | ||
50 | | '7' -> Some Home | ||
51 | | '8' -> Some End | ||
52 | | _ -> None | ||
53 | in | ||
54 | match (s, c) with | ||
55 | | `start, Some '\027' -> Skip { state = `esc } | ||
56 | | `esc, None -> Yield { value = Esc; state = `start } | ||
57 | | `esc, Some '[' | `escaped, Some 'O' -> Skip { state = `escaped } | ||
58 | | `escaped, Some i when Char.(i = '1' || ('3' <= i && i <= '8')) -> | ||
59 | Skip { state = `tilda i } | ||
60 | | `escaped, c -> ( | ||
61 | match Option.(c >>= escaped) with | ||
62 | | None -> Skip { state = `state } | ||
63 | | Some c -> Yield { value = c; state = `start }) | ||
64 | | `tilda i, Some '~' -> ( | ||
65 | match tilda i with | ||
66 | | None -> Skip { state = `start } | ||
67 | | Some k -> Yield { value = k; state = `start }) | ||
68 | | `esc, Some _ | `tilda _, _ -> Skip { state = `start } | ||
69 | | _, None -> Skip { state = `start } | ||
70 | | _, Some c -> Yield { value = of_char c; state = `start } | ||
71 | in | ||
72 | Sequence.unfold_with ~init:`start ~f:step Terminal.char_stream | ||
diff --git a/lib/modes.ml b/lib/modes.ml new file mode 100644 index 0000000..3d0e354 --- /dev/null +++ b/lib/modes.ml | |||
@@ -0,0 +1,18 @@ | |||
1 | type mode = Normal | Insert | ||
2 | type t = mode | ||
3 | type state = int | ||
4 | type 'a state_monad = state -> 'a * state | ||
5 | |||
6 | let run (f : 'a state_monad) (s : state) : 'a = f s |> fst | ||
7 | let return (a : 'a) : 'a state_monad = fun s -> (a, s) | ||
8 | |||
9 | let ( >>= ) (f : 'a state_monad) (g : 'a -> 'b state_monad) : 'b state_monad = | ||
10 | fun s -> | ||
11 | let a, s' = f s in | ||
12 | g a s' | ||
13 | |||
14 | let draw () : unit state_monad = return () | ||
15 | let get_keypress () : char state_monad = return 'a' | ||
16 | let handle_key (_ : char) : unit state_monad = return () | ||
17 | let rec loop () = () |> draw >>= get_keypress >>= handle_key >>= loop | ||
18 | let test = run (loop ()) 0 | ||
diff --git a/lib/terminal.ml b/lib/terminal.ml new file mode 100644 index 0000000..408f7a8 --- /dev/null +++ b/lib/terminal.ml | |||
@@ -0,0 +1,130 @@ | |||
1 | open Base | ||
2 | open Unix | ||
3 | open Util | ||
4 | |||
5 | type terminal_size = int * int | ||
6 | type state = { tio : terminal_io; size : terminal_size } | ||
7 | |||
8 | let escape ?(prefix = "") ?(args = []) op = | ||
9 | let open Bytes in | ||
10 | let args = List.map ~f:Int.to_string args |> String.concat ~sep:";" in | ||
11 | let lenp = String.length prefix in | ||
12 | let lena = String.length args in | ||
13 | let lenb = 3 + lena + lenp in | ||
14 | let dst = Bytes.create lenb in | ||
15 | set dst 0 '\x1b'; | ||
16 | set dst 1 '['; | ||
17 | From_string.blit ~src:prefix ~src_pos:0 ~dst ~dst_pos:2 ~len:lenp; | ||
18 | From_string.blit ~src:args ~src_pos:0 ~dst ~dst_pos:(2 + lenp) ~len:lena; | ||
19 | set dst (length dst - 1) op; | ||
20 | sequence_of_bytes dst | ||
21 | |||
22 | let clear_screen = escape 'J' ~args:[ 2 ] | ||
23 | let clear_to_eol = escape 'K' | ||
24 | let move_cursor x y = escape 'H' ~args:[ x; y ] | ||
25 | let move_down n = escape 'B' ~args:[ n ] | ||
26 | let move_right n = escape 'C' ~args:[ n ] | ||
27 | let query_cursor_pos = escape 'n' ~args:[ 6 ] | ||
28 | let reset_cursor = move_cursor 1 1 | ||
29 | |||
30 | let show_cursor show = | ||
31 | let cmd = if show then 'h' else 'l' in | ||
32 | escape cmd ~prefix:"?" ~args:[ 25 ] | ||
33 | |||
34 | let input_bytes = Bytes.create 1 | ||
35 | |||
36 | let get_char () = | ||
37 | let syscall () = read stdin input_bytes 0 1 in | ||
38 | match handle_unix_error syscall () with | ||
39 | | 0 -> None | ||
40 | | _ -> Some (Bytes.get input_bytes 0) | ||
41 | |||
42 | let char_stream : char option Sequence.t = | ||
43 | Sequence.unfold ~init:() ~f:(fun s -> Some (get_char (), s)) | ||
44 | |||
45 | let write_seq = | ||
46 | let syscall s = | ||
47 | let buf = Sequence.to_list s |> Bytes.of_char_list in | ||
48 | single_write stdout buf 0 (Bytes.length buf) |> ignore | ||
49 | in | ||
50 | handle_unix_error syscall | ||
51 | |||
52 | let write_lines (lines : char Sequence.t Sequence.t) = | ||
53 | let crnl = Sequence.of_list [ '\r'; '\n' ] in | ||
54 | let clear s = Sequence.append s clear_to_eol in | ||
55 | let syscall seq = | ||
56 | let buf = | ||
57 | Sequence.(map ~f:clear seq |> intersperse ~sep:crnl |> concat |> to_list) | ||
58 | |> Bytes.of_char_list | ||
59 | in | ||
60 | single_write stdout buf 0 (Bytes.length buf) |> ignore | ||
61 | in | ||
62 | handle_unix_error syscall lines | ||
63 | |||
64 | let cmds_to_sequence l = Sequence.(of_list l |> concat) | ||
65 | |||
66 | let restore_screen () = | ||
67 | cmds_to_sequence [ clear_screen; reset_cursor; show_cursor true ] |> write_seq | ||
68 | |||
69 | let redraw screen (x, y) = | ||
70 | let pre = cmds_to_sequence [ show_cursor false; reset_cursor ] | ||
71 | and post = cmds_to_sequence [ move_cursor x y; show_cursor true ] in | ||
72 | write_seq pre; | ||
73 | write_lines screen; | ||
74 | write_seq post | ||
75 | |||
76 | let get_state () = { tio = handle_unix_error tcgetattr stdin; size = (-1, -1) } | ||
77 | |||
78 | let size = | ||
79 | let query () = | ||
80 | cmds_to_sequence [ move_right 999; move_down 999; query_cursor_pos ] | ||
81 | |> write_seq | ||
82 | and get_reply () = | ||
83 | Sequence.( | ||
84 | char_stream | ||
85 | |> take_while ~f:Option.is_some | ||
86 | |> Fn.flip drop_eagerly 2 (* Drop escape sequence '<esc>[' *) | ||
87 | |> map ~f:(Option.value ~default:'R') | ||
88 | |> take_while ~f:(fun c -> Char.(c <> 'R')) | ||
89 | |> to_list |> String.of_char_list |> Stdlib.Scanf.sscanf | ||
90 | |> fun scanner -> scanner "%d;%d" (fun a b -> (a, b))) | ||
91 | in | ||
92 | query &> get_reply | ||
93 | |||
94 | let enable_raw_mode tio = | ||
95 | let syscall () = | ||
96 | tcsetattr stdin TCSAFLUSH | ||
97 | { | ||
98 | tio with | ||
99 | c_brkint = false; | ||
100 | c_csize = 8; | ||
101 | c_echo = false; | ||
102 | c_echonl = false; | ||
103 | c_icanon = false; | ||
104 | c_icrnl = false; | ||
105 | c_ignbrk = false; | ||
106 | c_igncr = false; | ||
107 | c_inlcr = false; | ||
108 | c_inpck = false; | ||
109 | c_isig = false; | ||
110 | c_istrip = false; | ||
111 | c_ixon = false; | ||
112 | c_opost = false; | ||
113 | c_parenb = false; | ||
114 | c_parmrk = false; | ||
115 | c_vmin = 0; | ||
116 | c_vtime = 1; | ||
117 | } | ||
118 | in | ||
119 | handle_unix_error syscall | ||
120 | |||
121 | let restore_status tio = | ||
122 | let syscall () = tcsetattr stdin TCSAFLUSH tio in | ||
123 | handle_unix_error syscall | ||
124 | |||
125 | let init () = | ||
126 | let state = get_state () in | ||
127 | enable_raw_mode state.tio (); | ||
128 | restore_status state.tio |> Stdlib.at_exit; | ||
129 | restore_screen |> Stdlib.at_exit; | ||
130 | { state with size = size () } | ||
diff --git a/lib/terminal.mli b/lib/terminal.mli new file mode 100644 index 0000000..0fd11ed --- /dev/null +++ b/lib/terminal.mli | |||
@@ -0,0 +1,67 @@ | |||
1 | (** A module to simplify communication with the underlying terminal | ||
2 | emulator. *) | ||
3 | |||
4 | open Base | ||
5 | open Unix | ||
6 | |||
7 | type terminal_size = int * int | ||
8 | (** Size of the terminal window. *) | ||
9 | |||
10 | type state = { | ||
11 | tio : terminal_io; (** Status flags for the terminal window *) | ||
12 | size : terminal_size; (** Size of the terminal window *) | ||
13 | } | ||
14 | (** Global state of the terminal window. *) | ||
15 | |||
16 | val get_char : unit -> char option | ||
17 | (** Non-blocking request for a keypress. | ||
18 | Use {!val:Terminal.char_stream} for an infinite sequence of input | ||
19 | bytes. | ||
20 | |||
21 | @return A [char] if a key was pressed, nothing otherwise. *) | ||
22 | |||
23 | val char_stream : char option Sequence.t | ||
24 | (** The infinite stream of input bytes from stdin. | ||
25 | Returns [None] if no key was pressed; this is to be able to | ||
26 | interpret the absense of a keypress as an action. *) | ||
27 | |||
28 | val write_seq : char Sequence.t -> unit | ||
29 | (** Write a sequence of strings to standard output. | ||
30 | |||
31 | @param seq The sequence of strings to output. *) | ||
32 | |||
33 | val restore_screen : unit -> unit | ||
34 | (** Clear screen and show cursor. Meant to be called on exit. *) | ||
35 | |||
36 | val redraw : char Sequence.t Sequence.t -> int * int -> unit | ||
37 | (** Redraw the screen, using the provided sequence of lines. | ||
38 | |||
39 | @param seq A sequence of lines. | ||
40 | @param cur Cursor position. *) | ||
41 | |||
42 | val get_state : unit -> state | ||
43 | (** Get current state for the terminal window *) | ||
44 | |||
45 | val size : unit -> int * int | ||
46 | (** Compute the current size of the terminal. | ||
47 | |||
48 | This is done by moving the cursor to the far bottom right position | ||
49 | and querying for the cursor position using the appropriate escape | ||
50 | sequences. | ||
51 | |||
52 | @return size of the terminal window in terms of character rows and columns. *) | ||
53 | |||
54 | val enable_raw_mode : terminal_io -> unit -> unit | ||
55 | (** Turn on raw mode by overriding the correct terminal flags. | ||
56 | This is done according to [man 3 termios]. | ||
57 | |||
58 | @param tio The current status of the terminal for [stdin]. *) | ||
59 | |||
60 | val restore_status : terminal_io -> unit -> unit | ||
61 | (** Override the terminal status. | ||
62 | |||
63 | @param tio The new terminal status for [stdin]. *) | ||
64 | |||
65 | val init : unit -> state | ||
66 | (** Turns on raw mode and makes sure to restore the previous terminal | ||
67 | status on exit. *) | ||
diff --git a/lib/text.ml b/lib/text.ml new file mode 100644 index 0000000..ff1f727 --- /dev/null +++ b/lib/text.ml | |||
@@ -0,0 +1,10 @@ | |||
1 | open Base | ||
2 | open Sequence | ||
3 | open Sequence.Infix | ||
4 | |||
5 | let center ~fill n text = | ||
6 | let padding = repeat fill and len_t = length text in | ||
7 | let len_p = (n - len_t) / 2 in | ||
8 | take (take padding len_p @ text @ padding) n | ||
9 | |||
10 | let extend ~fill n text = take (text @ repeat fill) n | ||
diff --git a/lib/util.ml b/lib/util.ml new file mode 100644 index 0000000..9ad3b59 --- /dev/null +++ b/lib/util.ml | |||
@@ -0,0 +1,26 @@ | |||
1 | (** The infamous [Util] module. A graveyard of broken expressions. *) | ||
2 | |||
3 | open Base | ||
4 | |||
5 | (** [f &> g] composes [g] with [f] to obrain [fun x -> g (f x)]; | ||
6 | in other words, it applies [f] {i and then} [g]. | ||
7 | |||
8 | @param f the first funtion to apply. | ||
9 | @param g the second function to apply. | ||
10 | @return the composition of [g] with [f]. *) | ||
11 | let ( &> ) f g = Fn.compose g f | ||
12 | |||
13 | (** Turn a sequence of bytes into a sequence. | ||
14 | |||
15 | @param b the input bytes. | ||
16 | @return a sequence of bytes. *) | ||
17 | let sequence_of_bytes (b : Bytes.t) : char Sequence.t = | ||
18 | let open Sequence.Generator in | ||
19 | let traverse b = | ||
20 | let len = Bytes.length b in | ||
21 | let rec loop i _ = | ||
22 | if i >= len then return () else yield (Bytes.get b i) >>= loop (i + 1) | ||
23 | in | ||
24 | loop 0 () | ||
25 | in | ||
26 | traverse b |> run | ||
diff --git a/lib/zipper.ml b/lib/zipper.ml new file mode 100644 index 0000000..2322a58 --- /dev/null +++ b/lib/zipper.ml | |||
@@ -0,0 +1,131 @@ | |||
1 | (* Module [Zipper]: functional zippers *) | ||
2 | |||
3 | open Base | ||
4 | |||
5 | type !+'a zipper = { | ||
6 | pos : int; | ||
7 | popped : 'a Sequence.t; | ||
8 | before : 'a Sequence.t; | ||
9 | after : 'a Sequence.t; | ||
10 | } | ||
11 | |||
12 | type !+'a t = 'a zipper | ||
13 | |||
14 | let empty = | ||
15 | { | ||
16 | pos = 0; | ||
17 | popped = Sequence.empty; | ||
18 | before = Sequence.empty; | ||
19 | after = Sequence.empty; | ||
20 | } | ||
21 | |||
22 | let before z = z.before | ||
23 | let after z = z.after | ||
24 | let focus z = after z |> Sequence.next |> Option.map ~f:fst | ||
25 | let focus_or z ~default = Option.value ~default (focus z) | ||
26 | let history z = z.popped | ||
27 | let is_far_left z = before z |> Sequence.is_empty | ||
28 | let is_far_right z = after z |> Sequence.is_empty | ||
29 | let is_empty z = is_far_left z && is_far_right z | ||
30 | let left_length z = z.pos | ||
31 | let right_length z = after z |> Sequence.length | ||
32 | let length z = left_length z + right_length z | ||
33 | |||
34 | let left z = | ||
35 | match Sequence.next z.before with | ||
36 | | None -> z | ||
37 | | Some (h, t) -> | ||
38 | { | ||
39 | z with | ||
40 | pos = z.pos - 1; | ||
41 | before = t; | ||
42 | after = Sequence.shift_right z.after h; | ||
43 | } | ||
44 | |||
45 | let rec left_while f z = | ||
46 | if (not (is_far_left z)) && Option.(focus z |> map ~f |> value ~default:false) | ||
47 | then left z |> left_while f | ||
48 | else z | ||
49 | |||
50 | let rec far_left z = if is_far_left z then z else z |> left |> far_left | ||
51 | |||
52 | let right z = | ||
53 | match Sequence.next z.after with | ||
54 | | None -> z | ||
55 | | Some (h, t) -> | ||
56 | { | ||
57 | z with | ||
58 | pos = z.pos + 1; | ||
59 | before = Sequence.shift_right z.before h; | ||
60 | after = t; | ||
61 | } | ||
62 | |||
63 | let rec right_while f z = | ||
64 | if | ||
65 | (not (is_far_right z)) && Option.(focus z |> map ~f |> value ~default:false) | ||
66 | then right z |> right_while f | ||
67 | else z | ||
68 | |||
69 | let rec far_right z = if is_far_right z then z else z |> right |> far_right | ||
70 | |||
71 | let goto n z = | ||
72 | let n = n - z.pos in | ||
73 | let step = if n < 0 then left else right in | ||
74 | Fn.apply_n_times ~n:(abs n) step z | ||
75 | |||
76 | let pop_after z = { z with after = Sequence.drop_eagerly z.after 1 } | ||
77 | let pop_before z = if is_far_left z then z else z |> left |> pop_after | ||
78 | let pop = pop_before | ||
79 | let push_after x z = { z with after = Sequence.shift_right z.after x } | ||
80 | |||
81 | let push_before x z = | ||
82 | { z with pos = z.pos + 1; before = Sequence.shift_right z.before x } | ||
83 | |||
84 | let push = push_before | ||
85 | |||
86 | let split z = | ||
87 | ( { z with after = Sequence.empty }, | ||
88 | { z with pos = 0; before = Sequence.empty } ) | ||
89 | |||
90 | let iter_before f z = Sequence.iter ~f z.before | ||
91 | let iter_after f z = Sequence.iter ~f z.after | ||
92 | |||
93 | let iter f z = | ||
94 | iter_before f z; | ||
95 | iter_after f z | ||
96 | |||
97 | let for_all f z = Sequence.(for_all ~f z.before && for_all ~f z.after) | ||
98 | let exists f z = Sequence.(exists ~f z.before || exists ~f z.after) | ||
99 | let find_before f z = Sequence.find ~f z.before | ||
100 | let find_after f z = Sequence.find ~f z.after | ||
101 | let map_before f z = { z with before = Sequence.map ~f z.before } | ||
102 | let map_after f z = { z with after = Sequence.map ~f z.after } | ||
103 | |||
104 | let map_focus f z = | ||
105 | match Sequence.next z.after with | ||
106 | | None -> z | ||
107 | | Some (h, t) -> { z with after = Sequence.shift_right t (f h) } | ||
108 | |||
109 | let map f z = | ||
110 | { z with before = Sequence.map ~f z.before; after = Sequence.map ~f z.after } | ||
111 | |||
112 | let mapi_before f z = { z with before = Sequence.mapi ~f z.before } | ||
113 | let mapi_after f z = { z with after = Sequence.mapi ~f z.after } | ||
114 | |||
115 | let mapi f z = | ||
116 | { | ||
117 | z with | ||
118 | before = Sequence.mapi ~f z.before; | ||
119 | after = Sequence.mapi ~f z.after; | ||
120 | } | ||
121 | |||
122 | let filter_before f z = { z with before = Sequence.filter ~f z.before } | ||
123 | let filter_after f z = { z with after = Sequence.filter ~f z.after } | ||
124 | let filter p z = z |> filter_before p |> filter_after p | ||
125 | let context_before n z = { z with before = Sequence.take z.before n } | ||
126 | let context_after n z = { z with after = Sequence.take z.after n } | ||
127 | let context ~b ?(a = b) z = z |> context_before b |> context_after a | ||
128 | let clear_history z = { z with popped = Sequence.empty } | ||
129 | let of_seq s = { empty with after = s } | ||
130 | let to_seq z = z |> far_left |> after | ||
131 | let window ~from ~len z = goto from z |> context_after len |> after | ||
diff --git a/lib/zipper.mli b/lib/zipper.mli new file mode 100644 index 0000000..c3c79a6 --- /dev/null +++ b/lib/zipper.mli | |||
@@ -0,0 +1,258 @@ | |||
1 | open Base | ||
2 | |||
3 | (** Linear zippers. | ||
4 | |||
5 | A zipper represents {b a paused traversal} of a certain data | ||
6 | structure. A linear zipper of type ['a zipper] represents a zipper | ||
7 | over a sequence of ['a] elements. | ||
8 | |||
9 | One can access the {b focused element} of the zipper with [focus]. | ||
10 | The focus of a zipper is also known as the the {b cursor} of the | ||
11 | zipper, and can be moved back and forth with [left] and [right]. | ||
12 | |||
13 | Elements can be added or removed at the cursor's position with | ||
14 | [push] and [pop] operations. Variants are available, e.g., to act | ||
15 | before the cursor, with suffix [_before], or after the cursor, with | ||
16 | suffix [_after]. *) | ||
17 | |||
18 | type !+'a zipper | ||
19 | (** A zipper is represented as a {b pair} of sequences of type ['a]. The | ||
20 | focus of the zipper is the element at the head of the second | ||
21 | sequence, if any. | ||
22 | |||
23 | Note that the first sequence is reversed. Given a sequence | ||
24 | [[1; 2; 3; 4; {5}; 6; 7]], where we represent the cursor using | ||
25 | curly braces [{_}], its representation as a zipper is | ||
26 | [([4; 3; 2; 1], [5; 6; 7])]. *) | ||
27 | |||
28 | type !+'a t = 'a zipper | ||
29 | (** An alias for the ['a zipper] type. *) | ||
30 | |||
31 | val empty : 'a zipper | ||
32 | (** Return an empty zipper *) | ||
33 | |||
34 | val before : 'a zipper -> 'a Sequence.t | ||
35 | (** Return the sequence before the cursor *) | ||
36 | |||
37 | val after : 'a zipper -> 'a Sequence.t | ||
38 | (** Return the sequence after the cursor *) | ||
39 | |||
40 | val focus : 'a zipper -> 'a option | ||
41 | (** Return the focus of the zipper, if any. *) | ||
42 | |||
43 | val focus_or : 'a zipper -> default:'a -> 'a | ||
44 | (** Return the focus of the zipper, or a user-provided default, otherwise. *) | ||
45 | |||
46 | val history : 'a zipper -> 'a Sequence.t | ||
47 | (** Returns the sequence of elements [pop]ped so far from the zipper. *) | ||
48 | |||
49 | val is_far_left : 'a zipper -> bool | ||
50 | (** Return whether the cursor is at the beginning of the zipper. *) | ||
51 | |||
52 | val is_far_right : 'a zipper -> bool | ||
53 | (** Return whether the cursor is at the end of the zipper. *) | ||
54 | |||
55 | val is_empty : 'a zipper -> bool | ||
56 | (** Return whether the zipper is empty. *) | ||
57 | |||
58 | val left_length : 'a zipper -> int | ||
59 | (** Return the number of elements before the cursor. *) | ||
60 | |||
61 | val right_length : 'a zipper -> int | ||
62 | (** Return the number of elements after the cursor. *) | ||
63 | |||
64 | val length : 'a zipper -> int | ||
65 | (** Return the length of the zipper. *) | ||
66 | |||
67 | (** {1 Moving the cursor} *) | ||
68 | |||
69 | val left : 'a zipper -> 'a zipper | ||
70 | (** Move the cursor one step to the left, if possible. | ||
71 | Calling [left z], | ||
72 | |||
73 | - if [z] is [([3; 2; 1], [4; 5])], the result is [([2; 1], [3; 4; 5])], | ||
74 | - if [z] is [([], [1; 2; 3])], the result is [([], [1; 2; 3])]. | ||
75 | *) | ||
76 | |||
77 | val left_while : ('a -> bool) -> 'a zipper -> 'a zipper | ||
78 | (** [left_while f z] moves the cursor in [z] to the left as long as the | ||
79 | predicate [f] is [true] when applied to the focus, or the left end | ||
80 | of the zipper is reached. *) | ||
81 | |||
82 | val far_left : 'a zipper -> 'a zipper | ||
83 | (** Move the cursor to the left, as much as possible. *) | ||
84 | |||
85 | val right : 'a zipper -> 'a zipper | ||
86 | (** Move the cursor one step to the right, if possible. | ||
87 | Calling [right z], | ||
88 | |||
89 | - if [z] is [([3; 2; 1], [4; 5])], the result is [([4; 3; 2; 1], [5])], | ||
90 | - if [z] is [([1; 2; 3], [])], the result is [([1; 2; 3], [])]. | ||
91 | *) | ||
92 | |||
93 | val right_while : ('a -> bool) -> 'a zipper -> 'a zipper | ||
94 | (** [right_while f z] moves the cursor in [z] to the right as long as | ||
95 | the predicate [f] is [true] when applied to the focus, or the right | ||
96 | end of the zipper is reached. *) | ||
97 | |||
98 | val far_right : 'a zipper -> 'a zipper | ||
99 | (** Move the cursor to the right, as much as possible. *) | ||
100 | |||
101 | val goto : int -> 'a zipper -> 'a zipper | ||
102 | (** Move the cursor to a specific (absolute) position in the zipper. | ||
103 | Depending on the current position, it either moves the cursor | ||
104 | forward or backwards, without crossing the zipper boundaries. *) | ||
105 | |||
106 | (** {1 Changes at the cursor} *) | ||
107 | |||
108 | (** Zippers provide [O(1)] operations performed at the cursor position. | ||
109 | This involve [push]ing and [pop]ping elements before and after the | ||
110 | cursor. *) | ||
111 | |||
112 | val pop_after : 'a zipper -> 'a zipper | ||
113 | (** Remove the element at the cursor position, if any, and return the | ||
114 | modified zipper. Calling [pop_after z], | ||
115 | |||
116 | - if [z] is [([3; 2; 1], [4; 5])], the result is [([3; 2; 1], [5])], | ||
117 | - if [z] is [([1; 2; 3], [])], the result is [([1; 2; 3], [])]. | ||
118 | *) | ||
119 | |||
120 | val pop_before : 'a zipper -> 'a zipper | ||
121 | (** Remove the element before the cursor, if any, and return the | ||
122 | modified zipper. Calling [pop_before z], | ||
123 | |||
124 | - if [z] is [([3; 2; 1], [4; 5])], the result is [([2; 1], [4, 5])], | ||
125 | - if [z] is [([], [1; 2; 3])], the result is [([], [1; 2; 3])]. | ||
126 | *) | ||
127 | |||
128 | val pop : 'a zipper -> 'a zipper | ||
129 | (** [pop] is an alias for [pop_before]. *) | ||
130 | |||
131 | val push_after : 'a -> 'a zipper -> 'a zipper | ||
132 | (** Insert an element after the cursor. | ||
133 | Calling [push_after 0 z], if [z] is [([3; 2; 1], [4; 5])], | ||
134 | the result is [([3; 2; 1], [0; 4, 5]))], *) | ||
135 | |||
136 | val push_before : 'a -> 'a zipper -> 'a zipper | ||
137 | (** Insert an element before the cursor. Return the modified zipper. | ||
138 | Calling [push_before 0 z], if [z] is [([3; 2; 1], [4; 5])], | ||
139 | the result is [([0; 3; 2; 1], [4, 5]))]. *) | ||
140 | |||
141 | val push : 'a -> 'a zipper -> 'a zipper | ||
142 | (** [push] is an alias for [push_before]. *) | ||
143 | |||
144 | val split : 'a zipper -> 'a zipper * 'a zipper | ||
145 | (** [split z] splits the zipper in two. [([3; 2; 1], [4; 5])] becomes | ||
146 | [([3; 2; 1], []), ([], [4; 5])]. *) | ||
147 | |||
148 | (** {1 Consuming zippers} *) | ||
149 | |||
150 | (** Since zippers are based on sequences, iterating over zippers | ||
151 | terminates only when both sequences are finite. | ||
152 | Unless otherwise stated, these functions will iterate on the | ||
153 | elements before the cursor, first. *) | ||
154 | |||
155 | val iter_before : ('a -> unit) -> 'a zipper -> unit | ||
156 | (** [iter_before f z] will call [f x] for all [x], elements before the | ||
157 | cursor in [z].*) | ||
158 | |||
159 | val iter_after : ('a -> unit) -> 'a zipper -> unit | ||
160 | (** [iter_after f z] will call [f x] for all [x], elements after the | ||
161 | cursor in [z].*) | ||
162 | |||
163 | val iter : ('a -> unit) -> 'a zipper -> unit | ||
164 | (** [iter f z] is equivalent to [iter_before f z; iter_after f z] *) | ||
165 | |||
166 | val for_all : ('a -> bool) -> 'a zipper -> bool | ||
167 | (** [for_all p z] tests whether a predicate [p] is [true] for all | ||
168 | elements in a zipper. *) | ||
169 | |||
170 | val exists : ('a -> bool) -> 'a zipper -> bool | ||
171 | (** [exists p z] tests whether at least one element in the zipper is | ||
172 | [true] according to the predicate [p]. *) | ||
173 | |||
174 | val find_before : ('a -> bool) -> 'a zipper -> 'a option | ||
175 | (** [find_before p z] will return the first element before the cursor in | ||
176 | [z] satisfying the predicate [p], if any. *) | ||
177 | |||
178 | val find_after : ('a -> bool) -> 'a zipper -> 'a option | ||
179 | (** [find_after p z] will return the first element after the cursor in | ||
180 | [z] satisfying the predicate [p], if any. *) | ||
181 | |||
182 | (** {1 Transforming zippers} *) | ||
183 | |||
184 | (** Since zippers are based on sequences, the functions in this section | ||
185 | are lazy; i.e., resulting elements of the zipper are computed only | ||
186 | when demanded. *) | ||
187 | |||
188 | val map_before : ('a -> 'a) -> 'a zipper -> 'a zipper | ||
189 | (** Map a function over all elements before the cursor. *) | ||
190 | |||
191 | val map_after : ('a -> 'a) -> 'a zipper -> 'a zipper | ||
192 | (** Map a function over all elements after the cursor. *) | ||
193 | |||
194 | val map_focus : ('a -> 'a) -> 'a zipper -> 'a zipper | ||
195 | (** Map a function over the element focused by the cursor, if any. *) | ||
196 | |||
197 | val map : ('a -> 'a) -> 'a zipper -> 'a zipper | ||
198 | (** Map a function over all elements of a zipper. *) | ||
199 | |||
200 | val mapi_before : (int -> 'a -> 'a) -> 'a zipper -> 'a zipper | ||
201 | (** [mapi_before] is analogous to {!Zipper.map_before}, but the function | ||
202 | takes an index and an element. | ||
203 | The index indicates the distance of an element from the cursor. *) | ||
204 | |||
205 | val mapi_after : (int -> 'a -> 'a) -> 'a zipper -> 'a zipper | ||
206 | (** [mapi_after] is analogous to {!Zipper.map_after}, but the function | ||
207 | takes an index and an element. | ||
208 | The index indicates the distance of an element from the cursor. *) | ||
209 | |||
210 | val mapi : (int -> 'a -> 'a) -> 'a zipper -> 'a zipper | ||
211 | (** [mapi] is analogous to {!Zipper.map}, but the function takes an | ||
212 | index and an element. | ||
213 | The index indicates the distance of an element from the cursor. *) | ||
214 | |||
215 | val filter_before : ('a -> bool) -> 'a zipper -> 'a zipper | ||
216 | (** [filter_before p z] filters the elements before the cursor in a | ||
217 | zipper [z] according to a predicate [p], i.e., keeping the elements | ||
218 | that satisfy the predicate. *) | ||
219 | |||
220 | val filter_after : ('a -> bool) -> 'a zipper -> 'a zipper | ||
221 | (** [filter_after p z] filters the elements after the cursor in a | ||
222 | zipper [z] according to a predicate [p], i.e., keeping the elements | ||
223 | that satisfy the predicate. *) | ||
224 | |||
225 | val filter : ('a -> bool) -> 'a zipper -> 'a zipper | ||
226 | (** [filter p z] filters the elements of the zipper [z] according to a | ||
227 | predicate [p], i.e., keeping the elements that satisfy the | ||
228 | predicate. *) | ||
229 | |||
230 | val context_before : int -> 'a zipper -> 'a zipper | ||
231 | (** [context_before n z] will limit the zipper [z] to [n] elements before | ||
232 | the cursor. *) | ||
233 | |||
234 | val context_after : int -> 'a zipper -> 'a zipper | ||
235 | (** [context_after n z] will limit the zipper [z] to [n] elements after | ||
236 | the cursor. *) | ||
237 | |||
238 | val context : b:int -> ?a:int -> 'a zipper -> 'a zipper | ||
239 | (** [context ~b ~a z] will limit the zipper [z] to [b] elements before | ||
240 | the cursor and [a] elements after the cursor. When [a] is not | ||
241 | provided, it defaults to [b]. *) | ||
242 | |||
243 | val clear_history : 'a zipper -> 'a zipper | ||
244 | (** Clear the history of the zipper. See {!Zipper.history}. *) | ||
245 | |||
246 | (** {1 Zippers and sequences} *) | ||
247 | |||
248 | val of_seq : 'a Sequence.t -> 'a zipper | ||
249 | (** Turn a sequence into a zipper with the cursor at the beginning. *) | ||
250 | |||
251 | val to_seq : 'a zipper -> 'a Sequence.t | ||
252 | (** Return the zipper as a sequence. | ||
253 | Calling [to_seq z], with [z] being [([3; 2; 1], [4; 5])], results in | ||
254 | [[1; 2; 3; 4; 5]]. *) | ||
255 | |||
256 | val window : from:int -> len:int -> 'a zipper -> 'a Sequence.t | ||
257 | (** [windows from len z] returns a sequence containing [len] elements | ||
258 | starting from [from]. *) | ||