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
next 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).