Add 20 Emacs features round 13: string-reverse, sort-words, uniq-lines, encode/decode-html-entities, url-decode, camelcase-to-snake, snake-to-camel, kebab-to-camel, wrap-region, unwrap-region, quote-region, strip-comments, insert-file-header, insert-license, insert-shebang, open-in-external, copy-line-number, rename-file-and-buffer, sudo-edit

ober

d1369d947d8f39ababfc66b956e2e7e864f1c993

diff --git a/docs/jemacs-vs-emacs.md b/docs/jemacs-vs-emacs.md
index 554e0d4..ed1bcf8 100644
--- a/docs/jemacs-vs-emacs.md
+++ b/docs/jemacs-vs-emacs.md
@@ -1366,6 +1366,26 @@ No remaining Tier 1 gaps. All core editing, completion, and navigation features 
 | MD5 hash | :orange_circle: | Compute MD5 hash of text via md5sum |
 | Word frequency | :orange_circle: | Word frequency analysis with top-50 display |
 | Text statistics | :orange_circle: | Characters, words, lines, sentences, reading time |
+| String reverse | :orange_circle: | Reverse selected text or current line |
+| Sort words | :orange_circle: | Alphabetically sort words in selection |
+| Unique lines | :orange_circle: | Remove duplicate lines from buffer |
+| Encode HTML entities | :orange_circle: | Encode &<>"' as HTML entities |
+| Decode HTML entities | :orange_circle: | Decode HTML entities back to characters |
+| URL decode | :orange_circle: | Decode URL-encoded text via Python |
+| CamelCase to snake_case | :orange_circle: | Case conversion for identifiers |
+| snake_case to camelCase | :orange_circle: | Case conversion for identifiers |
+| kebab-case to camelCase | :orange_circle: | Case conversion for identifiers |
+| Wrap region | :orange_circle: | Wrap selection with user-specified chars |
+| Unwrap region | :orange_circle: | Remove outermost wrapping characters |
+| Quote region | :orange_circle: | Prefix each line with > |
+| Strip comments | :orange_circle: | Remove comment lines (#, //, ;) |
+| Insert file header | :orange_circle: | Auto-detect comment style, insert header template |
+| Insert license | :orange_circle: | MIT, Apache, GPL, BSD, Unlicense templates |
+| Insert shebang | :orange_circle: | Shebang lines for bash, python, ruby, node, etc. |
+| Open in external app | :orange_circle: | Open file with xdg-open |
+| Copy line number | :orange_circle: | Copy current line number to kill ring |
+| Rename file and buffer | :orange_circle: | Rename file on disk and update buffer |
+| Sudo edit | :orange_circle: | Re-open file with sudo privileges |
 
 ---
 
diff --git a/src/jerboa-emacs/editor-extra-final.ss b/src/jerboa-emacs/editor-extra-final.ss
index 338ba7e..0f43644 100644
--- a/src/jerboa-emacs/editor-extra-final.ss
+++ b/src/jerboa-emacs/editor-extra-final.ss
@@ -5348,3 +5348,239 @@
         (editor-set-text ed content)
         (editor-goto-pos ed 0)
         (echo-message! echo (str words " words, " lines " lines"))))))
