Editing a Model / View in Emacs

I have several JSON files, and I am writing a mode that allows you to edit one property of a JSON object independently of the rest. For instance:

foo.json:

{
  "creation_timestamp": "1411210038.000000",
  "description": "lorem ipsum.\ndolor sit amet.",
  "version": 4
}

Opening foo.json results in this buffer:

lorem ipsum.

dolor sit amet.

Changing the first line to "foo bar" and saving the file results in foo.jsononly with the updated field description:

{
  "creation_timestamp": "1411210038.000000",
  "description": "foo bar.\ndolor sit amet.",
  "version": 4
}

What is the best strategy for this? My current attempt is this:

  • open the json file with find file
  • create an invisible overlay from min-point to max-point
  • parse json
  • insert the value of the property descriptionin point-min, creating a "view"
  • add the hook of the local write file and the hook after saving

local-write-file "" , json . after-save "" , .

. , ?

+3
2

format-alist . :

(defvar-local my-head nil
  "Header of json file cut off by json-descr format.")

(defvar-local my-tail nil
  "Tail of json file cut off by json-descr format.")

(defun my-from-fn (BEGIN END)
  "`format-alist'"
  (save-restriction
    (narrow-to-region BEGIN END)
    (goto-char (point-min))
    (let* ((b (re-search-forward "^[[:blank:]]*\"description\":[[:blank:]]*\"" nil t))
       (e (ignore-errors (1- (scan-sexps (1- b) 1)))))
      (unless (and b e)
    (error "Error in original mode")) ;;< TODO some more sensible error message
      ;; Save head and tail and delete corresponding buffer regions:
      (setq-local my-head (buffer-substring-no-properties (point-min) b))
      (setq-local my-tail (buffer-substring-no-properties e (point-max)))
      (delete-region e (point-max))
      (delete-region (point-min) b)
      ;; Formatting:
      (goto-char (point-min))
      (while (search-forward "\\n" nil t)
    (replace-match "\n"))
      )
    (point-max) ;;< required by `format-alist'
    ))

(defun my-to-fn (BEGIN END BUFFER)
  "`format-alist'"
  (save-restriction
    (narrow-to-region BEGIN END)
    ;; Formatting:
    (goto-char (point-min))
    (while (search-forward "\n" nil t)
      (replace-match "\\\\n"))
    ;; Insert head and tail:
    (let ((head (with-current-buffer BUFFER my-head))
      (tail (with-current-buffer BUFFER my-tail)))
      (goto-char (point-min))
      (insert head)
      (goto-char (point-max))
      (insert tail))
    (point-max)))

(add-to-list 'format-alist
         '(json-descr
           "File format for editing a single property of a json object."
           nil
           my-from-fn
           my-to-fn
           t ; MODIFY: my-to-fn modifies the buffer
           nil
           nil))

(define-derived-mode my-mode fundamental-mode "JDescr"
  "Major mode for editing json description properties."
  (format-decode-buffer 'json-descr))

, . . , . .

, , . . ( , .)

(defvar-local original-mode-other nil
  "Other buffer related to the current one.")


(define-derived-mode original-mode special-mode ""
  "Opens file in invisible auxiliary buffer."
  (let* ((b (re-search-forward "^[[:blank:]]*\"description\":[[:blank:]]*\"" nil t))
     (e (ignore-errors (1- (scan-sexps (1- b) 1))))
     (original-name (buffer-name))
     (original-buffer (current-buffer))
     str)
    (unless (and b e)
      (error "Error in original mode")) ;; TODO some more sensible error message
    (narrow-to-region b e)
    (setq str (buffer-substring-no-properties b e))
    (rename-buffer (concat " *" original-name))
    (with-current-buffer (switch-to-buffer (get-buffer-create original-name))
      ;; Set-up the clone buffer for editing the transformed content:
      (set-visited-file-name (buffer-file-name original-buffer) t)
      (setq original-mode-other original-buffer)
      (insert str)
      (set-buffer-modified-p nil)
      ;; Transform content to the format of the clone buffer:
      (goto-char (point-min))
      (while (search-forward "\\n" nil t) ;; TODO: Skip escaped \n.
    (replace-match "\n"))
      (add-to-list 'write-contents-functions  (lambda ()
                        ;; Transfer content to original buffer
                        (let ((str (buffer-substring-no-properties (point-min) (point-max))))
                          (with-current-buffer original-mode-other
                            (let ((inhibit-read-only t))
                              (delete-region (point-min) (point-max))
                              (insert str)
                              (goto-char (point-min))
                              ;; Transform content to the format of the original buffer:
                              (while (search-forward "\n" nil t)
                            (replace-match "\\\\n"))
                              (save-buffer)
                              )))
                        (set-buffer-modified-p nil)
                        t))
      (add-hook 'kill-buffer-hook (lambda ()
                    (kill-buffer original-mode-other)) t t)
      )))
0

, ( , / )?

, . , , , , :

  • (, ) .

  • (, C-c C-c), .

