├── README.md ├── images ├── figure1.gif ├── figure2.gif ├── figure3.gif └── figure4.gif └── org-include-inline.el /README.md: -------------------------------------------------------------------------------- 1 | # org-include-inline 2 | 3 | A minor mode for Org mode that displays #+INCLUDE directive contents inline within your Org buffers. 4 | 5 | ## Overview 6 | 7 | org-include-inline enhances the Org mode editing experience by showing included content directly beneath #+INCLUDE directives, without modifying the actual buffer content. This provides immediate visual feedback while maintaining the original document structure. 8 | 9 | ![Inline Include Demo](images/figure1.gif) 10 | 11 | ## Features 12 | 13 | - **Live Preview**: See included content directly in your buffer 14 | - **Multiple Include Types**: 15 | - Include entire files 16 | - Include specific line ranges 17 | - Include as source code blocks with syntax highlighting 18 | - Include as example blocks or other block types 19 | - Include specific headlines/subtrees using title, CUSTOM_ID, or ID 20 | - **Interactive Creation**: Easy-to-use commands for creating include directives 21 | - **Auto-refresh**: 22 | - Content updates automatically when source files change 23 | - Intelligent dependency tracking ensures all dependent org buffers are refreshed 24 | - Efficient cleanup on buffer/mode deactivation 25 | 26 | ## Installation 27 | 28 | You can install org-include-inline through your preferred package manager. For example, with `use-package`: 29 | 30 | ```elisp 31 | (use-package org-include-inline 32 | :hook (org-mode . org-include-inline-mode)) 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Basic Usage 38 | 39 | 1. Enable/disable the mode in any Org buffer: 40 | ```elisp 41 | M-x org-include-inline-mode 42 | ``` 43 | 44 | 2. Create include directives using any of these commands: 45 | - `M-x org-include-inline-insert-file` - Include an entire file 46 | - `M-x org-include-inline-insert-from-lines` - Include specific lines from a file 47 | - `M-x org-include-inline-insert-as-block` - Include file as a block (src, example, etc.) 48 | - `M-x org-include-inline-insert-named-block` - Include a named block from an Org file 49 | - `M-x org-include-inline-insert-headline` - Include a headline/subtree from an Org file 50 | - `M-x org-include-inline-insert-id` - Include an entry with ID from an Org file 51 | 52 | When using `org-include-inline-insert-named-block`, you can: 53 | 1. Select an Org file containing named blocks 54 | 2. Choose from a list of available named blocks (showing block type and language) 55 | 3. The block will be included with its original type and properties 56 | 57 | When using `org-include-inline-insert-headline`, you can: 58 | 1. Select an Org file containing headlines 59 | 2. Choose from a list of available headlines (showing level and CUSTOM_ID if available) 60 | 3. The headline will be included with all its content 61 | 62 | ![Interactive Headline Selection](images/figure3.gif) 63 | 64 | When using `org-include-inline-insert-id`, you can: 65 | 1. Select an Org file containing entries with IDs 66 | 2. Choose from a list of available entries (showing their headlines and IDs) 67 | 3. The entry will be included with all its content 68 | 69 | ![Interactive ID Selection](images/figure4.gif) 70 | 71 | 3. Auto-refresh after modified the source file: 72 | - `C-c '` go to the source file. When used on: 73 | - Regular includes: Opens the source file 74 | - ID includes: Opens the source file and jumps to the entry with that ID 75 | - Modify the source file. 76 | - Save the source file 77 | - The included content will be updated automatically. 78 | 79 | ![Interactive Line Selection](images/figure2.gif) 80 | 81 | ### Include Directive Examples 82 | 83 | ```org 84 | # Include an entire file 85 | #+INCLUDE: "path/to/file.org" 86 | 87 | # Include specific lines 88 | #+INCLUDE: "path/to/file.org" :lines "5-10" 89 | 90 | # Include as a source code block 91 | #+INCLUDE: "path/to/script.el" src emacs-lisp 92 | 93 | # Include as an example block 94 | #+INCLUDE: "path/to/config" example 95 | 96 | # Include specific lines as source code 97 | #+INCLUDE: "path/to/script.py" src python :lines "10-20" 98 | 99 | # Include with custom block type (must be quoted if starts with ':') 100 | #+INCLUDE: "path/to/file" ":custom-type" 101 | 102 | # Include a named block from an Org file 103 | #+INCLUDE: "path/to/file.org::block-name" 104 | 105 | # Include a specific headline/subtree (with all its content) 106 | #+INCLUDE: "path/to/file.org::*Target_Headline" 107 | 108 | # Include a subtree using its CUSTOM_ID 109 | #+INCLUDE: "path/to/file.org::#custom-id-value" 110 | 111 | # Include a subtree using its ID 112 | #+INCLUDE: "path/to/file.org::B5623BAE-013E-42C3-9956-3D367716B3CC" 113 | ``` 114 | 115 | ## Auto-refresh 116 | 117 | There are several ways to refresh the included content: 118 | 119 | 1. **Automatic Refresh**: 120 | - Content updates automatically when source files are saved 121 | - Refreshes when buffer is reverted or window configuration changes 122 | - Intelligent dependency tracking ensures all dependent org buffers are refreshed 123 | 124 | 2. **Manual Refresh**: 125 | - Use `C-c C-x C-v` (default key binding) to refresh manually 126 | - Click with mouse-1 (left click) on the included content 127 | - Call `M-x org-include-inline-refresh` or `M-x org-include-inline-refresh-buffer` 128 | 129 | 3. **Customization**: 130 | ```elisp 131 | ;; Customize the refresh key binding 132 | (setq org-include-inline-auto-refresh-key "C-c C-x C-v") 133 | ``` 134 | 135 | ## Commands 136 | 137 | - `org-include-inline-refresh-buffer` - Refresh all inline includes in the current buffer 138 | - `org-include-inline-insert-file` - Insert a directive to include an entire file 139 | - `org-include-inline-insert-from-lines` - Insert a directive to include specific lines 140 | - `org-include-inline-insert-as-block` - Insert a directive to include a file as a block 141 | - `org-include-inline-insert-named-block` - Insert a directive to include a named block from an Org file 142 | - `org-include-inline-insert-headline` - Insert a directive to include a headline/subtree from an Org file 143 | - `org-include-inline-insert-id` - Insert a directive to include an entry with ID from an Org file 144 | 145 | ## Customization 146 | 147 | ```elisp 148 | ;; Auto-enable in all Org buffers 149 | (setq org-include-inline-auto-enable-in-org-mode t) 150 | 151 | ;; Customize maximum lines to display 152 | (setq org-include-inline-max-lines-to-display 1000) 153 | 154 | ;; Customize the display face 155 | (set-face-attribute 'org-include-inline-face nil 156 | :background "black" 157 | :foreground "white") 158 | 159 | ;; Control export behavior 160 | (setq org-include-inline-export-behavior 'selective) ;; 'selective, 'ignore, or 'process 161 | 162 | ;; Control folding behavior 163 | (setq org-include-inline-respect-folding t) ;; nil to always show includes 164 | ``` 165 | 166 | ### Export Behavior 167 | 168 | The mode provides flexible control over how includes are handled during export: 169 | 170 | 1. **Global Export Behavior**: 171 | ```elisp 172 | ;; Choose one of: 173 | (setq org-include-inline-export-behavior 'selective) ;; default 174 | (setq org-include-inline-export-behavior 'ignore) ;; ignore all includes 175 | (setq org-include-inline-export-behavior 'process) ;; process all includes 176 | ``` 177 | 178 | 2. **Per-Include Control** (when `org-include-inline-export-behavior` is `'selective`): 179 | ```org 180 | #+INCLUDE: "file.org" ;; Will be exported (default) 181 | #+INCLUDE: "notes.org" :export: no ;; Will not be exported 182 | ``` 183 | 184 | 3. **Export Modes**: 185 | - `selective` (default): Process includes normally, except those marked with `:export: no` 186 | - `ignore`: Completely ignore all includes during export 187 | - `process`: Process all includes normally (same as org default) 188 | 189 | ### Folding Behavior 190 | 191 | The mode can respect Org's outline folding: 192 | 193 | 1. **Global Folding Control**: 194 | ```elisp 195 | ;; Choose one: 196 | (setq org-include-inline-respect-folding t) ;; default, hide with parent heading 197 | (setq org-include-inline-respect-folding nil) ;; always show includes 198 | ``` 199 | 200 | 2. **Behavior**: 201 | - When `t`: Includes are hidden when their parent heading is folded 202 | - When `nil`: Includes remain visible regardless of heading state 203 | - Includes before any heading are always visible 204 | 205 | ## Comparison with org-transclusion 206 | 207 | While both org-include-inline and org-transclusion deal with including content from other files, they serve different purposes: 208 | 209 | - **Purpose**: 210 | - org-include-inline: Focuses on visualizing Org's native #+INCLUDE directives inline 211 | - org-transclusion: Provides a more general transclusion system for various content types 212 | 213 | - **Implementation**: 214 | - org-include-inline: Uses overlays to display content beneath #+INCLUDE lines 215 | - org-transclusion: Creates actual text content in the buffer that can be edited 216 | 217 | - **Use Cases**: 218 | - org-include-inline: Best for: 219 | - Working with existing #+INCLUDE directives 220 | - Quick preview of included content 221 | - Source code inclusion and documentation 222 | - org-transclusion: Better for: 223 | - Complex transclusion needs 224 | - Live editing of transcluded content 225 | - Advanced linking between documents 226 | 227 | - **Simplicity**: 228 | - org-include-inline: Lightweight, focused on one specific feature 229 | - org-transclusion: More feature-rich but with higher complexity 230 | 231 | Choose org-include-inline if you mainly work with #+INCLUDE directives and want a simple, visual way to see included content. Choose org-transclusion for more advanced document transclusion needs. 232 | 233 | ## Contributing 234 | 235 | Contributions are welcome! Feel free to: 236 | - Report issues 237 | - Suggest enhancements 238 | - Submit pull requests 239 | 240 | ## License 241 | 242 | This project is licensed under the GNU General Public License v3.0. 243 | 244 | ## Author 245 | 246 | Yibie (gunshotbox@gmail.com) 247 | 248 | 249 | ``` -------------------------------------------------------------------------------- /images/figure1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-include-inline/68cb51f6621510c1e380a4c24d1fc21903cdc353/images/figure1.gif -------------------------------------------------------------------------------- /images/figure2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-include-inline/68cb51f6621510c1e380a4c24d1fc21903cdc353/images/figure2.gif -------------------------------------------------------------------------------- /images/figure3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-include-inline/68cb51f6621510c1e380a4c24d1fc21903cdc353/images/figure3.gif -------------------------------------------------------------------------------- /images/figure4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yibie/org-include-inline/68cb51f6621510c1e380a4c24d1fc21903cdc353/images/figure4.gif -------------------------------------------------------------------------------- /org-include-inline.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding: t; -*- 2 | 3 | ;;; org-include-inline.el --- Display #+INCLUDE contents inline in Org mode buffers. 4 | 5 | ;;; Commentary: 6 | ;; This package provides a minor mode for Org mode buffers that allows 7 | ;; for the inline display of content specified by #+INCLUDE directives. 8 | ;; It uses overlays to show the included content directly below the 9 | ;; #+INCLUDE line, without modifying the actual buffer content. 10 | ;; It also provides interactive functions to create #+INCLUDE directives. 11 | 12 | ;; AUTHOR: Yibie (gunshotbox@gmail.com) 13 | ;; VERSION: 0.1.0 14 | ;; DATE: 2025-05-08 15 | ;; KEYWORDS: org-mode, include, inline, overlay 16 | 17 | ;;; Code: 18 | 19 | (require 'org) 20 | (require 'org-element) 21 | (require 'org-src) 22 | (require 'org-id) 23 | 24 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 25 | ;;; Customization 26 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 27 | 28 | (defface org-include-inline-face 29 | '((((class color) (background light)) :background "black" :foreground "white" :extend t) 30 | (((class color) (background dark)) :background "black" :foreground "white" :extend t) 31 | (t :inherit default)) 32 | "Face used for displaying inline included content." 33 | :group 'org-include-inline) 34 | 35 | (defcustom org-include-inline-max-lines-to-display 500 36 | "Maximum number of lines to display from an included file. 37 | If an included section is larger, it will be truncated." 38 | :type 'integer 39 | :group 'org-include-inline) 40 | 41 | (defcustom org-include-inline-auto-enable-in-org-mode 'smart 42 | "How to automatically enable org-include-inline-mode in Org buffers. 43 | Possible values: 44 | - nil: Never auto-enable, manual activation only 45 | - t: Always auto-enable in all Org mode buffers 46 | - smart: Only auto-enable in buffers that contain #+INCLUDE directives (recommended)" 47 | :type '(choice 48 | (const :tag "Never auto-enable" nil) 49 | (const :tag "Always auto-enable" t) 50 | (const :tag "Smart auto-enable (only when #+INCLUDE found)" smart)) 51 | :group 'org-include-inline) 52 | 53 | (defcustom org-include-inline-auto-save t 54 | "Whether to automatically save buffers after refreshing includes. 55 | When non-nil, buffers will be saved after their includes are refreshed 56 | to ensure the dependency relationships are persisted." 57 | :type 'boolean 58 | :group 'org-include-inline) 59 | 60 | (defcustom org-include-inline-auto-refresh-key "C-c C-x C-v" 61 | "Key binding for auto-refresh command in org-include-inline-mode." 62 | :type 'string 63 | :group 'org-include-inline) 64 | 65 | (defcustom org-include-inline-additional-id-formats nil 66 | "Additional regular expressions to recognize org IDs. 67 | Each entry should be a regular expression string that matches your custom ID format. 68 | For example: '(\"\\`[A-Z]+[0-9]+\\'\") to match IDs like 'ABC123'." 69 | :type '(repeat string) 70 | :group 'org-include-inline) 71 | 72 | (defcustom org-include-inline-respect-folding t 73 | "Whether to hide includes when their parent heading is folded." 74 | :type 'boolean 75 | :group 'org-include-inline) 76 | 77 | (defcustom org-include-inline-export-behavior 'selective 78 | "How to handle includes during export. 79 | Possible values: 80 | - selective: process includes normally, except those marked with :export: no 81 | - ignore: completely ignore all includes 82 | - process: process all includes normally (same as org default)" 83 | :type '(choice 84 | (const :tag "Selective processing (default)" selective) 85 | (const :tag "Ignore all includes" ignore) 86 | (const :tag "Process all includes" process)) 87 | :group 'org-include-inline) 88 | 89 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 90 | ;;; Internal Variables and Data Structures 91 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 92 | 93 | (defvar-local org-include-inline--overlays nil 94 | "Buffer-local list of overlays created by org-include-inline-mode.") 95 | 96 | (defvar org-include-inline--source-buffers nil 97 | "Alist of (source-file . org-buffers) pairs. 98 | Each pair maps a source file to a list of org buffers that include it.") 99 | 100 | (defvar org-include-inline--storage-file 101 | (expand-file-name "org-include-inline-storage.el" user-emacs-directory) 102 | "File to store org-include-inline associations.") 103 | 104 | (defvar org-include-inline--block-types 105 | '("src" "example" "export" ":custom") 106 | "List of common block types for #+INCLUDE directives.") 107 | 108 | (defvar org-include-inline--common-languages 109 | '("emacs-lisp" "python" "sh" "C" "C++" "java" "javascript" "css" "html" "org" "latex") 110 | "List of common programming languages for src blocks.") 111 | 112 | (defvar-local org-include-inline--refreshing nil 113 | "Flag to prevent recursive refresh.") 114 | 115 | (defvar org-include-inline--last-refresh-time (make-hash-table :test 'equal) 116 | "Hash table to store last refresh time for each buffer.") 117 | 118 | (defvar-local org-include-inline--original-includes nil 119 | "Alist to store original includes during export process. 120 | Each element is a cons cell (keyword . value) where keyword is the org-element 121 | and value is the original include directive value.") 122 | 123 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 124 | ;;; Helper Functions 125 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 126 | 127 | (defun org-include-inline--save-associations () 128 | "Save current associations to storage file." 129 | (let ((data-to-save 130 | (mapcar (lambda (entry) 131 | (cons (car entry) ; source-file path (string) 132 | ;; Convert buffer objects to their file paths 133 | (mapcar #'buffer-file-name 134 | (cl-remove-if-not 135 | (lambda (b) 136 | (and (bufferp b) 137 | (buffer-live-p b) 138 | (buffer-file-name b))) 139 | (cdr entry))))) 140 | org-include-inline--source-buffers))) 141 | ;; Filter out entries where the list of buffer files became empty 142 | (setq data-to-save (cl-remove-if (lambda (entry) (null (cdr entry))) data-to-save)) 143 | (when data-to-save 144 | (let ((inhibit-message t)) ;; 抑制保存消息 145 | (with-temp-file org-include-inline--storage-file 146 | (insert ";; -*- mode: emacs-lisp; lexical-binding: t; -*-\n") 147 | (insert ";; Org Include Inline Storage\n") 148 | (insert ";; This file is auto-generated. Do not edit by hand.\n\n") 149 | (insert "(setq org-include-inline--source-buffers\n") 150 | (insert (format " '%S)\n" data-to-save))))))) 151 | 152 | (defun org-include-inline--load-associations () 153 | "Load associations from storage file." 154 | (when (file-exists-p org-include-inline--storage-file) 155 | (load org-include-inline--storage-file) 156 | ;; 将文件路径转换为 buffer 157 | (setq org-include-inline--source-buffers 158 | (mapcar (lambda (entry) 159 | (cons (car entry) ; source file path 160 | ;; Convert file paths to buffers, only if they exist 161 | (mapcar (lambda (file) 162 | (or (find-buffer-visiting file) 163 | (when (file-exists-p file) 164 | (find-file-noselect file)))) 165 | (cdr entry)))) 166 | org-include-inline--source-buffers)) 167 | ;; Remove any entries where all buffers failed to load 168 | (setq org-include-inline--source-buffers 169 | (cl-remove-if (lambda (entry) (null (cdr entry))) 170 | org-include-inline--source-buffers)))) 171 | 172 | (defun org-include-inline--parse-include-directive (line-text) 173 | "Parse an #+INCLUDE line and return a plist with info. 174 | The line should be in format: 175 | #+INCLUDE: \"FILE\" [BLOCK-TYPE LANGUAGE] [:lines \"N-M\"] or 176 | #+INCLUDE: \"FILE::*HEADLINE\" or 177 | #+INCLUDE: \"FILE::BLOCK-NAME\" or 178 | #+INCLUDE: \"FILE::ID\" 179 | 180 | BLOCK-TYPE can be 'src', 'example', etc. 181 | LANGUAGE is the source code language when BLOCK-TYPE is 'src' or 'export'." 182 | (let ((case-fold-search t) 183 | file type lines-spec headline-spec block-type language named-block id-spec 184 | line-after-file remainder-after-headline) 185 | 186 | ;; 1. Match #+INCLUDE: "FILE" part and get the rest of the line 187 | (when (string-match "^[ \t]*#\\+INCLUDE:[ \t]*\"\\([^\"]+\\)\"\\(.*\\)" line-text) 188 | (let ((file-spec (match-string 1 line-text))) 189 | (setq line-after-file (match-string 2 line-text)) 190 | 191 | ;; Check if this is a file::target specification 192 | (if (string-match "\\(.*?\\)::+\\(.*\\)\\'" file-spec) 193 | (let ((main-file (match-string 1 file-spec)) 194 | (spec (match-string 2 file-spec))) 195 | (setq file main-file) ; Set the actual file path 196 | (cond 197 | ;; ID format 198 | ((org-include-inline--is-valid-org-id-p spec) 199 | (setq type :id 200 | id-spec spec)) 201 | ;; Headline format (now handles timestamps) 202 | ((or (string-prefix-p "*" spec) 203 | (string-prefix-p "[" spec) ; Handle timestamp at start 204 | (string-match "^\\[.*\\]" spec)) ; Handle timestamp in middle 205 | (setq headline-spec spec 206 | type :headline)) 207 | ;; Named block 208 | (t 209 | (setq named-block spec 210 | type :named-block)))) 211 | ;; No :: in the file spec, treat as regular file 212 | (progn 213 | (setq file file-spec 214 | type :lines))) ; Default to :lines type for regular files 215 | 216 | (when file 217 | (setq file (expand-file-name file (if buffer-file-name 218 | (file-name-directory buffer-file-name) 219 | default-directory))) 220 | ;; Initially, the remainder for further parsing is everything after the file part 221 | (setq remainder-after-headline line-after-file) 222 | 223 | ;; 2. Try to parse block type and language 224 | (when (string-match "^[ \t]+\\([^ \t\n]+\\)\\(?:[ \t]+\\([^ \t\n]+\\)\\)?[ \t]*\\(.*\\)" line-after-file) 225 | (let ((first-param (match-string 1 line-after-file)) 226 | (second-param (match-string 2 line-after-file))) 227 | ;; Check if first parameter is a block type 228 | (unless (string-prefix-p ":" first-param) 229 | (setq block-type first-param 230 | language second-param 231 | remainder-after-headline (match-string 3 line-after-file))))) 232 | 233 | ;; 3. Try to parse ::headline-spec from remainder 234 | (when (and remainder-after-headline 235 | (string-match "^[ \t]*::\\(.+?\\)[ \t]*\\(.*\\)" remainder-after-headline)) 236 | (setq headline-spec (string-trim (match-string 1 remainder-after-headline))) 237 | (setq remainder-after-headline (match-string 2 remainder-after-headline))) 238 | 239 | ;; 4. Try to parse :lines from remainder 240 | (when (and remainder-after-headline 241 | (string-match "^[ \t]*:lines[ \t]+\"\\([0-9]+-\?[0-9]*\\)\"" remainder-after-headline)) 242 | (setq lines-spec (match-string 1 remainder-after-headline))) 243 | 244 | ;; Set default lines-spec for :lines type 245 | (when (eq type :lines) 246 | (setq lines-spec (or lines-spec "1-"))) 247 | 248 | `(:file ,file 249 | :type ,type 250 | :lines-spec ,lines-spec 251 | :headline-spec ,headline-spec 252 | :block-type ,block-type 253 | :language ,language 254 | :named-block ,named-block 255 | :id-spec ,id-spec 256 | :original-line ,line-text)))))) 257 | 258 | (defun org-include-inline--fetch-file-lines (file lines-spec &optional block-type language) 259 | "Fetch specific lines from FILE based on LINES-SPEC (e.g., \"1-10\", \"5\"). 260 | If BLOCK-TYPE is provided, wrap content in appropriate block. 261 | LANGUAGE is used when BLOCK-TYPE is 'src' or 'export'. 262 | Note that for 'example', 'export', or 'src' blocks, content is escaped." 263 | (unless (file-readable-p file) 264 | (message "Error: File not readable: %s" file) 265 | (format "Error: File not readable: %s" file)) 266 | 267 | (let ((start-line 1) 268 | (end-line most-positive-fixnum) 269 | (content "")) 270 | (when lines-spec ; lines-spec could be nil if we default to full file but user provides no :lines 271 | (cond 272 | ((string-match "\\`\\([0-9]+\\)-\\([0-9]+\\)\\'" lines-spec) ; Match full spec "S-E" 273 | (setq start-line (string-to-number (match-string 1 lines-spec))) 274 | (setq end-line (string-to-number (match-string 2 lines-spec)))) 275 | ((string-match "\\`\\([0-9]+\\)-\\'" lines-spec) ; Match "S-" 276 | (setq start-line (string-to-number (match-string 1 lines-spec)))) 277 | ((string-match "\\`\\([0-9]+\\)\\'" lines-spec) ; Match "S" (single line) 278 | (setq start-line (string-to-number (match-string 1 lines-spec))) 279 | (setq end-line start-line)) 280 | ((string-equal lines-spec "1-") ; Explicit full file through "1-" 281 | (setq start-line 1 end-line most-positive-fixnum)) 282 | (t (message "Warning: Invalid lines spec: \"%s\" for file %s. Defaulting to all lines." lines-spec file) 283 | (setq start-line 1 end-line most-positive-fixnum)))) 284 | 285 | (with-temp-buffer 286 | (insert-file-contents file) 287 | (goto-char (point-min)) 288 | (let ((current-line 1) 289 | (lines-count 0) 290 | (result-lines '())) 291 | (while (and (not (eobp)) (< current-line start-line)) 292 | (forward-line 1) 293 | (setq current-line (1+ current-line))) 294 | (while (and (not (eobp)) 295 | (<= current-line end-line) 296 | (< lines-count org-include-inline-max-lines-to-display)) 297 | (push (buffer-substring-no-properties (line-beginning-position) (line-end-position)) 298 | result-lines) 299 | (forward-line 1) 300 | (setq current-line (1+ current-line) 301 | lines-count (1+ lines-count))) 302 | (setq content (mapconcat #'identity (nreverse result-lines) "\n")) 303 | (when (and (not (eobp)) (<= current-line end-line)) ; Means it was truncated 304 | (setq content (concat content 305 | (format "\n... (truncated at %d lines)" 306 | org-include-inline-max-lines-to-display)))))) 307 | 308 | ;; Wrap content in appropriate block if needed 309 | (when (and block-type (not (string-empty-p content))) 310 | (let* ((block-name (if (string-prefix-p "\"" block-type) 311 | (substring block-type 1 -1) 312 | block-type)) 313 | (needs-escape (member block-name '("src" "example" "export"))) 314 | (escaped-content (if needs-escape 315 | (org-escape-code-in-string content) 316 | content))) 317 | (setq content 318 | (cond 319 | ((member block-name '("src" "export")) 320 | (format "#+begin_%s %s\n%s\n#+end_%s" 321 | block-name 322 | (or language "") 323 | escaped-content 324 | block-name)) 325 | (t 326 | (format "#+begin_%s\n%s\n#+end_%s" 327 | block-name 328 | escaped-content 329 | block-name)))))) 330 | content)) 331 | 332 | (defun org-include-inline--with-temp-org-buffer (file fn) 333 | "Create a temporary org buffer with FILE contents and execute FN. 334 | FN is a function that will be called with the temporary buffer as current. 335 | Ensures proper cleanup of the temporary buffer." 336 | (let ((temp-buffer (generate-new-buffer " *temp org-include-inline*"))) 337 | (unwind-protect 338 | (with-current-buffer temp-buffer 339 | (insert-file-contents file) 340 | (set-buffer-modified-p nil) 341 | (let ((delay-mode-hooks t)) 342 | (org-mode)) 343 | (prog1 (funcall fn) 344 | (set-buffer-modified-p nil))) 345 | (when (buffer-name temp-buffer) 346 | (kill-buffer temp-buffer))))) 347 | 348 | (defun org-include-inline--fetch-org-headline-content (file headline-spec &optional only-contents lines-spec) 349 | "Fetch content of a specific headline from an Org FILE. 350 | HEADLINE-SPEC 可以是 \"*Headline Text\" 或 \"#custom-id\"。 351 | If ONLY-CONTENTS is non-nil, return only the content (excluding property drawers, planning, etc.). 352 | LINES-SPEC is like \"1-10\", returning only a portion of the content." 353 | (unless (file-readable-p file) 354 | (message "Error: Org file not readable: %s" file) 355 | (format "Error: Org file not readable: %s" file)) 356 | (unless headline-spec 357 | (message "Error: No headline specification provided for file %s" file) 358 | (format "Error: No headline specified for %s" file)) 359 | 360 | (org-include-inline--with-temp-org-buffer 361 | file 362 | (lambda () 363 | (let* ((ast (org-element-parse-buffer 'greater-element)) 364 | (target-headline 365 | (org-element-map ast 'headline 366 | (lambda (headline) 367 | (let ((title (org-element-property :raw-value headline)) 368 | (custom-id (org-element-property :CUSTOM_ID headline))) 369 | (cond 370 | ((and (string-prefix-p "#" headline-spec) 371 | custom-id 372 | (string= custom-id (substring headline-spec 1))) 373 | headline) 374 | ((and (or (string-prefix-p "*" headline-spec) 375 | (string-prefix-p "[" headline-spec)) 376 | title 377 | ;; Clean up the headline spec and title for comparison 378 | (let* ((clean-spec (string-trim 379 | (replace-regexp-in-string 380 | "^\\*+\\s-*" "" headline-spec))) 381 | (clean-title (string-trim title))) 382 | ;; Use string-match for more flexible matching 383 | (or (string= clean-spec clean-title) 384 | (string-match (regexp-quote clean-spec) clean-title)))) 385 | headline)))) 386 | nil t))) 387 | (if (not target-headline) 388 | (format "Error: Headline/target '%s' not found in %s" headline-spec (file-name-nondirectory file)) 389 | (let* ((content-begin (org-element-property :contents-begin target-headline)) 390 | (content-end (org-element-property :contents-end target-headline)) 391 | (raw-content (if (and content-begin content-end) 392 | (buffer-substring-no-properties content-begin content-end) 393 | "")) 394 | (final-content raw-content)) 395 | (when only-contents 396 | (with-temp-buffer 397 | (insert raw-content) 398 | (let ((delay-mode-hooks t)) 399 | (org-mode)) 400 | (goto-char (point-min)) 401 | (when (re-search-forward "^:PROPERTIES:$" nil t) 402 | (let ((prop-end (and (re-search-forward "^:END:$" nil t) (point)))) 403 | (when prop-end (delete-region (point-min) prop-end)))) 404 | (goto-char (point-min)) 405 | (while (looking-at "^\s-*\(DEADLINE:\|SCHEDULED:\|CLOSED:\)") 406 | (forward-line 1)) 407 | (setq final-content (buffer-substring-no-properties (point) (point-max))))) 408 | (when (and lines-spec (not (string-empty-p final-content))) 409 | (let* ((lines (split-string final-content "\n")) 410 | (start 1) 411 | (end (length lines))) 412 | (cond 413 | ((string-match "\\`\\([0-9]+\\)-\\([0-9]+\\)\\'" lines-spec) 414 | (setq start (string-to-number (match-string 1 lines-spec))) 415 | (setq end (string-to-number (match-string 2 lines-spec)))) 416 | ((string-match "\\`\\([0-9]+\\)-\\'" lines-spec) 417 | (setq start (string-to-number (match-string 1 lines-spec)))) 418 | ((string-match "\\`-\\([0-9]+\\)\\'" lines-spec) 419 | (setq end (string-to-number (match-string 1 lines-spec)))) 420 | ((string-match "\\`\\([0-9]+\\)\\'" lines-spec) 421 | (setq start (string-to-number (match-string 1 lines-spec))) 422 | (setq end start))) 423 | (setq start (max 1 start)) 424 | (setq end (min (length lines) end)) 425 | (setq final-content (mapconcat #'identity (cl-subseq lines (1- start) end) "\n")))) 426 | (string-trim final-content))))))) 427 | 428 | (defun org-include-inline--fetch-named-block-content (file block-name) 429 | "Fetch content of a named block from FILE. 430 | BLOCK-NAME is the name of the block to fetch. 431 | Returns only the content of the block, without the #+NAME:, #+begin_src, and #+end_src lines." 432 | (unless (file-readable-p file) 433 | (message "Error: File not readable: %s" file) 434 | (format "Error: File not readable: %s" file)) 435 | 436 | (org-include-inline--with-temp-org-buffer 437 | file 438 | (lambda () 439 | (let* ((ast (org-element-parse-buffer)) 440 | (block (org-element-map ast '(src-block example-block) 441 | (lambda (element) 442 | (when (string= (org-element-property :name element) block-name) 443 | element)) 444 | nil t))) 445 | (if block 446 | (let* ((content (org-element-property :value block))) 447 | (if content 448 | (let ((trimmed (string-trim content))) 449 | trimmed) 450 | "")) ; Return empty string if no content 451 | (format "Error: Named block \"%s\" not found in %s" 452 | block-name (file-name-nondirectory file))))))) 453 | 454 | (defun org-include-inline--get-named-blocks (file) 455 | "Scan FILE for named blocks and return an alist of (name . properties). 456 | Each property list contains :name, :type (src, example, etc), and :language (for src blocks)." 457 | (org-include-inline--with-temp-org-buffer 458 | file 459 | (lambda () 460 | (let ((blocks nil) 461 | (ast (org-element-parse-buffer))) 462 | ;; First pass: collect all NAME keywords 463 | (org-element-map ast '(src-block example-block) 464 | (lambda (element) 465 | (let ((name (org-element-property :name element))) 466 | (when name 467 | (push (list name 468 | :type (if (eq (org-element-type element) 'src-block) 469 | "src" 470 | "example") 471 | :language (org-element-property :language element)) 472 | blocks))))) 473 | (nreverse blocks))))) 474 | 475 | (defun org-include-inline--fetch-org-id-content (id &optional only-contents lines-spec) 476 | "Fetch content of an entry with ID. 477 | ID is the entry ID (UUID, internal format, or custom format). 478 | If ONLY-CONTENTS is non-nil, return only the contents (excluding properties). 479 | LINES-SPEC is like \"1-10\", returning only those lines." 480 | (require 'org-id) 481 | (if-let* ((marker (org-id-find id t))) 482 | (with-current-buffer (marker-buffer marker) 483 | (save-excursion 484 | (goto-char marker) 485 | (beginning-of-line) 486 | (let ((content 487 | (save-restriction 488 | (org-narrow-to-subtree) 489 | (let ((raw-content (buffer-substring-no-properties (point) (point-max)))) 490 | (with-temp-buffer 491 | (org-mode) 492 | (insert raw-content) 493 | (goto-char (point-min)) 494 | (forward-line 1) 495 | (while (looking-at org-drawer-regexp) 496 | (let ((drawer-end (save-excursion 497 | (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)))) 498 | (when drawer-end 499 | (delete-region (point) (progn (goto-char drawer-end) 500 | (forward-line 1) 501 | (point)))))) 502 | (goto-char (point-min)) 503 | (forward-line 1) 504 | (while (looking-at org-planning-line-re) 505 | (delete-region (point) (progn (forward-line 1) (point)))) 506 | (buffer-substring-no-properties (point-min) (point-max))))))) 507 | (when (and content lines-spec) 508 | (let* ((lines (split-string content "\n")) 509 | (start 1) 510 | (end (length lines))) 511 | (when (string-match "\\`\\([0-9]+\\)-\\([0-9]+\\)\\'" lines-spec) 512 | (setq start (string-to-number (match-string 1 lines-spec)) 513 | end (string-to-number (match-string 2 lines-spec))) 514 | (setq start (max 1 start) 515 | end (min (length lines) end)) 516 | (setq content (mapconcat #'identity 517 | (cl-subseq lines (1- start) end) 518 | "\n"))))) 519 | (let ((final-content (if content (string-trim content) ""))) 520 | (or (and final-content (not (string-empty-p final-content)) 521 | final-content) 522 | (format "Error: No content found for ID %s" id)))))) 523 | (format "Error: Entry with ID %s not found" id))) 524 | 525 | (defun org-include-inline--limit-content (string) 526 | "Extract `org-include-inline-max-lines-to-display' lines content from STRING. 527 | If the content has more lines than the limit, it will be truncated and a message 528 | will be added indicating the truncation." 529 | (let* ((parts (split-string string "\n" nil)) 530 | (total-lines (length parts)) 531 | (max-lines (min org-include-inline-max-lines-to-display total-lines))) 532 | (if (< max-lines total-lines) 533 | (concat 534 | (mapconcat #'identity (butlast parts (- total-lines max-lines)) "\n") 535 | (format "\n... (truncated at %d lines)" org-include-inline-max-lines-to-display)) 536 | string))) 537 | 538 | (defun org-include-inline--create-or-update-overlay (point content &optional buffer) 539 | "Create or update an overlay at POINT to display CONTENT in BUFFER. 540 | If BUFFER is nil, use current buffer. Ensures overlay stays attached to buffer." 541 | (let ((buf (or buffer (current-buffer)))) 542 | (if (and content (> (length content) 0)) 543 | (progn 544 | (condition-case e-make-overlay 545 | (with-current-buffer buf 546 | (let ((ov (make-overlay point point buf)) 547 | (parent-pos (save-excursion 548 | (condition-case nil 549 | (progn 550 | (org-back-to-heading t) 551 | (point)) 552 | (error nil))))) 553 | ;; Store overlay's parent heading position if exists 554 | (overlay-put ov 'org-include-inline t) 555 | (when parent-pos 556 | (overlay-put ov 'org-include-parent-heading parent-pos)) 557 | ;; Use before-string for better visibility control and apply line limit 558 | (overlay-put ov 'before-string 559 | (propertize (org-include-inline--limit-content content) 560 | 'face 'org-include-inline-face 561 | 'org-include-inline t 562 | 'invisible 'org-include-inline)) 563 | (overlay-put ov 'evaporate nil) 564 | (overlay-put ov 'priority 100) 565 | (push ov org-include-inline--overlays))) 566 | (error 567 | (message "org-include-inline--create-or-update-overlay: ERROR on make-overlay (point %s, buffer %s): %S" 568 | point buf e-make-overlay)))) 569 | (message "org-include-inline--create-or-update-overlay: Content was NIL or empty. No overlay created. ===EXITING===")))) 570 | 571 | (defun org-include-inline--clear-overlays () 572 | "Remove all org-include-inline overlays from the current buffer." 573 | (when (and (boundp 'org-include-inline--overlays) 574 | org-include-inline--overlays) 575 | (dolist (ov org-include-inline--overlays) 576 | (when (overlayp ov) 577 | (delete-overlay ov))) 578 | (setq org-include-inline--overlays nil))) 579 | 580 | (defun org-include-inline--cleanup-on-kill () 581 | "Clean up org-include-inline registrations when a buffer is killed." 582 | (when (and (boundp 'org-include-inline-mode) org-include-inline-mode) 583 | (remhash (current-buffer) org-include-inline--last-refresh-time) 584 | (org-include-inline--unregister-buffer (current-buffer)))) 585 | 586 | (add-hook 'kill-buffer-hook #'org-include-inline--cleanup-on-kill) 587 | 588 | (defun org-include-inline--register-source-file (source-file org-buffer) 589 | "Register that ORG-BUFFER (a buffer object) includes SOURCE-FILE (a path string)." 590 | (let* ((source-path (expand-file-name source-file)) 591 | (existing-entry (assoc source-path org-include-inline--source-buffers))) 592 | (if existing-entry 593 | (unless (member org-buffer (cdr existing-entry)) 594 | (setf (cdr existing-entry) (cons org-buffer (cdr existing-entry))) 595 | (push (cons source-path (list org-buffer)) 596 | org-include-inline--source-buffers) 597 | (org-include-inline--save-associations))))) 598 | 599 | (defun org-include-inline--unregister-buffer (buffer) 600 | "Remove BUFFER (a buffer object) from all source file registrations." 601 | (setq org-include-inline--source-buffers 602 | (cl-loop for (source-file . buffers) in org-include-inline--source-buffers 603 | ;; `buffers` is a list of buffer objects after load_associations 604 | for new-buffers = (remq buffer buffers) ; Remove the specific buffer object 605 | when new-buffers 606 | collect (cons source-file new-buffers) 607 | ;; If new-buffers is empty, this source-file entry will be filtered out by save-associations 608 | )) 609 | (org-include-inline--save-associations)) 610 | 611 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 612 | ;;; Core Functions 613 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 614 | 615 | (defun org-include-inline--ensure-blank-lines-around-include () 616 | "Ensure there are blank lines around the INCLUDE directive at point. 617 | Should be called with point at the beginning of an INCLUDE line. 618 | Returns the point position after ensuring blank lines." 619 | (let ((orig-pos (point))) 620 | ;; Check and ensure blank line after INCLUDE 621 | (forward-line 1) 622 | ;; Look ahead to see if we need a blank line 623 | (let ((next-non-blank 624 | (save-excursion 625 | (while (and (not (eobp)) 626 | (looking-at-p "^[ \t]*$")) 627 | (forward-line 1)) 628 | (point)))) 629 | ;; Only add blank line if we found a non-blank line and it's not already separated 630 | (when (and (< (point) next-non-blank) 631 | (not (looking-at-p "^[ \t]*$"))) 632 | (insert "\n"))) 633 | 634 | ;; Return to original position 635 | (goto-char orig-pos))) 636 | 637 | (defun org-include-inline-refresh-buffer () 638 | "Refresh all inline includes in the current buffer. 639 | 640 | This function: 641 | 1. Clears all existing overlays 642 | 2. Scans for #+INCLUDE directives 643 | 3. For each directive: 644 | - Parses the include specification 645 | - Fetches content from source file 646 | - Creates overlay to display content 647 | 4. Registers source file dependencies for auto-refresh" 648 | (interactive) 649 | (unless org-include-inline--refreshing 650 | (let ((org-include-inline--refreshing t)) 651 | (org-include-inline--clear-overlays) 652 | 653 | ;; Only process if we're in an org buffer with the mode enabled 654 | (when (and (derived-mode-p 'org-mode) 655 | org-include-inline-mode) 656 | (let ((current-buffer (current-buffer)) 657 | (count 0) 658 | (source-files '())) ; Track source files for this buffer 659 | 660 | (save-excursion 661 | (goto-char (point-min)) 662 | ;; Find all #+INCLUDE directives 663 | (while (search-forward-regexp "^[ \t]*#\\+INCLUDE:" nil t) 664 | (setq count (1+ count)) 665 | 666 | (let* ((include-start (line-beginning-position)) 667 | (current-line-text 668 | (buffer-substring-no-properties 669 | include-start (line-end-position))) 670 | (include-info (org-include-inline--parse-include-directive current-line-text))) 671 | 672 | ;; Ensure blank lines around the INCLUDE directive 673 | (org-include-inline--ensure-blank-lines-around-include) 674 | 675 | (when include-info 676 | (let* ((source-file (plist-get include-info :file)) 677 | (type (plist-get include-info :type))) 678 | ;; Add to source files list if not already there 679 | (unless (member source-file source-files) 680 | (push source-file source-files)) 681 | 682 | ;; Calculate overlay position (next line after #+INCLUDE) 683 | (let ((overlay-pos (save-excursion 684 | (goto-char include-start) 685 | (forward-line 1) 686 | (point)))) 687 | 688 | ;; Fetch and display content based on include type 689 | (let ((content 690 | (cond 691 | ;; For :lines type, we need to handle the file directly 692 | ((eq type :lines) 693 | (if (file-readable-p source-file) 694 | (org-include-inline--fetch-file-lines 695 | source-file 696 | (plist-get include-info :lines-spec) 697 | (plist-get include-info :block-type) 698 | (plist-get include-info :language)) 699 | (format "Error: File not readable: %s" source-file))) 700 | ;; For :headline type, use the headline content function 701 | ((eq type :headline) 702 | (if (file-readable-p source-file) 703 | (org-include-inline--fetch-org-headline-content 704 | source-file 705 | (plist-get include-info :headline-spec)) 706 | (format "Error: File not readable: %s" source-file))) 707 | ;; For :named-block type, use the named block function 708 | ((eq type :named-block) 709 | (if (file-readable-p source-file) 710 | (org-include-inline--fetch-named-block-content 711 | source-file 712 | (plist-get include-info :named-block)) 713 | (format "Error: File not readable: %s" source-file))) 714 | ;; For :id type, use the ID content function 715 | ((eq type :id) 716 | (if (file-readable-p source-file) 717 | (org-include-inline--fetch-org-id-content 718 | (plist-get include-info :id-spec)) 719 | (format "Error: File not readable: %s" source-file))) 720 | (t 721 | (format "Error: Unknown include type for %s" 722 | (plist-get include-info :original-line)))))) 723 | 724 | ;; Create overlay if we have valid content 725 | (when (and content (> (length content) 0)) 726 | (org-include-inline--create-or-update-overlay 727 | overlay-pos content current-buffer) 728 | 729 | ;; Register this org buffer as dependent on the source file 730 | (org-include-inline--register-source-file 731 | source-file current-buffer)))))))) 732 | 733 | (message "Refresh complete. Processed %d includes." count))))))) 734 | 735 | (defun org-include-inline--refresh-dependent-buffers () 736 | "Refresh all org buffers that include the current buffer's file." 737 | (unless org-include-inline--refreshing ;; 防止递归 738 | (when buffer-file-name 739 | (let ((org-include-inline--refreshing t) 740 | (source-path (expand-file-name buffer-file-name))) 741 | (dolist (buffer-entry org-include-inline--source-buffers) 742 | (when (string= (car buffer-entry) source-path) 743 | (dolist (org-buffer (cdr buffer-entry)) 744 | (when (buffer-live-p org-buffer) 745 | (with-current-buffer org-buffer 746 | (org-include-inline-refresh-buffer)))))))))) 747 | 748 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 749 | ;;; Interactive #+INCLUDE Creation 750 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 751 | 752 | (defvar org-include-inline--confirm-lines-map (make-sparse-keymap) 753 | "Keymap for org-include-inline-confirm-lines in target buffer.") 754 | (define-key org-include-inline--confirm-lines-map (kbd "C-c C-l") 'org-include-inline-confirm-lines) 755 | 756 | (defun org-include-inline-insert-from-lines () 757 | "Interactively select a file and a range of lines to create an #+INCLUDE directive." 758 | (interactive) 759 | (let* ((current-org-buffer (current-buffer)) 760 | (target-file (read-file-name "Include lines from file: " nil nil t))) 761 | (unless (file-exists-p target-file) 762 | (message "File does not exist: %s" target-file) 763 | (error "File not found")) 764 | (let ((smart-path (org-include-inline--get-smart-path target-file))) 765 | (let ((target-buffer (find-file-noselect target-file))) 766 | (unless target-buffer 767 | (error "Failed to open target file: %s" target-file)) 768 | (with-current-buffer target-buffer 769 | (message "Setting up for file selection in: %s" (buffer-name)) 770 | (set (make-local-variable 'org-include-inline--target-org-buffer) current-org-buffer) 771 | (set (make-local-variable 'org-include-inline--relative-target-file) smart-path) 772 | (unless (and (boundp 'org-include-inline--target-org-buffer) 773 | (boundp 'org-include-inline--relative-target-file)) 774 | (error "Failed to set required variables")) 775 | (use-local-map org-include-inline--confirm-lines-map) 776 | (message "Select region in %s, then press C-c C-l to confirm selection" 777 | (file-name-nondirectory target-file))) 778 | (select-window (display-buffer target-buffer 779 | '(display-buffer-reuse-window display-buffer-pop-up-window))))))) 780 | 781 | (defun org-include-inline--insert-include (include-spec) 782 | "Insert an INCLUDE directive with INCLUDE-SPEC and ensure blank lines around it. 783 | INCLUDE-SPEC should be the complete include specification string." 784 | ;; Check if we need a newline before 785 | (unless (or (bolp) 786 | (save-excursion 787 | (beginning-of-line) 788 | (looking-at-p "^[ \t]*$"))) 789 | (insert "\n")) 790 | 791 | ;; Insert the INCLUDE directive 792 | (insert (format "#+INCLUDE: %s\n" include-spec)) 793 | 794 | ;; Check if we need a blank line after 795 | (save-excursion 796 | ;; Look ahead to find next non-blank line 797 | (let ((next-non-blank 798 | (save-excursion 799 | (forward-line 1) 800 | (while (and (not (eobp)) 801 | (looking-at-p "^[ \t]*$")) 802 | (forward-line 1)) 803 | (point)))) 804 | ;; Only add blank line if we found a non-blank line and it's not already separated 805 | (when (and (< (point) next-non-blank) 806 | (save-excursion 807 | (forward-line 1) 808 | (not (looking-at-p "^[ \t]*$")))) 809 | (forward-line 1) 810 | (insert "\n"))))) 811 | 812 | (defun org-include-inline-confirm-lines () 813 | "Confirm line selection and insert #+INCLUDE directive. Called from the target file buffer." 814 | (interactive) 815 | (unless (and (boundp 'org-include-inline--target-org-buffer) 816 | (buffer-live-p org-include-inline--target-org-buffer)) 817 | (error "No pending include operation. Use 'org-include-inline-insert-from-lines' first.")) 818 | (unless (boundp 'org-include-inline--relative-target-file) 819 | (error "Target file path not found. Please restart the include operation.")) 820 | (unless (region-active-p) 821 | (use-local-map nil) ; remove temporary keymap 822 | (error "No region selected. Please select lines to include.")) 823 | (let ((start (line-number-at-pos (region-beginning))) 824 | (end (line-number-at-pos (region-end))) 825 | (rel-path org-include-inline--relative-target-file) 826 | (org-buf org-include-inline--target-org-buffer)) 827 | (with-current-buffer org-buf 828 | (let ((actual-include-path rel-path)) 829 | (org-include-inline--insert-include 830 | (format "\"%s\" :lines \"%d-%d\"" actual-include-path start end)) 831 | (when org-include-inline-mode 832 | (org-include-inline-refresh-buffer)))) 833 | (message "Successfully inserted #+INCLUDE for lines %d-%d from %s" start end rel-path) 834 | (use-local-map nil) ; remove temporary keymap 835 | (when (boundp 'org-include-inline--target-org-buffer) 836 | (kill-local-variable 'org-include-inline--target-org-buffer)) 837 | (when (boundp 'org-include-inline--relative-target-file) 838 | (kill-local-variable 'org-include-inline--relative-target-file)) 839 | (if (one-window-p) 840 | (bury-buffer) 841 | (delete-window)))) 842 | 843 | (defun org-include-inline--get-smart-path (file) 844 | "Intelligently process file paths, prioritize using tilde to represent the user's home directory. 845 | If FILE is located in the user's home directory, return the path starting with ~. 846 | Otherwise, return the path relative to the current file." 847 | (let* ((expanded-file (expand-file-name file)) 848 | (home-dir (expand-file-name "~")) 849 | (current-dir (if buffer-file-name 850 | (file-name-directory buffer-file-name) 851 | default-directory)) 852 | (tilde-path (and (string-prefix-p home-dir expanded-file) 853 | (concat "~" (substring expanded-file (length home-dir))))) 854 | (relative-path (file-relative-name expanded-file current-dir))) 855 | (cond 856 | ((and tilde-path (< (length tilde-path) (length relative-path))) 857 | tilde-path) 858 | ((and relative-path (not (string-equal relative-path "."))) 859 | relative-path) 860 | (t expanded-file)))) 861 | 862 | (defun org-include-inline-insert-file () 863 | "Interactively select a file to create an #+INCLUDE directive for the entire file." 864 | (interactive) 865 | (let* ((target-file (read-file-name "Include entire file: " nil nil t)) 866 | (smart-path nil)) 867 | (unless (file-exists-p target-file) 868 | (message "File does not exist: %s" target-file) 869 | (error "File not found")) 870 | (setq smart-path (org-include-inline--get-smart-path target-file)) 871 | (org-include-inline--insert-include (format "\"%s\"" smart-path)) 872 | (message "Inserted #+INCLUDE for entire file: %s" smart-path) 873 | (when org-include-inline-mode 874 | (org-include-inline-refresh-buffer)))) 875 | 876 | (defun org-include-inline-insert-as-block () 877 | "Interactively create an #+INCLUDE directive with a specific block type." 878 | (interactive) 879 | (let* ((target-file (read-file-name "Include file as block: " nil nil t)) 880 | (smart-path nil) 881 | (block-type (completing-read "Block type: " org-include-inline--block-types nil nil)) 882 | (language nil)) 883 | 884 | ;; Validate file exists 885 | (unless (file-exists-p target-file) 886 | (message "File does not exist: %s" target-file) 887 | (error "File not found")) 888 | 889 | ;; Get the smart path (relative or with ~) 890 | (setq smart-path (org-include-inline--get-smart-path target-file)) 891 | 892 | ;; For src blocks, prompt for language 893 | (when (string= block-type "src") 894 | (setq language (completing-read "Programming language: " 895 | org-include-inline--common-languages 896 | nil ; predicate 897 | nil ; require-match 898 | nil ; initial-input 899 | nil ; hist 900 | (when-let* ((file-ext (file-name-extension target-file)) 901 | (lang (cond 902 | ((member file-ext '("el" "elisp")) "emacs-lisp") 903 | ((member file-ext '("py")) "python") 904 | ((member file-ext '("sh" "bash")) "sh") 905 | ((member file-ext '("c")) "C") 906 | ((member file-ext '("cpp" "cc" "cxx")) "C++") 907 | ((member file-ext '("js")) "javascript") 908 | ((member file-ext '("java")) "java") 909 | ((member file-ext '("css")) "css") 910 | ((member file-ext '("html" "htm")) "html") 911 | ((member file-ext '("org")) "org") 912 | ((member file-ext '("tex")) "latex")))) 913 | lang)))) 914 | 915 | ;; Handle :custom block type 916 | (when (string= block-type ":custom") 917 | (setq block-type (format "\"%s\"" 918 | (read-string "Enter custom block name (with leading :): ")))) 919 | 920 | ;; Insert the directive 921 | (org-include-inline--insert-include 922 | (cond 923 | ((and (string= block-type "src") language) 924 | (format "\"%s\" %s %s" smart-path block-type language)) 925 | (t 926 | (format "\"%s\" %s" smart-path block-type)))) 927 | 928 | (message "Inserted #+INCLUDE for %s as %s block" 929 | (file-name-nondirectory target-file) 930 | (if (string= block-type "src") 931 | (format "%s %s" block-type language) 932 | block-type)) 933 | 934 | ;; Refresh if mode is enabled 935 | (when org-include-inline-mode 936 | (org-include-inline-refresh-buffer)))) 937 | 938 | (defun org-include-inline-insert-named-block () 939 | "Interactively select and include a named block from an Org file." 940 | (interactive) 941 | (let* ((target-file (read-file-name "Include named block from Org file: " nil nil t)) 942 | (smart-path nil)) 943 | 944 | ;; Validate file exists and is org 945 | (unless (and (file-exists-p target-file) 946 | (string-match-p "\\.org$" target-file)) 947 | (message "File must be an existing .org file: %s" target-file) 948 | (error "Invalid file")) 949 | 950 | ;; Get named blocks 951 | (let ((blocks (org-include-inline--get-named-blocks target-file))) 952 | (unless blocks 953 | (message "No named blocks found in %s" target-file) 954 | (error "No named blocks")) 955 | 956 | ;; Get the smart path 957 | (setq smart-path (org-include-inline--get-smart-path target-file)) 958 | 959 | ;; Let user select a block 960 | (let* ((choices (mapcar (lambda (block) 961 | (let ((name (car block)) 962 | (type (plist-get (cdr block) :type)) 963 | (lang (plist-get (cdr block) :language))) 964 | (format "%s (%s%s)" 965 | name 966 | type 967 | (if lang (format " - %s" lang) "")))) 968 | blocks)) 969 | (choice (completing-read "Select block: " choices nil t)) 970 | (selected-block (nth (cl-position choice choices :test #'string=) blocks)) 971 | (block-name (car selected-block))) 972 | 973 | ;; Insert the directive 974 | (org-include-inline--insert-include 975 | (format "\"%s::%s\"" smart-path block-name)) 976 | (message "Inserted #+INCLUDE for named block \"%s\" from %s" 977 | block-name 978 | (file-name-nondirectory target-file)) 979 | 980 | ;; Refresh if mode is enabled 981 | (when org-include-inline-mode 982 | (org-include-inline-refresh-buffer)))))) 983 | 984 | (defun org-include-inline-insert-headline () 985 | "Interactively select and include a headline from an Org file." 986 | (interactive) 987 | (let* ((target-file (read-file-name "Include headline from Org file: " nil nil t))) 988 | (unless (and (file-exists-p target-file) 989 | (string-match-p "\\.org$" target-file)) 990 | (user-error "File must be an existing .org file: %s" target-file)) 991 | 992 | (let ((smart-path (org-include-inline--get-smart-path target-file)) 993 | (headlines '())) 994 | 995 | (setq headlines 996 | (org-include-inline--with-temp-org-buffer 997 | target-file 998 | (lambda () 999 | (org-element-map (org-element-parse-buffer 'greater-element) 'headline 1000 | (lambda (h) 1001 | (let* ((title (org-element-property :raw-value h)) 1002 | (custom-id (org-element-property :CUSTOM_ID h)) 1003 | (level (org-element-property :level h)) 1004 | (display-title (format "%s %s%s" 1005 | (make-string level ?*) 1006 | (if custom-id (format "[#%s] " custom-id) "") 1007 | (if (stringp title) title 1008 | (prin1-to-string title))))) 1009 | (list display-title custom-id title level))) 1010 | nil nil t)))) 1011 | 1012 | (unless headlines 1013 | (user-error "No headlines found in %s" target-file)) 1014 | 1015 | (let* ((choices (mapcar #'car headlines)) 1016 | (chosen-display (completing-read "Select headline: " choices nil t))) 1017 | (when chosen-display 1018 | (let* ((selection (cl-find chosen-display headlines :key #'car :test #'string=)) 1019 | (custom-id (nth 1 selection)) 1020 | (title (nth 2 selection)) 1021 | (level (nth 3 selection)) 1022 | (include-spec (if custom-id 1023 | (format "#%s" custom-id) 1024 | (format "*%s" title)))) 1025 | 1026 | (org-include-inline--insert-include 1027 | (format "\"%s::%s\"" smart-path include-spec)) 1028 | (message "Inserted #+INCLUDE for headline \"%s\" from %s" 1029 | chosen-display 1030 | (file-name-nondirectory target-file)) 1031 | 1032 | (when org-include-inline-mode 1033 | (org-include-inline-refresh-buffer)))))))) 1034 | 1035 | (defun org-include-inline--get-entries-with-ids (file) 1036 | "Scan FILE for entries with IDs and return an alist of (title . id). 1037 | Each entry contains the headline title and its ID." 1038 | (org-include-inline--with-temp-org-buffer 1039 | file 1040 | (lambda () 1041 | (let ((entries nil) 1042 | (ast (org-element-parse-buffer))) 1043 | ;; Collect all headlines with ID properties 1044 | (org-element-map ast 'headline 1045 | (lambda (element) 1046 | (let ((id (org-element-property :ID element)) 1047 | (title (org-element-property :raw-value element))) 1048 | (when id 1049 | (push (cons (or title (format "Headline at line %d" 1050 | (org-element-property :begin element))) 1051 | id) 1052 | entries))))) 1053 | (nreverse entries))))) 1054 | 1055 | (defun org-include-inline-insert-id () 1056 | "Interactively insert an #+INCLUDE directive based on ID." 1057 | (interactive) 1058 | (let* ((target-file (read-file-name "Select Org file containing target ID: " nil nil t))) 1059 | 1060 | (unless (and (file-exists-p target-file) 1061 | (string-match-p "\\.org$" target-file)) 1062 | (user-error "File must be an existing .org file: %s" target-file)) 1063 | 1064 | (let* ((smart-path (org-include-inline--get-smart-path target-file)) 1065 | (entries (org-include-inline--get-entries-with-ids target-file))) 1066 | 1067 | (unless entries 1068 | (user-error "No entries with IDs found in %s" target-file)) 1069 | 1070 | (let* ((choices (mapcar (lambda (entry) 1071 | (format "%s [%s]" (car entry) (cdr entry))) 1072 | entries)) 1073 | (chosen (completing-read "Select entry to include: " choices nil t)) 1074 | (chosen-id (cdr (nth (cl-position chosen choices :test #'string=) 1075 | entries)))) 1076 | 1077 | (org-include-inline--insert-include 1078 | (format "\"%s::%s\"" smart-path chosen-id)) 1079 | (message "Inserted #+INCLUDE directive for ID %s" chosen-id) 1080 | 1081 | (when org-include-inline-mode 1082 | (org-include-inline-refresh-buffer)))))) 1083 | 1084 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1085 | ;;; Minor Mode Definition 1086 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1087 | 1088 | ;;; Automatically enable org-include-inline-mode in org-mode 1089 | (defun org-include-inline-maybe-enable () 1090 | "Enable org-include-inline-mode based on auto-enable setting." 1091 | (when (derived-mode-p 'org-mode) 1092 | (pcase org-include-inline-auto-enable-in-org-mode 1093 | ('t 1094 | ;; Always enable 1095 | (org-include-inline-mode 1)) 1096 | ('smart 1097 | ;; Only enable if buffer contains #+INCLUDE directives 1098 | (save-excursion 1099 | (goto-char (point-min)) 1100 | (when (search-forward-regexp "^[ \t]*#\\+INCLUDE:" nil t) 1101 | (org-include-inline-mode 1)))) 1102 | ;; nil: do nothing (manual activation only) 1103 | ))) 1104 | (add-hook 'org-mode-hook #'org-include-inline-maybe-enable) 1105 | 1106 | (defun org-include-inline--after-save-handler () 1107 | "Handle after-save-hook for source files." 1108 | (when (and buffer-file-name 1109 | (not org-include-inline--refreshing)) 1110 | (let ((org-include-inline--refreshing t) 1111 | (source-path (expand-file-name buffer-file-name))) 1112 | (when (assoc source-path org-include-inline--source-buffers) 1113 | (dolist (buffer (cdr (assoc source-path org-include-inline--source-buffers))) 1114 | (when (buffer-live-p buffer) 1115 | (with-current-buffer buffer 1116 | (org-include-inline-refresh-buffer) 1117 | (puthash buffer (float-time) org-include-inline--last-refresh-time))))) 1118 | 1119 | (when (and org-include-inline-mode 1120 | (derived-mode-p 'org-mode)) 1121 | (org-include-inline-refresh-buffer) 1122 | (puthash (current-buffer) (float-time) org-include-inline--last-refresh-time))))) 1123 | 1124 | (defun org-include-inline--setup-file-watch () 1125 | "Set up file watching for source files." 1126 | (dolist (entry org-include-inline--source-buffers) 1127 | (let ((source-file (car entry))) 1128 | (when (and (stringp source-file) 1129 | (file-exists-p source-file)) 1130 | (file-notify-add-watch 1131 | source-file 1132 | '(change) 1133 | (lambda (event) 1134 | (when (eq (nth 1 event) 'changed) 1135 | (let ((source-path (nth 2 event))) 1136 | (dolist (buffer (cdr (assoc source-path org-include-inline--source-buffers))) 1137 | (when (buffer-live-p buffer) 1138 | (with-current-buffer buffer 1139 | (org-include-inline-refresh-buffer) 1140 | (puthash buffer (float-time) org-include-inline--last-refresh-time)))))))))))) 1141 | 1142 | (defun org-include-inline--is-valid-org-id-p (id) 1143 | "Check if ID is a valid Org ID string. 1144 | Supports various ID formats including UUID, org's internal format, and timestamp-based IDs. 1145 | Also supports custom formats defined in `org-include-inline-additional-id-formats'." 1146 | (and (stringp id) 1147 | (or 1148 | ;; UUID format: 8-4-4-4-12 hex digits 1149 | (string-match-p "\\`[A-Fa-f0-9]\\{8\\}-[A-Fa-f0-9]\\{4\\}-[A-Fa-f0-9]\\{4\\}-[A-Fa-f0-9]\\{4\\}-[A-Fa-f0-9]\\{12\\}\\'" id) 1150 | ;; Org's internal format: 32 hex digits 1151 | (string-match-p "\\`[A-Fa-f0-9]\\{32\\}\\'" id) 1152 | ;; Timestamp format: YYYY-MM-DD-HH-MM-SS.XXXXXX_XXXXX 1153 | (string-match-p "\\`[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}-[0-9]\\{2\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\.[0-9]+_[A-Za-z0-9]+\\'" id) 1154 | ;; Check against additional custom formats 1155 | (cl-some (lambda (pattern) 1156 | (string-match-p pattern id)) 1157 | org-include-inline-additional-id-formats)))) 1158 | 1159 | (defun org-include-inline--advice-org-link-open-as-file (old-fn path arg) 1160 | "Advice for `org-link-open-as-file' to handle org-include-inline ID links. 1161 | If PATH contains an ID reference, find and jump to that ID. 1162 | Otherwise, call the original function OLD-FN with PATH and ARG." 1163 | (if (and path 1164 | (string-match "\\(.*\\)::\\([A-Za-z0-9-]+\\)\\'" path)) 1165 | (let* ((file (match-string 1 path)) 1166 | (id (match-string 2 path))) 1167 | ;; Check if it looks like an ID 1168 | (if (org-include-inline--is-valid-org-id-p id) 1169 | (condition-case err 1170 | (progn 1171 | ;; Open the file first 1172 | (find-file file) 1173 | ;; Then try to find the ID 1174 | (or (org-id-goto id) 1175 | (user-error "Cannot find ID \"%s\" in file %s" 1176 | id (file-name-nondirectory file)))) 1177 | (error 1178 | (message "Error jumping to ID %s: %S" id err) 1179 | (user-error "Cannot open file %s or find ID \"%s\"" 1180 | (file-name-nondirectory file) id))) 1181 | ;; Not an ID, call original function 1182 | (funcall old-fn path arg))) 1183 | ;; No ID pattern, call original function 1184 | (funcall old-fn path arg))) 1185 | 1186 | ;; Remove old advice if it exists 1187 | (when (advice-member-p #'org-include-inline--advice-org-open-at-point 'org-open-at-point) 1188 | (advice-remove 'org-open-at-point #'org-include-inline--advice-org-open-at-point)) 1189 | 1190 | ;; Add new advice 1191 | (unless (advice-member-p #'org-include-inline--advice-org-link-open-as-file 'org-link-open-as-file) 1192 | (advice-add 'org-link-open-as-file :around #'org-include-inline--advice-org-link-open-as-file)) 1193 | 1194 | (defun org-include-inline--is-point-folded (point) 1195 | "Check if POINT is within any folded region in the current buffer. 1196 | Uses org-fold to accurately determine if point is in a folded region." 1197 | (save-excursion 1198 | (goto-char point) 1199 | (or 1200 | ;; Check if point is directly in a folded region 1201 | (org-fold-folded-p) 1202 | ;; Check if point is in a folded outline region 1203 | (let ((current-pos point)) 1204 | (save-excursion 1205 | (when (org-back-to-heading t) 1206 | (let* ((heading-pos (point)) 1207 | (subtree-end (save-excursion (org-end-of-subtree t t))) 1208 | ;; Check if the heading has a folded region 1209 | (folded (org-fold-folded-p))) 1210 | (and folded 1211 | (> current-pos heading-pos) 1212 | (< current-pos subtree-end))))))))) 1213 | 1214 | (defun org-include-inline--update-folding-state (&optional _cycle-state) 1215 | "Update visibility of includes based on heading fold state. 1216 | Uses org-fold for accurate visibility detection. 1217 | Argument _CYCLE-STATE is ignored but kept for compatibility with `org-cycle-hook'." 1218 | (when (and org-include-inline-mode 1219 | org-include-inline-respect-folding) 1220 | (save-excursion 1221 | (dolist (ov org-include-inline--overlays) 1222 | (when (overlay-buffer ov) ; ensure overlay still exists 1223 | (let* ((overlay-pos (overlay-start ov)) 1224 | (before-str (overlay-get ov 'before-string))) 1225 | (when before-str 1226 | ;; Check if the overlay position is within any folded region 1227 | (let ((should-hide (org-include-inline--is-point-folded overlay-pos))) 1228 | (put-text-property 0 (length before-str) 1229 | 'invisible 1230 | (when should-hide 'org-include-inline) 1231 | before-str))))))))) 1232 | 1233 | (defun org-include-inline--export-filter (backend) 1234 | "Filter function for export process. 1235 | Handles includes according to `org-include-inline-export-behavior': 1236 | - selective: process includes normally, except those marked with :export: no 1237 | - ignore: completely ignore all includes 1238 | - process: process all includes normally (same as org default)" 1239 | (when (bound-and-true-p org-include-inline-mode) 1240 | (let ((includes (org-element-map (org-element-parse-buffer) 'keyword 1241 | (lambda (keyword) 1242 | (when (string= (org-element-property :key keyword) "INCLUDE") 1243 | keyword))))) 1244 | ;; Save original state 1245 | (setq-local org-include-inline--original-includes 1246 | (mapcar (lambda (inc) 1247 | (cons inc (org-element-property :value inc))) 1248 | includes)) 1249 | 1250 | (pcase org-include-inline-export-behavior 1251 | ('ignore 1252 | ;; Comment out all includes 1253 | (dolist (inc includes) 1254 | (goto-char (org-element-property :begin inc)) 1255 | (insert "# "))) 1256 | 1257 | ('selective 1258 | ;; Only comment out includes explicitly marked with :export: no 1259 | (dolist (inc includes) 1260 | (save-excursion 1261 | (goto-char (org-element-property :begin inc)) 1262 | ;; Check for :export: property in the line 1263 | (let* ((line (buffer-substring-no-properties 1264 | (line-beginning-position) 1265 | (line-end-position))) 1266 | (export-prop (when (string-match ":export:\\s-*\\([^ \t\n]+\\)" line) 1267 | (match-string 1 line)))) 1268 | ;; Comment out only if :export: is explicitly "no" 1269 | (when (string= export-prop "no") 1270 | (goto-char (line-beginning-position)) 1271 | (insert "# ")))))) 1272 | 1273 | ('process 1274 | ;; Do nothing, let org process all includes normally 1275 | nil))))) 1276 | 1277 | (defun org-include-inline--after-export () 1278 | "Restore includes after export." 1279 | (when (bound-and-true-p org-include-inline--original-includes) 1280 | (save-excursion 1281 | (dolist (pair org-include-inline--original-includes) 1282 | (let ((keyword (car pair))) 1283 | (goto-char (org-element-property :begin keyword)) 1284 | (when (looking-at "^#\\s-*") ; Only remove if we added the comment 1285 | (delete-char 2))))) 1286 | (setq-local org-include-inline--original-includes nil))) 1287 | 1288 | ;; Move this function definition before define-minor-mode 1289 | (defun org-include-inline--setup-folding-hooks () 1290 | "Set up hooks for handling folding state changes." 1291 | (when org-include-inline-mode 1292 | ;; Hook into org-cycle for folding updates 1293 | (add-hook 'org-cycle-hook #'org-include-inline--update-folding-state nil t) 1294 | ;; Also hook into visibility changes 1295 | (add-hook 'org-fold-folded-hook #'org-include-inline--update-folding-state nil t))) 1296 | 1297 | ;;;###autoload 1298 | (define-minor-mode org-include-inline-mode 1299 | "Toggle display of #+INCLUDE contents inline in Org buffers. 1300 | With no argument, this command toggles the mode. 1301 | A positive prefix argument enables the mode, any other prefix argument disables it. 1302 | 1303 | When enabled, #+INCLUDE directives will have their content displayed 1304 | inline using overlays. 1305 | 1306 | Available commands: 1307 | `org-include-inline-refresh-buffer' - Refresh all inline includes in the current buffer 1308 | `org-include-inline-insert-file' - Insert a directive to include an entire file 1309 | `org-include-inline-insert-from-lines' - Insert a directive to include specific lines from a file 1310 | `org-include-inline-insert-as-block' - Insert a directive to include file as a block (src, example, etc.) 1311 | `org-include-inline-insert-named-block' - Insert a directive to include a named block from an Org file 1312 | `org-include-inline-insert-headline' - Insert a directive to include a headline/subtree from an Org file 1313 | `org-include-inline-insert-id' - Insert a directive to include an entry with ID" 1314 | :init-value nil 1315 | :lighter " IInc" 1316 | :keymap (let ((map (make-sparse-keymap))) 1317 | (define-key map (kbd org-include-inline-auto-refresh-key) 1318 | #'org-include-inline-refresh-buffer) 1319 | map) 1320 | :group 'org-include-inline 1321 | (if org-include-inline-mode 1322 | (progn 1323 | ;; Silently enable the mode 1324 | (unless org-include-inline--source-buffers 1325 | (org-include-inline--load-associations)) 1326 | ;; Add export hooks 1327 | (add-hook 'org-export-before-processing-hook 1328 | #'org-include-inline--export-filter nil t) 1329 | (add-hook 'org-export-after-processing-hook 1330 | #'org-include-inline--after-export nil t) 1331 | ;; Set up folding hooks 1332 | (org-include-inline--setup-folding-hooks) 1333 | (add-hook 'after-save-hook #'org-include-inline--after-save-handler nil t) 1334 | (add-hook 'after-revert-hook #'org-include-inline-refresh-buffer nil t) 1335 | (add-hook 'window-configuration-change-hook #'org-include-inline-refresh-buffer nil t) 1336 | (let ((org-include-inline--refreshing t)) 1337 | (org-include-inline-refresh-buffer) 1338 | (puthash (current-buffer) (float-time) org-include-inline--last-refresh-time))) 1339 | (progn 1340 | ;; Silently disable the mode 1341 | ;; Remove export hooks 1342 | (remove-hook 'org-export-before-processing-hook 1343 | #'org-include-inline--export-filter t) 1344 | (remove-hook 'org-export-after-processing-hook 1345 | #'org-include-inline--after-export t) 1346 | ;; Remove folding hooks 1347 | (remove-hook 'org-cycle-hook 1348 | #'org-include-inline--update-folding-state t) 1349 | (remove-hook 'org-fold-folded-hook 1350 | #'org-include-inline--update-folding-state t) 1351 | (remove-hook 'after-save-hook #'org-include-inline--after-save-handler t) 1352 | (remove-hook 'after-revert-hook #'org-include-inline-refresh-buffer t) 1353 | (remove-hook 'window-configuration-change-hook #'org-include-inline-refresh-buffer t) 1354 | (remhash (current-buffer) org-include-inline--last-refresh-time) 1355 | (org-include-inline--unregister-buffer (current-buffer)) 1356 | (org-include-inline--clear-overlays)))) 1357 | 1358 | (defalias 'org-include-inline-refresh 'org-include-inline-refresh-buffer) 1359 | 1360 | (with-eval-after-load 'org 1361 | (define-key org-mode-map (kbd "C-c C-x C-v") #'org-include-inline-refresh-buffer)) 1362 | 1363 | (provide 'org-include-inline) 1364 | ;;; org-include-inline.el ends here 1365 | --------------------------------------------------------------------------------