package tiny_appkits
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Application engines from scratch: a spreadsheet, rich text, paint, draw, CAD, editors and more
Install
dune-project
Dependency
Authors
Maintainers
Sources
0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0
doc/src/tiny_appkits.appkit_editor/Tui_emacs.ml.html
Source file Tui_emacs.ml
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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387(* Claude Code * * Copyright (C) 2026 Yoann Padioleau * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * (LGPL) as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. *) open Lisp (* See Tui_emacs.mli *) (*****************************************************************************) (* The model *) (*****************************************************************************) (* a command waiting for its arguments, read in the minibuffer *) type minibuffer = { prompt : string; input : string; command : Lisp.t; args : Lisp.t list; (* read so far, backwards *) codes : string list; (* the interactive codes left, the one being read first *) } type isearch = { forward : bool; text : string; origin : int; failing : bool } type model = { lisp : Emacs_editor.lisp; keys : string; (* a prefix typed so far: C-x *) prefix : Lisp.t; (* C-u's argument for the next command, or nil *) mini : minibuffer option; isearch : isearch option; last_command : Lisp.t; } let editor (m : model) : Emacs_editor.t = m.lisp.host let buf (m : model) : Emacs_editor.buffer = Emacs_editor.current (editor m) let with_editor (m : model) (e : Emacs_editor.t) : model = { m with lisp = { m.lisp with host = e } } let with_buf (m : model) (b : Emacs_editor.buffer) : model = with_editor m (Emacs_editor.set_current b (editor m)) let say (m : model) (s : string) : model = with_editor m { (editor m) with message = s } (*****************************************************************************) (* Running a command *) (*****************************************************************************) (* a Lisp evaluation's error, into the echo area *) let run_lisp (m : model) (f : Emacs_editor.lisp -> Lisp.t * Emacs_editor.lisp) : Lisp.t option * model = match Lisp_eval.protect { m.lisp with fuel = 1_000_000 } f with | Ok v, st -> (Some v, { m with lisp = st }) | Error e, st -> (None, say { m with lisp = st } (Lisp_eval.error_message e)) let self_insert = Sym "self-insert-command" (* the command called on its arguments: the variables it reads set, an undo boundary first, and afterwards what it asked of the loop *) let rec execute (m : model) (command : Lisp.t) (args : Lisp.t list) : model = let b = buf m in let prefix = m.prefix and last = m.last_command in let e = editor m in let typing = command = self_insert && m.last_command = self_insert in let m = with_editor m (if typing then e else Emacs_editor.boundary e) in let st = m.lisp in let st = st |> Lisp_eval.set_var "last-command" m.last_command |> Lisp_eval.set_var "this-command" command |> Lisp_eval.set_var "current-prefix-arg" m.prefix |> Lisp_eval.set_var "mode-name" (Str b.mode) in let _, m = run_lisp { m with lisp = st; prefix = nil } (fun st -> Lisp_eval.apply st command args) in let this = Option.value (Lisp_eval.get_var m.lisp "this-command") ~default:command in let m = { m with last_command = this } in let request = (editor m).request in let m = with_editor m { (editor m) with request = None } in match request with | None -> m | Some (Call_interactively c) -> call_interactively m c | Some (Isearch forward) -> say { m with isearch = Some { forward; text = ""; origin = (buf m).point; failing = false } } "" | Some Universal_argument -> (* C-u C-u: 16; the prefix survives this command, which isn't one for last-command *) { m with prefix = (match prefix with Int n -> Int (n * 4) | _ -> Int 4); last_command = last } (* the codes of (interactive "p\nsName: "): what each argument is *) and call_interactively (m : model) (command : Lisp.t) : model = match Lisp_eval.interactive_spec m.lisp command with | None -> say m (Lisp.princ command ^ " is not a command") | Some spec -> let codes = match spec with Str "" | Sym "nil" -> [] | Str s -> String.split_on_char '\n' s | _ -> [] in next_argument m command [] codes and next_argument (m : model) (command : Lisp.t) (args : Lisp.t list) (codes : string list) : model = match codes with | [] -> execute m command (List.rev args) | code :: rest -> ( let b = buf m in match code.[0] with | 'p' -> next_argument m command ((match m.prefix with Int n -> Int n | _ -> Int 1) :: args) rest | 'P' -> next_argument m command (m.prefix :: args) rest | 'd' -> next_argument m command (Int (b.point + 1) :: args) rest | 'r' -> ( match b.mark with | Some mk -> next_argument m command (Int (max mk b.point + 1) :: Int (min mk b.point + 1) :: args) rest | None -> say m "The mark is not set now, so there is no region") | _ -> { m with mini = Some { prompt = String.sub code 1 (String.length code - 1); input = ""; command; args; codes } }) (*****************************************************************************) (* The minibuffer *) (*****************************************************************************) (* what the code being read can complete to *) let candidates (m : model) (mini : minibuffer) : string list = let st = m.lisp in let names l = List.sort_uniq compare (List.map fst l) in match (List.hd mini.codes).[0] with | 'C' -> List.filter (fun f -> Lisp_eval.interactive_spec st (Sym f) <> None) (names st.funs) | 'a' -> names st.funs | 'b' | 'B' -> List.map (fun (b : Emacs_editor.buffer) -> b.name) (editor m).buffers | 'f' | 'F' -> names (editor m).disk | _ -> [] let drop_last (s : string) : string = if s = "" then "" else String.sub s 0 (String.length s - 1) let starts_with ~(prefix : string) (s : string) = String.length s >= String.length prefix && String.sub s 0 (String.length prefix) = prefix let matching (m : model) (mini : minibuffer) : string list = List.filter (starts_with ~prefix:mini.input) (candidates m mini) (* the longest prefix the candidates share: what TAB completes to *) let common_prefix (l : string list) : string = match l with | [] -> "" | first :: rest -> let n = List.fold_left (fun n s -> let k = ref 0 in while !k < n && !k < String.length s && s.[!k] = first.[!k] do incr k done; !k) (String.length first) rest in String.sub first 0 n (* the text typed, made the argument its code says *) let finish (m : model) (mini : minibuffer) : model = let code = (List.hd mini.codes).[0] and input = mini.input in let m = { m with mini = None } in let arg = match code with | 'n' -> ( match int_of_string_opt (String.trim input) with Some n -> Ok (Int n) | None -> Error "Please enter a number.") | 'x' -> ( match Lisp_read.read input 0 with v, _ -> Ok v | exception Lisp_read.Error msg -> Error ("Invalid read syntax: " ^ msg)) | 'C' | 'a' -> if List.mem input (candidates m mini) then Ok (Sym input) else Error "[No match]" | 'b' | 'B' -> (* nothing typed: the buffer before this one *) if input = "" then match (editor m).buffers with _ :: other :: _ -> Ok (Str other.name) | _ -> Ok (Str (buf m).name) else Ok (Str input) | 'k' -> Ok (Str input) | _ -> if input = "" && (code = 'f' || code = 'F') then Error "No file name" else Ok (Str input) in match arg with | Ok v -> next_argument m mini.command (v :: mini.args) (List.tl mini.codes) | Error msg -> say m msg (* global-map's bindings, the newest first *) let bindings (m : model) : Lisp.t list = Option.value (Lisp.to_list (Option.value (Lisp_eval.get_var m.lisp "global-map") ~default:nil)) ~default:[] let binding (m : model) (keys : string) : Lisp.t option = List.find_map (function Cons (Str k, command) when k = keys -> Some command | _ -> None) (bindings m) (* whether some binding starts with [keys] and is longer: C-x *) let is_prefix (m : model) (keys : string) : bool = List.exists (function Cons (Str k, _) -> String.length k > String.length keys && starts_with ~prefix:keys k | _ -> false) (bindings m) let minibuffer_key (m : model) (mini : minibuffer) (k : string) : model = if (List.hd mini.codes).[0] = 'k' then (* a key sequence read whole, prefixes and all: C-h k *) let keys = mini.input ^ k in if is_prefix m keys && binding m keys = None then { m with mini = Some { mini with input = keys } } else finish m { mini with input = keys } else match k with | "\x07" -> say { m with mini = None } "Quit" | "\r" -> finish m mini | "\x7f" | "\b" -> { m with mini = Some { mini with input = drop_last mini.input } } | "\t" -> ( match matching m mini with | [] -> say m "[No match]" | l -> { m with mini = Some { mini with input = common_prefix l } }) | _ when String.length k = 1 && k.[0] >= ' ' -> { m with mini = Some { mini with input = mini.input ^ k } } | _ -> m (*****************************************************************************) (* Incremental search *) (*****************************************************************************) (* the search string found from [from], point at its end (forward) or start (backward); failing, point stays *) let find (m : model) (is : isearch) (from : int) : model = let b = buf m in match Emacs_editor.search { b with point = from } is.text ~forward:is.forward ~bound:None with | Some p -> with_buf { m with isearch = Some { is with failing = false } } { b with point = p } | None -> { m with isearch = Some { is with failing = true } } let rec isearch_key (m : model) (is : isearch) (k : string) : model = let b = buf m in let n = String.length is.text in match k with | "\x13" | "\x12" -> (* the next match, past the current one: forward from its end, backward from its start (point is at one or the other) *) let forward = k = "\x13" in let from = if forward then (if is.forward then b.point else b.point + 1) else if is.forward then b.point - 1 else b.point in if is.text = "" then m else find m { is with forward } from | "\x7f" | "\b" -> let is = { is with text = drop_last is.text } in find (with_buf m { b with point = is.origin }) is is.origin | "\x07" -> say (with_buf { m with isearch = None } { b with point = is.origin }) "Quit" | "\r" -> say (with_buf { m with isearch = None } { b with mark = Some is.origin }) "Mark saved where search started" | _ when String.length k = 1 && k.[0] >= ' ' -> (* a longer string, searched from the current match's start: it may still match there *) let is = { is with text = is.text ^ k } in find m is (if is.forward then (if is.failing then b.point else b.point - n) else b.point + n + 1) | _ -> (* any other key ends the search, then does what it does *) key (with_buf { m with isearch = None } { b with mark = Some is.origin }) k (*****************************************************************************) (* Keys *) (*****************************************************************************) (* C-g: what was half-typed forgotten, and a command for last-command, so that an undo after it undoes the undos *) and quit (m : model) : model = say { m with prefix = nil; last_command = Sym "keyboard-quit" } "Quit" and key (m : model) (k : string) : model = match (m.isearch, m.mini) with | Some is, _ -> isearch_key m is k | None, Some mini -> minibuffer_key m mini k | None, None -> ( let keys = m.keys ^ k in let m = { m with keys = "" } in match binding m keys with | Some command -> if command = Sym "keyboard-quit" then quit m else call_interactively m command | None -> if k = "\x07" then quit m else if is_prefix m keys then { m with keys } else if String.length keys = 1 && keys.[0] >= ' ' && keys.[0] < '\x7f' then begin let st = Lisp_eval.set_var "last-command-event" (Int (Char.code keys.[0])) m.lisp in call_interactively { m with lisp = st } self_insert end else say m (Emacs_editor.key_description keys ^ " is undefined")) (* the window follows point: recentred when point leaves it, as Emacs scrolls by default *) let follow (m : model) : model = let b = buf m in let line = Emacs_editor.line_of b b.point in if line < b.top || line >= b.top + Emacs_editor.window_height then with_buf m { b with top = max 0 (line - (Emacs_editor.window_height / 2)) } else m let update (ev : Tui.event) (m : model) : model = match ev with | Tick _ -> m | Key k -> (* the echo area cleared by the next key, as in Emacs *) let m = if m.mini = None && m.isearch = None then with_editor m { (editor m) with message = "" } else m in follow (key m k) (*****************************************************************************) (* The display *) (*****************************************************************************) let reverse = { Vt.plain with reverse = true } (* a line as the window shows it: cut at the right edge with a $ *) let shown (line : string) : string = let line = String.map (fun c -> if c < ' ' then '?' else c) line in if String.length line > 80 then String.sub line 0 79 ^ "$" else line let mode_line (m : model) : string = let b = buf m in let lines = Emacs_editor.line_of b (Gap_buffer.length b.text) + 1 in let where = if b.top = 0 && lines <= Emacs_editor.window_height then "All" else if b.top = 0 then "Top" else if b.top + Emacs_editor.window_height >= lines then "Bot" else Printf.sprintf "%d%%" (100 * b.top / lines) in let s = Printf.sprintf "-UUU:%s- %-18s %-4s L%-5d (%s) " (if b.modified then "**" else "--") b.name where (Emacs_editor.line_of b b.point + 1) b.mode in s ^ String.make (max 0 (80 - String.length s)) '-' let echo (m : model) : string * int option = match (m.mini, m.isearch) with | Some mini, _ -> let prompt = mini.prompt ^ if (List.hd mini.codes).[0] = 'k' then Emacs_editor.key_description mini.input else mini.input in (* the completions, after what is typed: icomplete's way *) let more = match matching m mini with | [] | [ _ ] -> "" | l when mini.input <> "" || List.length l < 12 -> " {" ^ String.concat " | " (List.filteri (fun i _ -> i < 8) l) ^ if List.length l > 8 then " ...}" else "}" | _ -> "" in (prompt ^ more, Some (String.length prompt)) | None, Some is -> let p = (if is.failing then "Failing " else "") ^ "I-search" ^ (if is.forward then "" else " backward") ^ ": " ^ is.text in (p, None) | None, None -> if m.keys <> "" then (Emacs_editor.key_description m.keys ^ "-", None) else ((editor m).message, None) let view (m : model) : Curses.t = let b = buf m in let text = Gap_buffer.to_string b.text in let lines = String.split_on_char '\n' text in let screen = Curses.create ~rows:24 ~cols:80 in let screen = ref screen in List.iteri (fun i line -> if i >= b.top && i < b.top + Emacs_editor.window_height then screen := Curses.put (i - b.top) 0 (shown line) !screen) lines; let screen = Curses.put ~attrs:reverse Emacs_editor.window_height 0 (mode_line m) !screen in let echo_text, mini_col = echo m in let screen = Curses.put (Emacs_editor.window_height + 1) 0 (shown echo_text) screen in let line = Emacs_editor.line_of b b.point in let col = b.point - Emacs_editor.line_start b line in let cursor = match mini_col with Some c -> (Emacs_editor.window_height + 1, min 79 c) | None -> (line - b.top, min 79 col) in Curses.cursor (Some cursor) screen (*****************************************************************************) (* The disk, and the start *) (*****************************************************************************) let dot_emacs = {|;; .emacs: read by TinyEmacs when it starts, as Emacs reads yours. ;; Change something, then M-x eval-buffer, or C-x C-e after one ;; expression, and it takes effect at once: the editor is this Lisp. ;; a command of our own: M-x hello, or C-c h (defun hello () "Say hello in the echo area." (interactive) (message "Hello from your .emacs! %d buffers." (length (buffer-list)))) (global-set-key "\C-ch" 'hello) ;; C-x l: the line point is on (global-set-key "\C-xl" 'what-line) |} let tutorial = {|TinyEmacs tutorial. Emacs commands use the Control key (C-f: hold Control, type f) and the Meta key (M-f: hold Alt, or type Escape then f). C-v the next screen M-v the previous one C-f forward a character C-b back a character C-n next line C-p previous line M-f forward a word M-b back a word C-a start of the line C-e end of the line M-< start of the buffer M-> end of the buffer Type text anywhere: it is inserted. DEL deletes before the cursor, C-d after it. C-k kills the rest of the line; C-y yanks it back, here or anywhere. C-SPC sets the mark, move, then C-w kills the region between; M-w copies it. C-/ undoes, again and again; after any other command, it undoes the undos. C-s searches as you type; C-s again for the next one, RET to stop. C-x C-f visits a file, C-x C-s saves it, C-x b switches buffers, C-x C-b lists them. C-g cancels whatever is half-typed. Everything is Lisp. In *scratch*, type (+ 1 2) then C-j. M-x runs a command by name (TAB completes). C-h k then a key says what it runs; C-h f what a function does. Visit .emacs to see how a key is bound. C-x C-c quits. |} let notes = "Things to do:\n- learn C-k and C-y\n- write a command in .emacs\n- try C-u C-u C-f\n" let disk = [ (".emacs", dot_emacs); ("TUTORIAL", tutorial); ("notes.txt", notes) ] let init : model = let lisp = Emacs_editor.create ~disk ~simple:Emacs_simple.text in let m = { lisp; keys = ""; prefix = nil; mini = None; isearch = None; last_command = nil } in (* the user's .emacs, as Emacs loads it when it starts *) let _, m = run_lisp m (fun st -> (nil, Lisp_eval.load st dot_emacs)) in if (editor m).message <> "" then m else say m "Welcome to TinyEmacs. C-h t: the tutorial; C-x C-c: quit." let program : model Tui.program = { init; update; view; over = (fun m -> (editor m).quit) } let buffer_name (m : model) = (buf m).name let text (m : model) = Gap_buffer.to_string (buf m).text let point (m : model) = (buf m).point + 1 let message (m : model) = (editor m).message let file (m : model) (name : string) = List.assoc_opt name (editor m).disk
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>