1 ;;; org-link-edit.el --- Slurp and barf with Org links -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2015-2017 Kyle Meyer <kyle@kyleam.com>
5 ;; Author: Kyle Meyer <kyle@kyleam.com>
6 ;; URL: https://gitlab.com/kyleam/org-link-edit
7 ;; Keywords: convenience
9 ;; Package-Requires: ((cl-lib "0.5") (org "8.2.10"))
11 ;; This program is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; This program is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
26 ;; Org Link Edit provides Paredit-inspired slurping and barfing
27 ;; commands for Org link descriptions.
29 ;; There are four slurp and barf commands, all which operate when
30 ;; point is on an Org link.
32 ;; - org-link-edit-forward-slurp
33 ;; - org-link-edit-backward-slurp
34 ;; - org-link-edit-forward-barf
35 ;; - org-link-edit-backward-barf
37 ;; Org Link Edit doesn't bind these commands to any keys. Finding
38 ;; good keys for these commands is difficult because, while it's
39 ;; convenient to be able to quickly repeat these commands, they won't
40 ;; be used frequently enough to be worthy of a short, repeat-friendly
41 ;; binding. Using Hydra [1] provides a nice solution to this. After
42 ;; an initial key sequence, any of the commands will be repeatable
43 ;; with a single key. (Plus, you get a nice interface that displays
44 ;; the key for each command.) Below is one example of how you could
47 ;; (define-key org-mode-map YOUR-KEY
48 ;; (defhydra hydra-org-link-edit ()
50 ;; ("j" org-link-edit-forward-slurp "forward slurp")
51 ;; ("k" org-link-edit-forward-barf "forward barf")
52 ;; ("u" org-link-edit-backward-slurp "backward slurp")
53 ;; ("i" org-link-edit-backward-barf "backward barf")
54 ;; ("q" nil "cancel")))
56 ;; In addition to the slurp and barf commands, the command
57 ;; `org-link-edit-transport-next-link' searches for the next (or
58 ;; previous) link and moves it to point, using the word at point or
59 ;; the selected region as the link's description.
61 ;; [1] https://github.com/abo-abo/hydra
66 (require 'org-element
)
69 (defun org-link-edit--on-link-p (&optional element
)
70 (let ((el (or element
(org-element-context))))
71 ;; Don't use `org-element-lineage' because it isn't available
72 ;; until Org version 8.3.
73 (while (and el
(not (memq (car el
) '(link))))
74 (setq el
(org-element-property :parent el
)))
77 (defun org-link-edit--link-data ()
78 "Return list with information about the link at point.
80 - the position at the start of the link
81 - the position at the end of the link
83 - the link description (nil when on a plain link)"
84 (let ((el (org-element-context)))
85 (unless (org-link-edit--on-link-p el
)
86 (user-error "Point is not on a link"))
88 (goto-char (org-element-property :begin el
))
90 ;; Use match-{beginning,end} because match-end is consistently
91 ;; positioned after ]], while the :end property is positioned
92 ;; at the next word on the line, if one is present.
93 ((looking-at org-bracket-link-regexp
)
94 (list (match-beginning 0)
97 (org-link-unescape (match-string-no-properties 1)))
98 (or (and (match-end 3)
99 (match-string-no-properties 3))
101 ((looking-at org-plain-link-re
)
102 (list (match-beginning 0)
104 (org-link-unescape (match-string-no-properties 0))
107 (error "What am I looking at?"))))))
109 (defun org-link-edit--forward-blob (n &optional no-punctuation
)
110 "Move forward N blobs (backward if N is negative).
112 A block of non-whitespace characters is a blob. If
113 NO-PUNCTUATION is non-nil, trailing punctuation characters are
114 not considered part of the blob when going in the forward
117 If the edge of the buffer is reached before completing the
118 movement, return nil. Otherwise, return t."
119 (let* ((forward-p (> n
0))
121 (skip-func (if forward-p
'skip-syntax-forward
'skip-syntax-backward
))
124 (funcall skip-func
" ")
125 (setq skip-func-retval
(funcall skip-func
"^ "))
126 (setq nblobs
(1- nblobs
)))
127 (when (and forward-p no-punctuation
)
128 (let ((punc-tail-offset (save-excursion (skip-syntax-backward "."))))
129 ;; Don't consider trailing punctuation as part of the blob
130 ;; unless the whole blob consists of punctuation.
131 (unless (= skip-func-retval
(- punc-tail-offset
))
132 (goto-char (+ (point) punc-tail-offset
)))))
133 (/= skip-func-retval
0)))
136 (defun org-link-edit-forward-slurp (&optional n
)
137 "Slurp N trailing blobs into link's description.
139 The \[\[http://orgmode.org/\]\[Org mode\]\] site
144 The \[\[http://orgmode.org/\]\[Org mode site\]\]
146 A blob is a block of non-whitespace characters. When slurping
147 forward, trailing punctuation characters are not considered part
150 After slurping, return the slurped text and move point to the
151 beginning of the link.
153 If N is negative, slurp leading blobs instead of trailing blobs."
159 (org-link-edit-backward-slurp (- n
)))
161 (cl-multiple-value-bind (beg end link desc
) (org-link-edit--link-data)
162 (goto-char (save-excursion
164 (or (org-link-edit--forward-blob n
'no-punctuation
)
165 (user-error "Not enough blobs after the link"))
167 (let ((slurped (buffer-substring-no-properties end
(point))))
168 (setq slurped
(replace-regexp-in-string "\n+" " " slurped
))
169 (when (and (= (length desc
) 0)
170 (string-match "^\\s-+\\(.*\\)" slurped
))
171 (setq slurped
(match-string 1 slurped
)))
172 (setq desc
(concat desc slurped
)
173 end
(+ end
(length slurped
)))
174 (delete-region beg
(point))
175 (insert (org-make-link-string link desc
))
180 (defun org-link-edit-backward-slurp (&optional n
)
181 "Slurp N leading blobs into link's description.
183 The \[\[http://orgmode.org/\]\[Org mode\]\] site
188 \[\[http://orgmode.org/\]\[The Org mode\]\] site
190 A blob is a block of non-whitespace characters.
192 After slurping, return the slurped text and move point to the
193 beginning of the link.
195 If N is negative, slurp trailing blobs instead of leading blobs."
201 (org-link-edit-forward-slurp (- n
)))
203 (cl-multiple-value-bind (beg end link desc
) (org-link-edit--link-data)
204 (goto-char (save-excursion
206 (or (org-link-edit--forward-blob (- n
))
207 (user-error "Not enough blobs before the link"))
209 (let ((slurped (buffer-substring-no-properties (point) beg
)))
210 (when (and (= (length desc
) 0)
211 (string-match "\\(.*\\)\\s-+$" slurped
))
212 (setq slurped
(match-string 1 slurped
)))
213 (setq slurped
(replace-regexp-in-string "\n+" " " slurped
))
214 (setq desc
(concat slurped desc
)
215 beg
(- beg
(length slurped
)))
216 (delete-region (point) end
)
217 (insert (org-make-link-string link desc
))
221 (defun org-link-edit--split-first-blobs (string n
)
222 "Split STRING into (N first blobs . other) cons cell.
223 'N first blobs' contains all text from the start of STRING up to
224 the start of the N+1 blob. 'other' includes the remaining text
225 of STRING. If the number of blobs in STRING is fewer than N,
227 (when (< n
0) (user-error "N cannot be negative"))
230 (goto-char (point-min))
231 (with-syntax-table org-mode-syntax-table
232 (let ((within-bound (org-link-edit--forward-blob n
)))
233 (skip-syntax-forward " ")
234 (cons (buffer-substring 1 (point))
236 (buffer-substring (point) (point-max))))))))
238 (defun org-link-edit--split-last-blobs (string n
)
239 "Split STRING into (other . N last blobs) cons cell.
240 'N last blobs' contains all text from the end of STRING back to
241 the end of the N+1 last blob. 'other' includes the remaining
242 text of STRING. If the number of blobs in STRING is fewer than
244 (when (< n
0) (user-error "N cannot be negative"))
247 (goto-char (point-max))
248 (with-syntax-table org-mode-syntax-table
249 (let ((within-bound (org-link-edit--forward-blob (- n
))))
250 (skip-syntax-backward " ")
251 (cons (and within-bound
252 (buffer-substring 1 (point)))
253 (buffer-substring (point) (point-max)))))))
256 (defun org-link-edit-forward-barf (&optional n
)
257 "Barf N trailing blobs from link's description.
259 The \[\[http://orgmode.org/\]\[Org mode\]\] site
264 The \[\[http://orgmode.org/\]\[Org\]\] mode site
266 A blob is a block of non-whitespace characters.
268 After barfing, return the barfed text and move point to the
269 beginning of the link.
271 If N is negative, barf leading blobs instead of trailing blobs."
277 (org-link-edit-backward-barf (- n
)))
279 (cl-multiple-value-bind (beg end link desc
) (org-link-edit--link-data)
280 (when (= (length desc
) 0)
281 (user-error "Link has no description"))
282 (pcase-let ((`(,new-desc .
,barfed
) (org-link-edit--split-last-blobs
284 (unless new-desc
(user-error "Not enough blobs in description"))
286 (delete-region beg end
)
287 (insert (org-make-link-string link new-desc
))
288 (when (string= new-desc
"")
289 (setq barfed
(concat " " barfed
)))
295 (defun org-link-edit-backward-barf (&optional n
)
296 "Barf N leading blobs from link's description.
298 The \[\[http://orgmode.org/\]\[Org mode\]\] site
303 The Org \[\[http://orgmode.org/\]\[mode\]\] site
305 A blob is a block of non-whitespace characters.
307 After barfing, return the barfed text and move point to the
308 beginning of the link.
310 If N is negative, barf trailing blobs instead of leading blobs."
316 (org-link-edit-forward-barf (- n
)))
318 (cl-multiple-value-bind (beg end link desc
) (org-link-edit--link-data)
319 (when (= (length desc
) 0)
320 (user-error "Link has no description"))
321 (pcase-let ((`(,barfed .
,new-desc
) (org-link-edit--split-first-blobs
323 (unless new-desc
(user-error "Not enough blobs in description"))
325 (delete-region beg end
)
326 (insert (org-make-link-string link new-desc
))
327 (when (string= new-desc
"")
328 (setq barfed
(concat barfed
" ")))
333 (defun org-link-edit--next-link-data (&optional previous
)
335 (if (funcall (if previous
#'re-search-backward
#'re-search-forward
)
336 org-any-link-re nil t
)
337 (org-link-edit--link-data)
338 (user-error "No %s link found" (if previous
"previous" "next")))))
341 (defun org-link-edit-transport-next-link (&optional previous beg end
)
342 "Move the next link to point.
344 If the region is active, use the selected text as the link's
345 description. Otherwise, use the word at point.
347 With prefix argument PREVIOUS, move the previous link instead of
350 Non-interactively, use the text between BEG and END as the
351 description, moving the next (or previous) link relative BEG and
353 (interactive (cons current-prefix-arg
355 (list (region-beginning) (region-end)))))
359 (cons (progn (goto-char beg
)
361 (progn (goto-char end
)
363 ((not (looking-at-p "\\s-"))
364 (progn (skip-syntax-backward "w")
365 (let ((beg (point-marker)))
366 (skip-syntax-forward "w")
367 (cons beg
(point-marker))))))))
368 (when (or (and desc-bounds
369 (or (progn (goto-char (car desc-bounds
))
370 (org-link-edit--on-link-p))
371 (progn (goto-char (cdr desc-bounds
))
372 (org-link-edit--on-link-p))))
373 (progn (goto-char pt
)
374 (org-link-edit--on-link-p)))
375 (user-error "Cannot transport next link with point on a link"))
376 (goto-char (or (car desc-bounds
) pt
))
377 (cl-multiple-value-bind (link-beg link-end link orig-desc
)
378 (org-link-edit--next-link-data previous
)
379 (unless (or (not desc-bounds
) (= (length orig-desc
) 0))
380 (user-error "Link already has a description"))
381 (delete-region link-beg link-end
)
382 (insert (org-make-link-string
385 (delete-and-extract-region (car desc-bounds
)
389 (provide 'org-link-edit
)
390 ;;; org-link-edit.el ends here