Merge branch 'maint'
[org-mode.git] / contrib / lisp / org-link-edit.el
blob000dd1c2c878d1d07a21a26328b711fb5b9b11ef
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
8 ;; Version: 1.1.1
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/>.
24 ;;; Commentary:
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
45 ;; configure this.
47 ;; (define-key org-mode-map YOUR-KEY
48 ;; (defhydra hydra-org-link-edit ()
49 ;; "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
63 ;;; Code:
65 (require 'org)
66 (require 'org-element)
67 (require 'cl-lib)
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)))
75 (eq (car el) 'link)))
77 (defun org-link-edit--link-data ()
78 "Return list with information about the link at point.
79 The list includes
80 - the position at the start of the link
81 - the position at the end of the link
82 - the link text
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"))
87 (save-excursion
88 (goto-char (org-element-property :begin el))
89 (cond
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)
95 (match-end 0)
96 (save-match-data
97 (org-link-unescape (match-string-no-properties 1)))
98 (or (and (match-end 3)
99 (match-string-no-properties 3))
100 "")))
101 ((looking-at org-plain-link-re)
102 (list (match-beginning 0)
103 (match-end 0)
104 (org-link-unescape (match-string-no-properties 0))
105 nil))
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
115 direction.
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))
120 (nblobs (abs n))
121 (skip-func (if forward-p 'skip-syntax-forward 'skip-syntax-backward))
122 skip-func-retval)
123 (while (/= nblobs 0)
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)))
135 ;;;###autoload
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
148 of a blob.
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."
154 (interactive "p")
155 (setq n (or n 1))
156 (cond
157 ((= n 0))
158 ((< n 0)
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
163 (goto-char end)
164 (or (org-link-edit--forward-blob n 'no-punctuation)
165 (user-error "Not enough blobs after the link"))
166 (point)))
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))
176 (goto-char beg)
177 slurped)))))
179 ;;;###autoload
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."
196 (interactive "p")
197 (setq n (or n 1))
198 (cond
199 ((= n 0))
200 ((< n 0)
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
205 (goto-char beg)
206 (or (org-link-edit--forward-blob (- n))
207 (user-error "Not enough blobs before the link"))
208 (point)))
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))
218 (goto-char beg)
219 slurped)))))
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,
226 'other' is nil."
227 (when (< n 0) (user-error "N cannot be negative"))
228 (with-temp-buffer
229 (insert string)
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))
235 (and within-bound
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
243 N, 'other' is nil."
244 (when (< n 0) (user-error "N cannot be negative"))
245 (with-temp-buffer
246 (insert string)
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)))))))
255 ;;;###autoload
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."
272 (interactive "p")
273 (setq n (or n 1))
274 (cond
275 ((= n 0))
276 ((< n 0)
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
283 desc n)))
284 (unless new-desc (user-error "Not enough blobs in description"))
285 (goto-char beg)
286 (delete-region beg end)
287 (insert (org-make-link-string link new-desc))
288 (when (string= new-desc "")
289 (setq barfed (concat " " barfed)))
290 (insert barfed)
291 (goto-char beg)
292 barfed)))))
294 ;;;###autoload
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."
311 (interactive "p")
312 (setq n (or n 1))
313 (cond
314 ((= n 0))
315 ((< n 0)
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
322 desc n)))
323 (unless new-desc (user-error "Not enough blobs in description"))
324 (goto-char beg)
325 (delete-region beg end)
326 (insert (org-make-link-string link new-desc))
327 (when (string= new-desc "")
328 (setq barfed (concat barfed " ")))
329 (goto-char beg)
330 (insert barfed)
331 barfed)))))
333 (defun org-link-edit--next-link-data (&optional previous)
334 (save-excursion
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")))))
340 ;;;###autoload
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
348 the next link.
350 Non-interactively, use the text between BEG and END as the
351 description, moving the next (or previous) link relative BEG and
352 END."
353 (interactive (cons current-prefix-arg
354 (and (use-region-p)
355 (list (region-beginning) (region-end)))))
356 (let ((pt (point))
357 (desc-bounds (cond
358 ((and beg end)
359 (cons (progn (goto-char beg)
360 (point-marker))
361 (progn (goto-char end)
362 (point-marker))))
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
383 link
384 (if desc-bounds
385 (delete-and-extract-region (car desc-bounds)
386 (cdr desc-bounds))
387 orig-desc))))))
389 (provide 'org-link-edit)
390 ;;; org-link-edit.el ends here