+
+;; ===== Round 13 Batch 2 =====
+
+;; --- Feature 11: Unwrap Region ---
+
+(def (cmd-unwrap-region app)
+  "Remove the outermost wrapping characters from selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (or (= sel-start sel-end) (< (- sel-end sel-start) 2))
+      (echo-message! echo "No selection or too short")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (unwrapped (substring text 1 (- (string-length text) 1))))
+        (editor-replace-selection ed unwrapped)
+        (echo-message! echo "Unwrapped")))))
+
+;; --- Feature 12: Quote Region ---
+
+(def (cmd-quote-region app)
+  "Quote each line in the selection with > prefix."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (lines (string-split text #\newline))
+             (quoted (map (lambda (l) (str "> " l)) lines))
+             (result (string-join quoted "\n")))
+        (editor-replace-selection ed result)
+        (echo-message! echo "Region quoted")))))
+
+;; --- Feature 13: Strip Comments ---
+
+(def (cmd-strip-comments app)
+  "Remove comment lines from the buffer (lines starting with # or //)."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0))
+         (text (editor-get-text ed len))
+         (lines (string-split text #\newline))
+         (non-comments (filter (lambda (l)
+                                 (let ((trimmed (string-trim l)))
+                                   (and (> (string-length trimmed) 0)
+                                        (not (string-prefix? "#" trimmed))
+                                        (not (string-prefix? "//" trimmed))
+                                        (not (string-prefix? ";" trimmed)))))
+                               lines))
+         (removed (- (length lines) (length non-comments))))
+    (editor-set-text ed (string-join non-comments "\n"))
+    (editor-goto-pos ed 0)
+    (echo-message! echo (str "Stripped " removed " comment lines"))))
+
+;; --- Feature 14: Insert File Header ---
+
+(def (cmd-insert-file-header app)
+  "Insert a file header comment with filename, author, date."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (buf (edit-window-buffer win))
+         (file (buffer-file buf))
+         (name (if file
+                 (let loop ((i (- (string-length file) 1)))
+                   (cond ((< i 0) file)
+                         ((char=? (string-ref file i) #\/)
+                          (substring file (+ i 1) (string-length file)))
+                         (else (loop (- i 1)))))
+                 "untitled"))
+         (user (or (getenv "USER") "unknown"))
+         (date (number->string (time-second (current-time))))
+         (ext (if file (path-extension file) ""))
+         (comment-style (cond
+                          ((member ext '("ss" "scm" "el" "lisp" "clj")) ";;")
+                          ((member ext '("py" "rb" "sh" "bash" "zsh" "yaml" "yml")) "#")
+                          ((member ext '("c" "cpp" "h" "java" "js" "ts" "go" "rs")) "//")
+                          (else "//")))
+         (header (string-append
+                   comment-style " " name "\n"
+                   comment-style " Author: " user "\n"
+                   comment-style " Created: " date "\n"
+                   comment-style " Description: \n\n")))
+    (send-message ed SCI_GOTOPOS 0 0)
+    (editor-insert-text ed header)
+    (echo-message! echo "File header inserted")))
+
+;; --- Feature 15: Insert License ---
+
+(def *license-templates*
+  '(("MIT" . "MIT License\n\nCopyright (c) [year] [author]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND.")
+    ("Apache-2.0" . "Licensed under the Apache License, Version 2.0")
+    ("GPL-3.0" . "This program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License.")
+    ("BSD-2" . "Redistribution and use in source and binary forms, with or without\nmodification, are permitted.")
+    ("Unlicense" . "This is free and unencumbered software released into the public domain.")))
+
+(def (cmd-insert-license app)
+  "Insert a license header into the buffer."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (row (tui-rows)) (width (tui-cols))
+         (names (map car *license-templates*))
+         (choice (echo-read-string-with-completion echo "License: " names row width)))
+    (when (and choice (not (string-empty? choice)))
+      (let ((tmpl (assoc choice *license-templates*)))
+        (if tmpl
+          (begin
+            (editor-insert-text ed (cdr tmpl))
+            (echo-message! echo (str "Inserted " choice " license")))
+          (echo-message! echo "Unknown license"))))))
+
+;; --- Feature 16: Insert Shebang ---
+
+(def *shebang-templates*
+  '(("bash" . "#!/usr/bin/env bash")
+    ("sh" . "#!/bin/sh")
+    ("python" . "#!/usr/bin/env python3")
+    ("python2" . "#!/usr/bin/env python2")
+    ("ruby" . "#!/usr/bin/env ruby")
+    ("node" . "#!/usr/bin/env node")
+    ("perl" . "#!/usr/bin/env perl")
+    ("scheme" . "#!/usr/bin/env scheme --script")))
+
+(def (cmd-insert-shebang app)
+  "Insert a shebang line at the top of the buffer."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (row (tui-rows)) (width (tui-cols))
+         (names (map car *shebang-templates*))
+         (choice (echo-read-string-with-completion echo "Shebang: " names row width)))
+    (when (and choice (not (string-empty? choice)))
+      (let ((tmpl (assoc choice *shebang-templates*)))
+        (when tmpl
+          (send-message ed SCI_GOTOPOS 0 0)
+          (editor-insert-text ed (str (cdr tmpl) "\n"))
+          (echo-message! echo (str "Shebang inserted: " (cdr tmpl))))))))
+
+;; --- Feature 17: Open in External App ---
+
+(def (cmd-open-in-external app)
+  "Open the current file with the system's default application."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (buf (edit-window-buffer win))
+         (file (buffer-file buf)))
+    (if (not file)
+      (echo-message! echo "No file associated with buffer")
+      (with-catch
+        (lambda (e) (echo-message! echo (str "open error: " e)))
+        (lambda ()
+          (let-values (((si so se pid)
+                        (open-process-ports
+                          (str "xdg-open " (shell-quote file) " 2>/dev/null &")
+                          'block (native-transcoder))))
+            (close-port si) (close-port so) (close-port se)
+            (echo-message! echo (str "Opened externally: " file))))))))
+
+;; --- Feature 18: Copy Line Number ---
+
+(def (cmd-copy-line-number app)
+  "Copy the current line number to the kill ring."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (line-num (+ 1 (send-message ed SCI_LINEFROMPOSITION
+                          (send-message ed SCI_GETCURRENTPOS 0 0) 0)))
+         (line-str (number->string line-num)))
+    (send-message ed SCI_COPYTEXT (string-length line-str) line-str)
+    (echo-message! echo (str "Copied line number: " line-num))))
+
+;; --- Feature 19: Rename File and Buffer ---
+
+(def (cmd-rename-file-and-buffer app)
+  "Rename the current file on disk and update the buffer name."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (buf (edit-window-buffer win))
+         (file (buffer-file buf)))
+    (if (not file)
+      (echo-message! echo "No file associated with buffer")
+      (let* ((row (tui-rows)) (width (tui-cols))
+             (new-name (echo-read-string echo (str "Rename to (from " file "): ") row width)))
+        (when (and new-name (not (string-empty? new-name)))
+          (let ((new-path (string-trim new-name)))
+            (with-catch
+              (lambda (e) (echo-message! echo (str "Rename error: " e)))
+              (lambda ()
+                (rename-file file new-path)
+                (buffer-file-set! buf new-path)
+                (echo-message! echo (str "Renamed to " new-path))))))))))
+
+;; --- Feature 20: Sudo Edit ---
+
+(def (cmd-sudo-edit app)
+  "Re-open the current file with sudo privileges."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (buf (edit-window-buffer win))
+         (file (buffer-file buf)))
+    (if (not file)
+      (echo-message! echo "No file associated with buffer")
+      (with-catch
+        (lambda (e) (echo-message! echo (str "sudo error: " e)))
+        (lambda ()
+          (let-values (((si so se pid)
+                        (open-process-ports (str "sudo cat " (shell-quote file))
+                          'block (native-transcoder))))
+            (close-port si)
+            (let loop ((lines '()))
+              (let ((line (get-line so)))
+                (if (eof-object? line)
+                  (begin
+                    (close-port so) (close-port se)
+                    (let ((content (string-join (reverse lines) "\n")))
+                      (editor-set-text ed content)
+                      (editor-goto-pos ed 0)
+                      (echo-message! echo (str "Opened with sudo: " file))))
+                  (loop (cons line lines)))))))))))
diff --git a/src/jerboa-emacs/editor-extra-modes.ss b/src/jerboa-emacs/editor-extra-modes.ss
index 8f5fc7a..241fa6e 100644
--- a/src/jerboa-emacs/editor-extra-modes.ss
+++ b/src/jerboa-emacs/editor-extra-modes.ss
@@ -5649,3 +5649,276 @@
              (ed (edit-window-editor win)))
         (send-message ed SCI_COPYTEXT (string-length name) name)
         (echo-message! echo (str "Copied: " name))))))
