在Emacs中实现类似Model/View的编辑

4

我有一些JSON文件,我正在编写一个模式,该模式允许独立于其他部分编辑JSON对象的单个属性。例如:

foo.json:

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

打开foo.json文件会显示以下内容:
lorem ipsum.

dolor sit amet.

将第一行修改为“foo bar”,并保存文件,结果是更新了description字段的foo.json文件。
{
  "creation_timestamp": "1411210038.000000",
  "description": "foo bar.\ndolor sit amet.",
  "version": 4
}

什么是最佳策略? 我目前的尝试是这样的:
  1. 使用find-file打开JSON文件
  2. 从point-min到point-max创建一个不可见的覆盖层
  3. 解析JSON
  4. 在point-min处插入description属性的值,创建一个“视图”
  5. 添加local-write-file hook和after-save hook
local-write-file hook会删除“视图”,更新覆盖层中的JSON,并保存文件。 after-save hook会重新创建“视图”,以便用户可以继续编辑。
这种方法很啰嗦,而且容易出错。 有没有更好的方法来处理屏幕表示与磁盘表示不同的数据?
2个回答

0

你的使用情况是否真的像你描述的那样简单(不是解决方案概述,而是问题/使用情况)?

如果是这样,你的解决方案听起来有些过度。如果使用情况就像编辑特定键的值一样简单,我可能会这样做:

  1. 在临时缓冲区中显示该字段的内容(与键对应的值),以进行编辑。

  2. 绑定一个键(例如,C-c C-c)将编辑后的值保存回文件。

例如,我在Bookmark+中编辑书签标签时也是这样做的(还使用了另一个命令编辑所有书签字段)。编辑标签的命令是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's 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's 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)))))

最相关的部分是这些:
1. 定义一个命令来开始编辑和一个命令来结束编辑并保存更改。 2. 使用`bmkp-with-output-to-plain-temp-buffer`提供一个编辑缓冲区(本质上是`with-output-to-temp-buffer`,但该宏在某些Emacs版本中还添加了不需要的“帮助”模式内容)。 3. 将编辑缓冲区放置在一个简单的次要模式中,将`C-c C-c`绑定到保存并退出命令。 4. 用要编辑的文本填充编辑缓冲区。弹出到缓冲区进行编辑。 5. 在保存并退出命令(`bmkp-edit-tags-send`)中,更新原始数据,用编辑缓冲区的内容替换相关字段内容。保存更新后的数据。返回到原始缓冲区。

这很有趣,教会了我很多东西,但是我认为它涉及到一个略微不同的问题。如果我理解正确,在书签+中,“model”缓冲区(书签列表)仍然是供用户消费的;它将在“edit”缓冲区之前创建,并将超越它们。在我的情况下,“model”缓冲区应该在打开时不可见,在编辑期间被冻结,并在保存后被销毁。可以用特定的簿记方法实现所有这些,或者几乎可以实现(我认为无法防止杀死缓冲区),但如果可能的话,我更希望避免这种情况。 :) - mmr

0

您可以在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))

实际上,这也可以解释为一个更一般的问题。将文件加载到隐藏缓冲区中。使用另一个可见缓冲区编辑其转换后的内容。在保存可见缓冲区时,实际上将内容再次转换回原始格式并保存隐藏缓冲区。

我现在没有时间按照上述描述实现通用情况。下面的代码大致涵盖了您的特殊情况。(请注意,这只是一个快速的演示目的的hack。)

(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)
      )))

@hyperstruct 我删除了 jka-compr 的注释,原因有两个:1. jka-compr 主要使用外部程序;2. 它钩入了内部 Emacs 函数的深层次。 - Tobias
可能更喜欢使用json.el而不是re-search-forward(请参见http://edward.oconnor.cx/2006/03/json.el)。它自emacs 23.1以来就已经包含了。 - Ehvince
@hyperstruct 我认为最自然的方法是使用 format-alist。我在答案开头包含了你的示例的解决方案。辅助缓冲区的簿记由 format 包处理。 - Tobias
很好。我之前不熟悉Emacs的格式处理方式;在重新实现我的模式后,我认为它是解决问题的最佳匹配。给未来的读者一个提示:在my-to-fn中,with-current-buffer调用至关重要。最初我没有使用它们,认为my-to-fn会访问缓冲区本地的my-headmy-tail变量;但事实并非如此。我还没有深入研究,但我假设my-to-fn会遇到格式转换过程中创建的临时辅助缓冲区。@Tobias 关于多缓冲区的想法,请参见我对Drew答案的评论。 - mmr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接