+ , ( , ). bmkp-edit-tags. ( C-c C-c ) bmkp-edit-tags-send. , . , :


(defmacro bmkp-with-output-to-plain-temp-buffer (buf &rest body)
  "Like `with-output-to-temp-buffer', but with no `*Help*' navigation stuff."
  `(unwind-protect
    (progn
      (remove-hook 'temp-buffer-setup-hook 'help-mode-setup)
      (remove-hook 'temp-buffer-show-hook  'help-mode-finish)
      (with-output-to-temp-buffer ,buf ,@body))
    (add-hook 'temp-buffer-setup-hook 'help-mode-setup)
    (add-hook 'temp-buffer-show-hook  'help-mode-finish)))

(define-derived-mode bmkp-edit-tags-mode emacs-lisp-mode
    "Edit Bookmark Tags"
  "Mode for editing bookmark tags.
When you have finished composing, type \\[bmkp-edit-tags-send]."
  :group 'bookmark-plus)

;; This binding must be defined *after* the mode, so `bmkp-edit-tags-mode-map' is defined.
;; (Alternatively, we could use a `defvar' to define `bmkp-edit-tags-mode-map' before
;; calling `define-derived-mode'.)
(define-key bmkp-edit-tags-mode-map "\C-c\C-c" 'bmkp-edit-tags-send)

(defun bmkp-edit-tags (bookmark)        ; Bound to `C-x p t e'
  "Edit BOOKMARK tags, and maybe save the result.
The edited value must be a list each of whose elements is either a
 string or a cons whose key is a string.
BOOKMARK is a bookmark name or a bookmark record."
  (interactive (list (bookmark-completing-read "Edit tags for bookmark" (bmkp-default-bookmark-name))))
  (setq bookmark  (bmkp-get-bookmark-in-alist bookmark))
  (let* ((btags    (bmkp-get-tags bookmark))
         (bmkname  (bmkp-bookmark-name-from-record bookmark))
         (edbuf    (format "*Edit Tags for Bookmark `%s'*" bmkname)))
    (setq bmkp-return-buffer  (current-buffer))
    (bmkp-with-output-to-plain-temp-buffer edbuf
      (princ
       (substitute-command-keys
        (concat ";; Edit tags for bookmark\n;;\n;; \"" bmkname "\"\n;;\n"
                ";; The edited value must be a list each of whose elements is\n"
                ";; either a string or a cons whose key is a string.\n;;\n"
                ";; DO NOT MODIFY THESE COMMENTS.\n;;\n"
                ";; Type \\<bmkp-edit-tags-mode-map>`\\[bmkp-edit-tags-send]' when done.\n\n")))
      (let ((print-circle  bmkp-propertize-bookmark-names-flag)) (pp btags))
      (goto-char (point-min)))
    (pop-to-buffer edbuf)
    (buffer-enable-undo)
    (with-current-buffer (get-buffer edbuf) (bmkp-edit-tags-mode))))

(defun bmkp-edit-tags-send (&optional batchp)
  "Use buffer contents as the internal form of a bookmark tags.
DO NOT MODIFY the header comment lines, which begin with `;;'."
  (interactive)
  (unless (eq major-mode 'bmkp-edit-tags-mode) (error "Not in `bmkp-edit-tags-mode'"))
  (let (bname)
    (unwind-protect
         (let (tags bmk)
           (goto-char (point-min))
           (unless (search-forward ";; Edit tags for bookmark\n;;\n;; ")
             (error "Missing header in edit buffer"))
           (unless (stringp (setq bname  (read (current-buffer))))
             (error "Bad bookmark name in edit-buffer header"))
           (unless (setq bmk  (bmkp-get-bookmark-in-alist bname 'NOERROR))
             (error "No such bookmark: `%s'" bname))
           (unless (bmkp-bookmark-type bmk) (error "Invalid bookmark"))
           (goto-char (point-min))
           (setq tags  (read (current-buffer)))
           (unless (listp tags) (error "Tags sexp is not a list of strings or an alist with string keys"))
           (bookmark-prop-set bmk 'tags tags)
           (setq bname  (bmkp-bookmark-name-from-record bmk))
           (bmkp-record-visit bmk batchp)
           (bmkp-refresh/rebuild-menu-list bname batchp)
           (bmkp-maybe-save-bookmarks)
           (unless batchp (message "Updated bookmark file with edited tags")))
      (kill-buffer (current-buffer)))
    (when bmkp-return-buffer
      (pop-to-buffer bmkp-return-buffer)
      (when (equal (buffer-name (current-buffer)) "*Bookmark List*")
        (bmkp-bmenu-goto-bookmark-named bname)))))

:

  • .

  • bmkp-with-output-to-plain-temp-buffer ( with-output-to-temp-buffer, Emacs , ).

  • , C-c C-c save-and-exit.

  • . , .

  • save-and-exit (bmkp-edit-tags-send) , -. . .

0

All Articles