+
+;; ===== Round 13 Batch 1 =====
+
+;; --- Feature 1: String Reverse ---
+
+(def (cmd-string-reverse app)
+  "Reverse the selected text or current line."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      ;; Reverse current line
+      (let* ((line-num (send-message ed SCI_LINEFROMPOSITION
+                         (send-message ed SCI_GETCURRENTPOS 0 0) 0))
+             (start (send-message ed SCI_POSITIONFROMLINE line-num 0))
+             (end (send-message ed SCI_GETLINEENDPOSITION line-num 0))
+             (text (editor-get-text-range ed start (- end start)))
+             (reversed (list->string (reverse (string->list text)))))
+        (send-message ed SCI_SETTARGETSTART start 0)
+        (send-message ed SCI_SETTARGETEND end 0)
+        (send-message ed SCI_REPLACETARGET -1 reversed)
+        (echo-message! echo "Line reversed"))
+      ;; Reverse selection
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (reversed (list->string (reverse (string->list text)))))
+        (editor-replace-selection ed reversed)
+        (echo-message! echo "Selection reversed")))))
+
+;; --- Feature 2: Sort Words ---
+
+(def (cmd-sort-words app)
+  "Sort words in selection or current line alphabetically."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection — select text to sort words")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (words (filter (lambda (w) (> (string-length w) 0))
+                            (string-split text #\space)))
+             (sorted (sort string<? words))
+             (result (string-join sorted " ")))
+        (editor-replace-selection ed result)
+        (echo-message! echo (str "Sorted " (length sorted) " words"))))))
+
+;; --- Feature 3: Unique Lines ---
+
+(def (cmd-uniq-lines app)
+  "Remove duplicate lines from the buffer."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (len (send-message ed SCI_GETLENGTH 0 0))
+         (text (editor-get-text ed len))
+         (lines (string-split text #\newline))
+         (seen (make-hash-table))
+         (unique (filter (lambda (line)
+                           (if (hash-key? seen line) #f
+                             (begin (hash-put! seen line #t) #t)))
+                         lines))
+         (removed (- (length lines) (length unique))))
+    (editor-set-text ed (string-join unique "\n"))
+    (editor-goto-pos ed 0)
+    (echo-message! echo (str "Removed " removed " duplicate lines"))))
+
+;; --- Feature 4: Encode HTML Entities ---
+
+(def (cmd-encode-html-entities app)
+  "Encode special characters as HTML entities in selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (encoded (let loop ((chars (string->list text)) (acc '()))
+                        (if (null? chars)
+                          (list->string (reverse acc))
+                          (let ((c (car chars)))
+                            (cond
+                              ((char=? c #\&) (loop (cdr chars) (append (reverse (string->list "&amp;")) acc)))
+                              ((char=? c #\<) (loop (cdr chars) (append (reverse (string->list "&lt;")) acc)))
+                              ((char=? c #\>) (loop (cdr chars) (append (reverse (string->list "&gt;")) acc)))
+                              ((char=? c #\") (loop (cdr chars) (append (reverse (string->list "&quot;")) acc)))
+                              ((char=? c #\') (loop (cdr chars) (append (reverse (string->list "&#39;")) acc)))
+                              (else (loop (cdr chars) (cons c acc)))))))))
+        (editor-replace-selection ed encoded)
+        (echo-message! echo "HTML entities encoded")))))
+
+;; --- Feature 5: Decode HTML Entities ---
+
+(def (cmd-decode-html-entities app)
+  "Decode HTML entities back to characters in selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (decoded text))
+        ;; Simple entity replacements
+        (let* ((d1 (let loop ((s decoded))
+                     (let ((pos (string-contains s "&amp;")))
+                       (if (not pos) s
+                         (loop (string-append (substring s 0 pos) "&"
+                                 (substring s (+ pos 5) (string-length s))))))))
+               (d2 (let loop ((s d1))
+                     (let ((pos (string-contains s "&lt;")))
+                       (if (not pos) s
+                         (loop (string-append (substring s 0 pos) "<"
+                                 (substring s (+ pos 4) (string-length s))))))))
+               (d3 (let loop ((s d2))
+                     (let ((pos (string-contains s "&gt;")))
+                       (if (not pos) s
+                         (loop (string-append (substring s 0 pos) ">"
+                                 (substring s (+ pos 4) (string-length s))))))))
+               (d4 (let loop ((s d3))
+                     (let ((pos (string-contains s "&quot;")))
+                       (if (not pos) s
+                         (loop (string-append (substring s 0 pos) "\""
+                                 (substring s (+ pos 6) (string-length s)))))))))
+          (editor-replace-selection ed d4)
+          (echo-message! echo "HTML entities decoded"))))))
+
+;; --- Feature 6: URL Decode ---
+
+(def (cmd-url-decode app)
+  "Decode URL-encoded text in selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let ((text (editor-get-text-range ed sel-start (- sel-end sel-start))))
+        (with-catch
+          (lambda (e) (echo-message! echo (str "Decode error: " e)))
+          (lambda ()
+            (let-values (((si so se pid)
+                          (open-process-ports
+                            (str "python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read()),end=\"\")' 2>/dev/null")
+                            'block (native-transcoder))))
+              (display text si)
+              (close-port si)
+              (let ((decoded (get-line so)))
+                (close-port so) (close-port se)
+                (when (not (eof-object? decoded))
+                  (editor-replace-selection ed decoded)
+                  (echo-message! echo "URL decoded"))))))))))
+
+;; --- Feature 7: CamelCase to snake_case ---
+
+(def (cmd-camelcase-to-snake app)
+  "Convert camelCase/PascalCase to snake_case in selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (result (let loop ((chars (string->list text)) (acc '()) (prev-lower #f))
+                       (if (null? chars)
+                         (list->string (reverse acc))
+                         (let ((c (car chars)))
+                           (if (and prev-lower (char-upper-case? c))
+                             (loop (cdr chars) (cons (char-downcase c) (cons #\_ acc)) #f)
+                             (loop (cdr chars) (cons (char-downcase c) acc) (char-lower-case? c))))))))
+        (editor-replace-selection ed result)
+        (echo-message! echo "Converted to snake_case")))))
+
+;; --- Feature 8: snake_case to camelCase ---
+
+(def (cmd-snake-to-camel app)
+  "Convert snake_case to camelCase in selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (parts (string-split text #\_))
+             (result (if (null? parts) ""
+                       (string-append
+                         (car parts)
+                         (apply string-append
+                           (map (lambda (p)
+                                  (if (> (string-length p) 0)
+                                    (string-append
+                                      (string (char-upcase (string-ref p 0)))
+                                      (substring p 1 (string-length p)))
+                                    ""))
+                                (cdr parts)))))))
+        (editor-replace-selection ed result)
+        (echo-message! echo "Converted to camelCase")))))
+
+;; --- Feature 9: kebab-case to camelCase ---
+
+(def (cmd-kebab-to-camel app)
+  "Convert kebab-case to camelCase in selection."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+             (parts (string-split text #\-))
+             (result (if (null? parts) ""
+                       (string-append
+                         (car parts)
+                         (apply string-append
+                           (map (lambda (p)
+                                  (if (> (string-length p) 0)
+                                    (string-append
+                                      (string (char-upcase (string-ref p 0)))
+                                      (substring p 1 (string-length p)))
+                                    ""))
+                                (cdr parts)))))))
+        (editor-replace-selection ed result)
+        (echo-message! echo "Converted to camelCase")))))
+
+;; --- Feature 10: Wrap Region ---
+
+(def (cmd-wrap-region app)
+  "Wrap the selection with user-specified characters."
+  (let* ((echo (app-state-echo app))
+         (frame (app-state-frame app))
+         (win (current-window frame))
+         (ed (edit-window-editor win))
+         (sel-start (send-message ed SCI_GETSELECTIONSTART 0 0))
+         (sel-end (send-message ed SCI_GETSELECTIONEND 0 0)))
+    (if (= sel-start sel-end)
+      (echo-message! echo "No selection")
+      (let* ((row (tui-rows)) (width (tui-cols))
+             (wrapper (echo-read-string echo "Wrap with (e.g. \" or ( or <tag>): " row width)))
+        (when (and wrapper (not (string-empty? wrapper)))
+          (let* ((text (editor-get-text-range ed sel-start (- sel-end sel-start)))
+                 (open-char (string-trim wrapper))
+                 (close-char (cond
+                               ((string=? open-char "(") ")")
+                               ((string=? open-char "[") "]")
+                               ((string=? open-char "{") "}")
+                               ((string=? open-char "<") ">")
+                               ((string=? open-char "\"") "\"")
+                               ((string=? open-char "'") "'")
+                               ((string=? open-char "`") "`")
+                               (else open-char)))
+                 (wrapped (str open-char text close-char)))
+            (editor-replace-selection ed wrapped)
+            (echo-message! echo "Region wrapped")))))))
diff --git a/src/jerboa-emacs/editor-extra-regs2.ss b/src/jerboa-emacs/editor-extra-regs2.ss
index 28ff72c..51852d1 100644
--- a/src/jerboa-emacs/editor-extra-regs2.ss
+++ b/src/jerboa-emacs/editor-extra-regs2.ss
@@ -1703,4 +1703,26 @@
   (register-command! 'md5-hash cmd-md5-hash)
   (register-command! 'word-frequency cmd-word-frequency)
   (register-command! 'text-statistics cmd-text-statistics)
+  ;; Round 13 batch 1: string-reverse, sort-words, uniq-lines, encode-html-entities, decode-html-entities, url-decode, camelcase-to-snake, snake-to-camel, kebab-to-camel, wrap-region
+  (register-command! 'string-reverse cmd-string-reverse)
+  (register-command! 'sort-words cmd-sort-words)
+  (register-command! 'uniq-lines cmd-uniq-lines)
+  (register-command! 'encode-html-entities cmd-encode-html-entities)
+  (register-command! 'decode-html-entities cmd-decode-html-entities)
+  (register-command! 'url-decode cmd-url-decode)
+  (register-command! 'camelcase-to-snake cmd-camelcase-to-snake)
+  (register-command! 'snake-to-camel cmd-snake-to-camel)
+  (register-command! 'kebab-to-camel cmd-kebab-to-camel)
+  (register-command! 'wrap-region cmd-wrap-region)
+  ;; Round 13 batch 2: unwrap-region, quote-region, strip-comments, insert-file-header, insert-license, insert-shebang, open-in-external, copy-line-number, rename-file-and-buffer, sudo-edit
+  (register-command! 'unwrap-region cmd-unwrap-region)
+  (register-command! 'quote-region cmd-quote-region)
+  (register-command! 'strip-comments cmd-strip-comments)
+  (register-command! 'insert-file-header cmd-insert-file-header)
+  (register-command! 'insert-license cmd-insert-license)
+  (register-command! 'insert-shebang cmd-insert-shebang)
+  (register-command! 'open-in-external cmd-open-in-external)
+  (register-command! 'copy-line-number cmd-copy-line-number)
+  (register-command! 'rename-file-and-buffer cmd-rename-file-and-buffer)
+  (register-command! 'sudo-edit cmd-sudo-edit)
 )