emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
From: Mehmet Tekman <mtekman89@gmail.com>
To: emacs-orgmode@gnu.org
Subject: [ANN] lisp/ob-tangle-sync.el
Date: Wed, 26 Apr 2023 16:48:12 +0200	[thread overview]
Message-ID: <CAHHeYzJ6koLOr9=K82bjGX3fo6RHRJcvgdhJ6Ym08uPavuXnXQ@mail.gmail.com> (raw)

[-- Attachment #1: Type: text/plain, Size: 2423 bytes --]

Dear fellow org-users,

I would like to contribute some a new library into org-mode, which
performs automatic synchronization between tangled files and their
org-mode source blocks via a global minor mode
`org-babel-tangle-sync-mode' which uses the after-save-hook.

Full disclosure:

I created a MELPA package with similar functionality 3 years ago, but
it did not use the existing org libraries much and needlessly
reinvented many wheels:

  https://gitlab.com/mtekman/org-tanglesync.el

This is a complete (and very concise) rewrite that uses the org
framework and that I wish to incorporate into org-mode itself. My
changes to the org-mode main branch can be found here:

  https://gitlab.com/mtekman/org-mode (ob-tangle-sync branch)


A toy example of what this does would be as follows:

- Source =emacs_conf= org-mode file

#+begin_src bash :comments yes :tangle ~/.bashrc
export PATH=$PATH:/opt/bin
#+end_src

- Tangled =~/.bashrc= file

#+begin_src bash
# [[file:repos/_mtekman/emacs_conf.org::*bashrc][bashrc:1]]
export PATH=$PATH:/opt/bin
#+end_src

By activating =org-babel-tangle-sync-mode=, I can edit either of those
buffers and every time that I save it would automatically update the
other.  If I add the header argument =:tangle-sync <action>= then I
can specify an action of:
- skip :: do nothing, just save the buffer, even if the sync mode is active
- pull :: only pull changes from =~/.bashrc= into the Emacs_conf org-mode file
- export :: only export changes from Emacs_conf to =~/.bashrc=, even if called
            from =~/.bashrc=
- both :: (default) synchronize freely between tangled file and source block
          (the is also the nil value)

By default, the mode acts on any org-mode source block with =:tangle=
headers and any tangled file with file comments. This can be changed
by setting a list of "sync files" via =org-babel-tangle-sync-files=
which only acts if the source block exists in a file given in that
custom variable.


I'm currently trying to write tests for the library, but I would also
greatly welcome any feedback and comments on the currently written
code (attached as a diff, and also available in the above org-mode
repo)

As this is also my first time submitting directly to GNU Emacs, I, um,
might also need some hand-holding. I think I've followed the main
org-contribute guidelines, but I've likely missed or glossed over a
step or two.

Cheerful regards,

Mehmet

[-- Attachment #2: lisp_ob-tangle-sync.el --]
[-- Type: text/x-emacs-lisp, Size: 7952 bytes --]

diff --git a/lisp/ob-tangle-sync.el b/lisp/ob-tangle-sync.el
new file mode 100644
index 000000000..61c23f647
--- /dev/null
+++ b/lisp/ob-tangle-sync.el
@@ -0,0 +1,172 @@
+;;; ob-tangle-sync.el --- Synchronize Source Code and Org Files -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2009-2023 Free Software Foundation, Inc.
+
+;; Author: Mehmet Tekman
+;; Keywords: literate programming, reproducible research
+;; URL: https://orgmode.org
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Synchronize the code between source blocks and raw source-code files.
+
+;;; Code:
+
+(require 'org-macs)
+(org-assert-version)
+
+(require 'ol)
+(require 'org)
+(require 'org-element)
+(require 'ob-core)
+
+(defgroup org-babel-tangle-sync nil
+  "Options for synchronizing source code and code blocks."
+  :tag "Org Babel Tangle sync"
+  :group 'org-babel-tangle)
+
+;;;###autoload
+(define-minor-mode org-babel-tangle-sync-mode
+  "Global minor mode that synchronizes tangled files after every save."
+  :global t
+  :interactive t
+  :lighter " o-ts"
+  (if org-babel-tangle-sync-mode
+      (add-hook 'after-save-hook 'org-babel-tangle-sync-synchronize nil t)
+    (remove-hook 'after-save-hook 'org-babel-tangle-sync-synchronize t)))
+
+(defcustom org-babel-tangle-sync-files nil
+  "A list of `org-mode' files.
+When `org-babel-tangle-sync-mode' is enabled only files listed
+here are subject to the org-babel-tangle-sync treatment.  If nil,
+then all org files with tangle headers are considered."
+  :group 'org-babel-tangle-sync
+  :type 'list
+  :package-version '(Org . "9.6.5")
+  :set (lambda (_var val) (mapcar #'(lambda (x) (expand-file-name x)) val)))
+
+
+(defun org-babel-tangle-sync--babel-tangle-jump (link block-name)
+  "Jump from a tangled file to the Org file without returning anything.
+The location of the code block in the Org file is given by a
+combination of the LINK filename and header, followed by the
+BLOCK-NAME Org mode source block number.  The code is borrowed
+heavily from `org-babel-tangle-jump-to-org'"
+  ;; Go to the beginning of the relative block in Org file.
+  ;; Explicitly allow fuzzy search even if user customized
+  ;; otherwise.
+  (let (org-link-search-must-match-exact-headline)
+    (org-link-open-from-string link))
+  ;;(setq target-buffer (current-buffer))
+  (if (string-match "[^ \t\n\r]:\\([[:digit:]]+\\)" block-name)
+      (let ((n (string-to-number (match-string 1 block-name))))
+	(if (org-before-first-heading-p) (goto-char (point-min))
+	  (org-back-to-heading t))
+	;; Do not skip the first block if it begins at point min.
+	(cond ((or (org-at-heading-p)
+		   (not (eq (org-element-type (org-element-at-point))
+			    'src-block)))
+	       (org-babel-next-src-block n))
+	      ((= n 1))
+	      (t (org-babel-next-src-block (1- n)))))
+    (org-babel-goto-named-src-block block-name))
+  (goto-char (org-babel-where-is-src-block-head))
+  (forward-line 1))
+
+;;;###autoload
+(defun org-babel-tangle-sync-synchronize ()
+  "Synchronize a tangled code block to its source-specific file, or vice versa.
+If the cursor is either within the source file or in destination
+tangled file, perform a desired tangling action.  The tangling
+action by default is to detangle the tangled files' changes back
+to its source block, or to tangle the source block to its tangled
+file.  Actions are one of `skip' (no action), `pull' (detangle
+only), `export' (tangle only), and `both' (default, synchronize
+in both directions).  All `org-mode' source blocks and all tangled
+files with comments are considered valid targets, unless
+specified otherwise by `org-babel-tangle-sync-files'."
+  (interactive)
+  (let* ((link (save-excursion
+                 (progn (re-search-backward org-link-bracket-re nil t)
+		        (match-string-no-properties 0))))
+         (block-name (match-string 2))
+         (orgfile-p (string= major-mode "org-mode"))
+         (tangled-file-p (and link (not orgfile-p))))
+
+    ;; Tangled File → Source Block
+    (if tangled-file-p
+        ;; Examine the block: Get the source file and the desired tangle-sync action
+        (let* ((parsed-link (with-temp-buffer
+	                      (let ((org-inhibit-startup nil))
+	                        (insert link)
+	                        (org-mode)
+	                        (goto-char (point-min))
+	                        (org-element-link-parser))))
+               (source-file (expand-file-name
+                             (org-element-property :path parsed-link)))
+               (sync-action (save-window-excursion
+                              (progn
+                                (org-babel-tangle-sync--babel-tangle-jump link block-name)
+                                (alist-get :tangle-sync
+                                           (nth 2 (org-babel-get-src-block-info
+                                                   'no-eval)))))))
+          ;; De-tangle file back to source block if:
+          ;; - member of sync file list (or list is empty)
+          ;; - source file tangle-sync action isn't "skip" or "export",
+          (if (or (null org-babel-tangle-sync-files)
+                  (member source-file org-babel-tangle-sync-files))
+              (cond ((string= sync-action "skip") nil)
+                    ((string= sync-action "export")
+                     (save-window-excursion
+                       (progn (org-babel-tangle-sync--babel-tangle-jump link block-name)
+                              (let ((current-prefix-arg '(16)))
+                                (call-interactively 'org-babel-tangle))
+                              (message "Exported from %s" source-file))))
+                    (t
+                     (save-window-excursion
+                       (org-babel-detangle)
+                       (message "Synced to %s" source-file))))))
+
+      ;; Source Block → Tangled File (or Source Block ← Tangled File (via "pull"))
+      (when orgfile-p
+        ;; Tangle action of Source file on Block if:
+        ;; - member of sync file list (or list is empty)
+        ;; Actions
+        ;; - pull (Source Block ← File)
+        ;; - skip (nothing)
+        ;; - export, both, nil (Source Block → File)
+        (if (or (null org-babel-tangle-sync-files)
+                (member buffer-file-name org-babel-tangle-sync-files))
+
+            (let* ((src-headers (nth 2 (org-babel-get-src-block-info 'no-eval)))
+                   (tangle-file (cdr (assq :tangle src-headers)))
+                   (tangle-action (alist-get :tangle-sync src-headers)))
+              (when tangle-file
+                (cond ((string= tangle-action "pull") (save-excursion
+                                                        (org-babel-detangle tangle-file)))
+                      ((string= tangle-action "skip") nil)
+                      (t (let ((current-prefix-arg '(16)))
+                           (call-interactively 'org-babel-tangle)
+                           ;; Revert to see changes, then re-enable the mode
+                           (with-current-buffer (get-file-buffer tangle-file)
+                             (revert-buffer)
+                             (org-babel-tangle-sync-mode t))))))))))))
+
+(provide 'ob-tangle-sync)
+
+;;; ob-tangle-sync.el ends here

             reply	other threads:[~2023-04-26 14:49 UTC|newest]

Thread overview: 77+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-04-26 14:48 Mehmet Tekman [this message]
2023-04-26 16:43 ` [ANN] lisp/ob-tangle-sync.el John Wiegley
2023-04-26 18:43   ` Mehmet Tekman
2023-04-27  2:55 ` Ruijie Yu via General discussions about Org-mode.
2023-04-27  6:27   ` Mehmet Tekman
2023-04-28 10:57     ` Ruijie Yu via General discussions about Org-mode.
2023-04-28 11:28       ` Mehmet Tekman
2023-05-02 20:43         ` Mehmet Tekman
2023-05-03  2:31           ` Ruijie Yu via General discussions about Org-mode.
2023-05-03  7:53             ` Mehmet Tekman
2023-05-03  8:34               ` Mehmet Tekman
2023-05-03  8:44                 ` Ihor Radchenko
2023-05-03 11:43           ` Ihor Radchenko
2023-05-03 13:54             ` Mehmet Tekman
2023-05-03 18:06               ` Ihor Radchenko
2023-05-03 15:05             ` Mehmet Tekman
2023-05-03 15:21               ` Ihor Radchenko
     [not found]                 ` <87lei577g4.fsf@gmail.com>
     [not found]                   ` <87lei5v1fg.fsf@localhost>
     [not found]                     ` <87fs8duyae.fsf@localhost>
2023-05-09 14:03                       ` Mehmet Tekman
2023-05-10  9:46                         ` Ihor Radchenko
2023-05-10 11:06                           ` mtekman89
2023-05-10 11:32                             ` Ihor Radchenko
2023-05-10 16:20                               ` Mehmet Tekman
2023-05-12 12:33                                 ` Ihor Radchenko
2023-05-16 12:49                                   ` Mehmet Tekman
2023-05-16 18:57                                     ` Ihor Radchenko
2023-05-17 13:45                                       ` Mehmet Tekman
2023-05-18 10:30                                         ` Ihor Radchenko
2023-05-19  7:10                                           ` Mehmet Tekman
2023-07-15 12:38                                             ` Ihor Radchenko
2023-07-16  9:42                                               ` Mehmet Tekman
2023-07-17 11:29                                                 ` Mehmet Tekman
2023-07-18  8:47                                                   ` Ihor Radchenko
2023-07-21  8:48                                                     ` Mehmet Tekman
2023-07-22  8:02                                                       ` Ihor Radchenko
2023-07-25 11:19                                                         ` Mehmet Tekman
2023-07-25 16:19                                                           ` Ihor Radchenko
2023-07-31 13:41                                                             ` Mehmet Tekman
2023-07-31 16:38                                                               ` Ihor Radchenko
2023-07-31 20:11                                                                 ` Mehmet Tekman
2023-08-01  7:54                                                                   ` Ihor Radchenko
2023-08-01  8:49                                                                     ` Mehmet Tekman
2023-08-01  9:30                                                                       ` Ihor Radchenko
2023-08-01 18:19                                                                         ` Bastien Guerry
2023-08-02  7:29                                                                           ` Ihor Radchenko
2023-08-02 14:46                                                                   ` Mehmet Tekman
2023-08-03  6:32                                                                     ` Mehmet Tekman
2023-08-03  7:35                                                                     ` Ihor Radchenko
2023-08-03  8:08                                                                       ` Mehmet Tekman
2023-08-03  8:16                                                                         ` Ihor Radchenko
     [not found]                                                                           ` <CAHHeYzL6Z5_gGbTUrNzKDh5swgCSQiYsSj3Cs0gFy_d=eXbSBA@mail.gmail.com>
     [not found]                                                                             ` <87o7jo1q2s.fsf@localhost>
2023-08-03  8:46                                                                               ` Mehmet Tekman
2023-08-04  7:41                                                                                 ` Mehmet Tekman
2023-08-04  8:09                                                                                   ` Ihor Radchenko
2023-08-04 13:14                                                                                     ` Mehmet Tekman
2023-08-04 16:34                                                                                       ` Mehmet Tekman
2023-08-06  9:07                                                                                         ` Ihor Radchenko
2023-08-08 19:41                                                                                           ` Mehmet Tekman
2023-08-08 19:51                                                                                             ` Ihor Radchenko
2023-08-08 20:04                                                                                               ` Mehmet Tekman
2023-08-09  8:04                                                                                                 ` Ihor Radchenko
2023-08-05  8:48                                                                                       ` Ihor Radchenko
2023-08-05 22:54                                                                                         ` Mehmet Tekman
2023-11-10  9:41                                                                                           ` Ihor Radchenko
2023-11-10  9:53                                                                                             ` Mehmet Tekman
2023-12-11 13:40                                                                                               ` Ihor Radchenko
2023-12-11 14:28                                                                                                 ` Mehmet Tekman
2024-04-29  5:16                                                                                                   ` João Pedro
2024-04-29  7:43                                                                                                     ` Mehmet Tekman
2024-04-29 16:21                                                                                                       ` João Pedro
2024-05-05 16:47                                                                                                         ` Mehmet Tekman
2024-05-06  1:56                                                                                                           ` João Pedro
2024-05-06 12:53                                                                                                             ` Ihor Radchenko
2024-05-06 16:28                                                                                                             ` Mehmet Tekman
2024-05-06 12:45                                                                                                           ` Ihor Radchenko
2024-06-23 10:48                                                                                                     ` Ihor Radchenko
2024-07-23  8:47                                                                                                     ` Ihor Radchenko
2023-04-27 12:02 ` Ihor Radchenko
2023-04-27 13:01   ` Mehmet Tekman

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.orgmode.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAHHeYzJ6koLOr9=K82bjGX3fo6RHRJcvgdhJ6Ym08uPavuXnXQ@mail.gmail.com' \
    --to=mtekman89@gmail.com \
    --cc=emacs-orgmode@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).