Emacs中的模型/视图编辑

时间:2014-10-21 14:10:05

标签: json model-view-controller emacs elisp

我有一些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"保存文件会导致foo.json仅更新description字段:

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

最佳策略是什么?我目前的尝试是这样的:

  1. 使用find-file
  2. 打开JSON文件
  3. 从point-min到point-max
  4. 创建一个不可见的叠加层
  5. 解析json
  6. 在point-min插入description属性的值,创建"视图"
  7. 添加本地写入文件挂钩和保存后挂钩
  8. local-write-file钩子杀死"视图",更新叠加层中的json,并保存文件。 after-save挂钩重新创建"视图"所以用户可以继续编辑。

    这是冗长而脆弱的。是否有更好的方法来处理屏幕表示应与磁盘表示不同的数据?

2 个答案:

答案 0 :(得分:0)

您的用例是否真的像您描述的场景一样简单(不是解决方案大纲,而是问题/用例)?

如果是这样,你的解决方案听起来有点矫枉过正。如果用例就像编辑特定键的值一样简单,我可能会这样做:

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

  2. 绑定密钥(例如C-c C-c)以将修改后的值保存回文件。

  3. 我在Bookmark+中执行此操作,例如用于编辑书签的标签(以及使用不同的命令编辑所有书签的字段)。编辑标签的命令是bmkp-edit-tags。命令(绑定到编辑缓冲区中的C-c C-c)为bmkp-edit-tags-send。在上下文中,代码为here。这是脱离背景:


    (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绑定到save-and-exit命令。

    4. 使用要编辑的文本填充编辑缓冲区。弹出缓冲区进行编辑。

    5. 在save-and-exit命令(bmkp-edit-tags-send)中,更新原始数据,用编辑缓冲区内容替换相关的字段内容。保存更新的数据。返回原始缓冲区。

答案 1 :(得分: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))

实际上,人们也可以将此解释为更普遍的问题。将文件加载到隐藏缓冲区中。使用另一个可见缓冲区来编辑其转换的内容。保存可见缓冲区实际上会将内容转换回原始格式并保存隐藏缓冲区。

我现在没时间实施上述一般情况。 以下代码大致涵盖了您的特殊情况。 (注意,这只是一个快速入侵,仅用于演示目的。)

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