Skip to content

Instantly share code, notes, and snippets.

@jimblandy
Last active July 18, 2017 00:24
Show Gist options
  • Save jimblandy/30a38eb9b673979e6da8839089f79cbe to your computer and use it in GitHub Desktop.
Save jimblandy/30a38eb9b673979e6da8839089f79cbe to your computer and use it in GitHub Desktop.
Emacs major mode for editing SpiderMonkey JS_FN_HELP entries.
;;; edit-js-fn.el --- Edit JS_FN_HELP sections in SpiderMonkey shell files
;; See js-fn-edit-documentation.
(defvar js-fn-edit-original-buffer nil
"The buffer to which a js-fn-edit-mode buffer will store the edited text.")
(defvar js-fn-edit-original-region nil
"The region of js-fn-edit-original-buffer the original definition occupies.")
(defvar js-fn-edit-original-text nil
"The original text of the JS_FN_HELP form being edited.")
(defun js-fn-edit-documentation ()
"Edit the documentation of a SpiderMonkey native function defined with JS_FN_HELP."
(interactive)
(let* ((original-buffer (current-buffer))
(original-region (mapcar 'copy-marker (js-fn-bounds)))
(original-text (buffer-substring-no-properties
(car original-region) (cadr original-region)))
(buf (generate-new-buffer "*edit JS_FN_HELP*")))
(set-buffer buf)
(erase-buffer)
(js-fn-edit-mode)
(setq js-fn-edit-original-buffer original-buffer)
(setq js-fn-edit-original-region original-region)
(setq js-fn-edit-original-text original-text)
(insert original-text)
(undo-boundary)
(goto-char (point-min))
;; Unquote the summary line.
(forward-line 1)
(js-fn-unquote)
(js-fn-expect-and-delete
"," "JS_FN_HELP summary string should be followed by a comma")
;; Insert the separator line.
(insert "\n\n")
;; Unquote the detailed description.
(js-fn-unquote)
(js-fn-expect-and-delete
")," "JS_FN_HELP summary string should be followed by a closing paren and comma")
;; Remove common indentation from detailed description.
(goto-char (point-min))
(forward-line 3)
(while (< (point) (point-max))
(if (looking-at "^ ") (replace-match ""))
(forward-line 1))
(goto-char (point-min))
(forward-line 3)
(pop-to-buffer (current-buffer))
(message "Press C-c C-c when you’re finished editing.")))
(defconst js-fn-edit-header-regex
"^ JS_FN_HELP(\".*\", .*, [0-9]+, [0-9]+,"
"Regular expression for the first line of a well-formed JS_FN_HELP form.")
(defun js-fn-edit-finish ()
"Replace the original JS_FN_HELP form with the edited version.
Once you’ve edited the buffer created by \\[js-fn-edit-documentation],
this command reformats your revised text as C++ code and replaces the original
JS_FN_HELP form with a new one."
(interactive)
(goto-char (point-min))
;; Check that the buffer still seems well-formatted.
(unless (looking-at js-fn-edit-header-regex)
(message "JS_FN_HELP buffer needs to start with a valid JS_FN_HELP header"))
(forward-line 1)
(unless (looking-at ".")
(message "JS_FN_HELP buffer needs summary line"))
(forward-line 1)
(unless (and (bolp) (eolp))
(message "JS_FN_HELP buffer needs a blank line between summary and full docs"))
(forward-line 1)
(unless (looking-at ".")
(message "JS_FN_HELP buffer needs some docs after summary and blank separator line"))
;; Remove the separator line.
(delete-region (1- (point)) (point))
;; Indent the full docs.
(while (< (point) (point-max))
(unless (and (bolp) (eolp))
(insert " "))
(forward-line 1))
;; Remove any blank lines at the end, and ensure there is no final newline.
(goto-char (point-max))
(skip-chars-backward " \t\n")
(delete-region (point) (point-max))
;; Quote the summary.
(goto-char (point-min))
(forward-line 1)
(js-fn-quote-region (point) (progn (end-of-line) (point)))
(insert ",")
;; Quote the detailed description.
(forward-line 1)
(js-fn-quote-region (point) (point-max))
(insert "),\n")
;; Replace the original definition with our modified text.
(let ((new-text (buffer-string))
(original-region js-fn-edit-original-region)
(original-text js-fn-edit-original-text))
(set-buffer js-fn-edit-original-buffer)
(unless (string= (buffer-substring (car original-region) (cadr original-region))
original-text)
(error "Original text in buffer ‘%s’ has changed; not updating"
(buffer-name (current-buffer))))
(goto-char (car original-region))
(delete-region (car original-region) (cadr original-region))
(insert new-text))
(quit-restore-window nil 'kill))
(defun js-fn-bounds ()
"Return the extent of the JS_FN_HELP form around point.
The result is a list (START END)."
(save-excursion
(forward-line 1)
(let ((limit (point)))
(unless (search-backward "JS_FN_HELP" nil t)
(error "Not within a JS_FN_HELP form"))
(unless (looking-at js-fn-edit-header-regex)
(message "JS_FN_HELP header doesn’t seem to be well-formed"))
(forward-line 0)
(let ((start (point)))
(forward-sexp 2)
(forward-line 1)
(list start (point))))))
(defun js-fn-mark ()
"Set the region around the JS_FN_HELP form around point."
(interactive)
(let ((bounds (js-fn-bounds)))
(set-mark (car bounds))
(goto-char (cadr bounds))))
(defun js-fn-unquote ()
"Replace the C++ string starting at point with the text it represents.
There may be whitespace before the string literal.
If there are several string literals in succession that would be
concatenated into a single string, replace them all with the text they represent.
Leave point at the end of the text."
(with-syntax-table c++-mode-syntax-table
(let (start)
(while (progn
(setq start (point))
(skip-chars-forward " \t\n")
(looking-at "\""))
(let ((end (copy-marker (save-excursion (forward-sexp 1) (1- (point))))))
(delete-region start (1+ (point)))
(while (search-forward "\\" end 'at-limit)
(cond
((looking-at "n")
(delete-region (1- (point)) (1+ (point)))
(insert-before-markers "\n"))
((looking-at "t")
(delete-region (1- (point)) (1+ (point)))
(insert-before-markers "\t"))
(t (delete-region (1- (point)) (point)))))
(delete-region (point) (1+ (point)))))
(goto-char start))))
(defun js-fn-expect-and-delete (regexp message)
(skip-chars-forward " \t\n")
(unless (looking-at regexp)
(error message))
(replace-match ""))
(defun js-fn-quote-region (start end)
"Turn the text from START to END into a properly quoted C string literal.
If the region contains newlines, quote them reasonably.
Leave point at the end of the text."
(save-restriction
(narrow-to-region start end)
(goto-char (point-min))
(insert "\"")
(while (re-search-forward "[\n\"\\]" nil 'at-limit)
(if (string= (match-string 0) "\n")
(progn
(delete-region (1- (point)) (point))
(insert "\\n")
(if (< (point) (point-max))
(insert "\"\n\"")))
(forward-char -1)
(insert "\\")
(forward-char 1)))
(insert "\"")))
(defvar js-fn-edit-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "\C-c\C-c" 'js-fn-edit-finish)
map)
"Local keymap for edit-js-fn-mode.")
(define-derived-mode js-fn-edit-mode
text-mode "JS_FN_HELP"
"A tiny major mode for editing SpiderMonkey JS_FN_HELP forms.
\\<js-fn-edit-mode-map>With point in a JS_FN_HELP form, run the command
\\[js-fn-edit-documentation]. This will pop up a buffer in
‘js-fn-edit-mode’ that holds the JS_FN_HELP form, but with all
quoting, escaping, and common indentation removed so you can edit
the text comfortably.
When you are done, press \\[js-fn-edit-finish] to replace the
original JS_FN_HELP form with the edited text."
(setq fill-column 72)
(make-local-variable 'js-fn-edit-original-buffer)
(make-local-variable 'js-fn-edit-original-region)
(make-local-variable 'js-fn-edit-original-text))
(provide 'edit-js-fn)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment