jrblevin / markdown-mode

Emacs Markdown Mode
http://jblevins.org/projects/markdown-mode/
GNU General Public License v3.0
901 stars 164 forks source link

preserve indent-tabs-mode in markdown-edit-code-block #816

Open froh opened 1 year ago

froh commented 1 year ago

Expected Behavior

When I edit a nested code block using markdown-edit-code-block and I reindent it in the edit-indirect buffer, it's indentation should be preserved when I commit the edit back to the markdown document.

Actual Behavior

The indent-rigidly in markdown--edit-indirect-after-commit-function gets a region indented according to the default settings of indent-tabs-mode and tab-width, which may easily mismatch the markdown document's settings (in my case: markdown set to indent with spaces, default however is tabs). poor indent-rigidly can't but mess up at this time.

Steps to Reproduce

  1. create markdown document with a nested code block, nested deep enough
  2. edit it (C-c ')
  3. indent it (C-M-q)
  4. commit back (C-c C-c)

see the broken indentation. I have a simple solution proposal, so no cut&paste example.

Backtrace

-

Software Versions

froh commented 1 year ago

proposed fix: preserve the relevant buffer-local settings, see ===>> and <<===

(defun markdown-edit-code-block ()
  "Edit Markdown code block in an indirect buffer."
  (interactive)
  (save-excursion
    (if (fboundp 'edit-indirect-region)
        (let* ((bounds (markdown-get-enclosing-fenced-block-construct))
               (begin (and bounds (not (null (nth 0 bounds))) (goto-char (nth 0 bounds)) (line-beginning-position 2)))
               (end (and bounds(not (null (nth 1 bounds)))  (goto-char (nth 1 bounds)) (line-beginning-position 1))))
          (if (and begin end)
              (let* ((indentation (and (goto-char (nth 0 bounds)) (current-indentation)))
                     (original-indent-tabs-mode indent-tabs-mode) ;;; ====>>
                     (original-tab-width tab-width)  ;;; =====>>
                     (lang (markdown-code-block-lang))
                     (mode (or (and lang (markdown-get-lang-mode lang))
                               markdown-edit-code-block-default-mode))
                     (edit-indirect-guess-mode-function
                      (lambda (_parent-buffer _beg _end)
                        (funcall mode)))
                     (indirect-buf (edit-indirect-region begin end 'display-buffer)))
                ;; reset `sh-shell' when indirect buffer
                (when (and (not (member system-type '(ms-dos windows-nt)))
                           (member mode '(shell-script-mode sh-mode))
                           (member lang (append
                                         (mapcar (lambda (e) (symbol-name (car e)))
                                                 sh-ancestor-alist)
                                         '("csh" "rc" "sh"))))
                  (with-current-buffer indirect-buf
                    (sh-set-shell lang)))
                (when (> indentation 0) ;; un-indent in edit-indirect buffer
                  (with-current-buffer indirect-buf
                    (setq indent-tabs-mode original-indent-tabs-mode) ;;; <<====
                    (setq tab-width original-tab-width)  ;;; <<====
                    (indent-rigidly (point-min) (point-max) (- indentation)))))
            (user-error "Not inside a GFM or tilde fenced code block")))
      (when (y-or-n-p "Package edit-indirect needed to edit code blocks. Install it now? ")
        (progn (package-refresh-contents)
               (package-install 'edit-indirect)
               (markdown-edit-code-block))))))

alternative:

create some markdown--edit-indirect-after-create-function which copies select local variables. the hook then needs to access the edit-indirect internal edit-indirect--overlay

syohex commented 1 year ago

Could you show us how to reproduce in detail ? How are indentations broken ? Original code, broken indentation code etc.

froh commented 1 year ago

here is some badly indented markdown

# abcdefg
 * How does it work?
    * Design rationales

       * ...

    * Architecture patterns

       * monolith, embedding python, C++, Java in single image

    * Diagrams

       * plain JNA, cython, operation dispatch in C++/cython like data store

         ``` plantuml
         @startuml
         !theme cb_seq_Greens_9 from https://raw.githubusercontent.com/mweagle/C4-PlantUML-Themes/main/palettes
         !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
         !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

         LAYOUT_AS_SKETCH()

         System_Boundary(sb, "fastest possible:\nflow coordination in C++,\nFFI plain JNA, cython") {

            Container_Boundary(cpp, "C++ world", "C++", "operations in C++:\nhigh performance") {
               Component(A.cpp, "A.cc", "C++", "some operation A")
              Container_Boundary(flow, "flow coordination") {
             Component(cpp_flow, "flow", "C++", "flow core library")
               }
               Container_Boundary(store, "fast object store", "C++ library\npy, JVM bindings", "shared blackboard:\ninsanely fast data exchange") {
                  Component(cpp_store, "object store core library", "C++ .so")
                  ComponentDb(store_memory, "object store shared memory", "shared memory")
                  Rel_D(cpp_store, store_memory, "control and own")
               }
            }

            Container_Boundary(py, "Python world", "Python", "operations in Python:\nrapid development") {
            Component(B.py, "B.py", "Python", "some operation B")
              Component(python_store, "store API\nPython wrapper", "cython")
             Component(python_flow, "flow API\nPython wrapper", "cython")
            }

            Container_Boundary(java, "Java world", "Java", "operations in Java:\nmassive existing infrastructure") {
          Component(C.java, "C.java", "Java", "some operation C")
          Component(java_store, "store API\nJava wrapper", "JNA")
               Component(java_flow, "flow API\nJava wrapper", "JNA")
            }

            BiRel_U(A.cpp, cpp_flow, "invoke", "native, .so")

            BiRel_U(B.py, python_flow, "invoke", "python")
            BiRel_U(python_flow, cpp_flow, "invoke", "cython")

            BiRel_U(C.java, java_flow, "invoke", "Java")
            BiRel_U(java_flow, cpp_flow, "invoke", "JNA")

            BiRel_D(A.cpp, cpp_store, "invoke", "native .so")
            Rel_D(A.cpp, store_memory, "R/W", "native .so")

            Rel_D(B.py, python_store, "interact", "python")
            BiRel_D(python_store, cpp_store, "invoke", "cython")
            Rel_D(python_store, store_memory, "R/W", "cython")

            Rel_D(C.java, java_store, "interact", "Java")
            BiRel_D(java_store, cpp_store, "invoke", "JNA")
            Rel_D(java_store, store_memory, "R/W", "JNA")

         }

         SHOW_FLOATING_LEGEND()
         Lay_Distance(A.cpp, LEGEND(), 1)
         @enduml

         ```
    * some more

 * and on it goes

<!--  LocalWords:  diff
  Local Variables:
  markdown-asymmetric-header: t
  indent-tabs-mode: nil
  mode: gfm
  End:
 -->
  1. prepare test

    • system default ìndent-tabs-modeshall be the installation default,t`
    • package-install plantuml-mode, edit-indirec from melpa (for puml indent, edit indirect)
  2. double check indent-tabs-mode (shall be nil)

  3. edit code block C-c '

  4. in the edit-indirect buffer

    • check indent-tabs-mode (should be t, the system default)
    • the mode should be plantuml-mode .
    • go to opening curly brace, reindent C-M-q
  5. search for tabs C-s C-q i

  6. save or commit (C-x C-s / C-c C-c)

==> broken indent

syohex commented 1 year ago

How about using edit-indirect-after-creation-hook and write your own customizations ? I think it is impossible to support all modes to preserve such variables because some modes use their own offset level and tab variables instead of indent-tabs-mode, tab-width. And I'm not sure everyone expects the behavior you suggest.

froh commented 1 year ago

I see the point of "which variables to move over"?. And I can use the hook, of course.

for the time being would you advise to use this stanza to capture the values from the original buffer?

(with-current-buffer (overlay-buffer edit-indirect--overlay) ;;; this feels like hijacking an internal variable
      (save-excursion
         ...))

In the description of the hooks it was unspecific which buffer you're in when each hook is invoked and how to access the "other" buffer.