aboutsummaryrefslogtreecommitdiffstats
path: root/lisp/package/package-describe.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/package/package-describe.el')
-rw-r--r--lisp/package/package-describe.el419
1 files changed, 419 insertions, 0 deletions
diff --git a/lisp/package/package-describe.el b/lisp/package/package-describe.el
new file mode 100644
index 00000000000..15a7f78ffaf
--- /dev/null
+++ b/lisp/package/package-describe.el
@@ -0,0 +1,419 @@
1;;; package-describe.el --- Help buffer for packages -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2025 Philip Kaludercic
4
5;; Author: Philip Kaludercic <philipk@posteo.net>
6
7;; This program is free software; you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation, either version 3 of the License, or
10;; (at your option) any later version.
11
12;; This program is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;; GNU General Public License for more details.
16
17;; You should have received a copy of the GNU General Public License
18;; along with this program. If not, see <https://www.gnu.org/licenses/>.
19
20;;; Commentary:
21
22;;; Code:
23
24(require 'package-core)
25(require 'package-elpa)
26(require 'package-misc)
27(require 'package-install)
28
29(require 'browse-url)
30(require 'lisp-mnt)
31
32(defface package-help-section-name
33 '((t :inherit (bold font-lock-function-name-face)))
34 "Face used on section names in package description buffers."
35 :version "25.1"
36 :group 'package)
37
38(defun package--print-help-section (name &rest strings)
39 "Print \"NAME: \", right aligned to the 13th column.
40If more STRINGS are provided, insert them followed by a newline.
41Otherwise no newline is inserted."
42 (declare (indent 1))
43 (insert (make-string (max 0 (- 11 (string-width name))) ?\s)
44 (propertize (concat name ": ") 'font-lock-face 'package-help-section-name))
45 (when strings
46 (apply #'insert strings)
47 (insert "\n")))
48
49(defun package--get-description (desc)
50 "Return a string containing the long description of the package DESC.
51The description is read from the installed package files."
52 ;; Installed packages have nil for kind, so we look for README
53 ;; first, then fall back to the Commentary header.
54
55 ;; We don’t include README.md here, because that is often the home
56 ;; page on a site like github, and not suitable as the package long
57 ;; description.
58 (let ((files '("README-elpa" "README-elpa.md" "README" "README.rst" "README.org"))
59 file
60 (srcdir (package-desc-dir desc))
61 result)
62 (while (and files
63 (not result))
64 (setq file (pop files))
65 (when (file-readable-p (expand-file-name file srcdir))
66 ;; Found a README.
67 (with-temp-buffer
68 (insert-file-contents (expand-file-name file srcdir))
69 (setq result (buffer-string)))))
70
71 (or
72 result
73
74 ;; Look for Commentary header.
75 (lm-commentary (expand-file-name
76 (format "%s.el" (package-desc-name desc)) srcdir))
77 "")))
78
79(defun package--describe-add-library-links ()
80 "Add links to library names in package description."
81 (while (re-search-forward "\\<\\([-[:alnum:]]+\\.el\\)\\>" nil t)
82 (if (locate-library (match-string 1))
83 (make-text-button (match-beginning 1) (match-end 1)
84 'xref (match-string-no-properties 1)
85 'help-echo "Read this file's commentary"
86 :type 'package--finder-xref))))
87
88(defun package-install-button-action (button)
89 "Run `package-install' on the package BUTTON points to.
90Used for the `action' property of buttons in the buffer created by
91`describe-package'."
92 (let ((pkg-desc (button-get button 'package-desc)))
93 (when (y-or-n-p (format-message "Install package `%s'? "
94 (package-desc-full-name pkg-desc)))
95 (package-install pkg-desc nil)
96 (describe-package (package-desc-name pkg-desc)))))
97
98(defun package-delete-button-action (button)
99 "Run `package-delete' on the package BUTTON points to.
100Used for the `action' property of buttons in the buffer created by
101`describe-package'."
102 (let ((pkg-desc (button-get button 'package-desc)))
103 (when (y-or-n-p (format-message "Delete package `%s'? "
104 (package-desc-full-name pkg-desc)))
105 (package-delete pkg-desc)
106 (describe-package (package-desc-name pkg-desc)))))
107
108(defun package-keyword-button-action (button)
109 "Show filtered \"*Packages*\" buffer for BUTTON.
110The buffer is filtered by the `package-keyword' property of BUTTON.
111Used for the `action' property of buttons in the buffer created by
112`describe-package'."
113 (let ((pkg-keyword (button-get button 'package-keyword)))
114 (package-show-package-list t (list pkg-keyword))))
115
116(defun package-make-button (text &rest properties)
117 "Insert button labeled TEXT with button PROPERTIES at point.
118PROPERTIES are passed to `insert-text-button', for which this
119function is a convenience wrapper used by `describe-package-1'."
120 (let ((button-text (if (display-graphic-p) text (concat "[" text "]")))
121 (button-face (if (display-graphic-p)
122 (progn
123 (require 'cus-edit) ; for the custom-button face
124 'custom-button)
125 'link)))
126 (apply #'insert-text-button button-text 'face button-face 'follow-link t
127 properties)))
128
129(defun package--finder-goto-xref (button)
130 "Jump to a Lisp file for the BUTTON at point."
131 (let* ((file (button-get button 'xref))
132 (lib (locate-library file)))
133 (if lib (finder-commentary lib)
134 (message "Unable to locate `%s'" file))))
135
136(define-button-type 'package--finder-xref 'action #'package--finder-goto-xref)
137
138(defun describe-package-1 (pkg)
139 "Insert the package description for PKG.
140Helper function for `describe-package'."
141 (require 'lisp-mnt)
142 (let* ((desc (or
143 (if (package-desc-p pkg) pkg)
144 (cadr (assq pkg package-alist))
145 (let ((built-in (assq pkg package--builtins)))
146 (if built-in
147 (package--from-builtin built-in)
148 (cadr (assq pkg package-archive-contents))))))
149 (name (if desc (package-desc-name desc) pkg))
150 (pkg-dir (if desc (package-desc-dir desc)))
151 (reqs (if desc (package-desc-reqs desc)))
152 (required-by (if desc (package--used-elsewhere-p desc nil 'all)))
153 (version (if desc (package-desc-version desc)))
154 (archive (if desc (package-desc-archive desc)))
155 (extras (and desc (package-desc-extras desc)))
156 (website (cdr (assoc :url extras)))
157 (commit (cdr (assoc :commit extras)))
158 (keywords (if desc (package-desc--keywords desc)))
159 (built-in (eq pkg-dir 'builtin))
160 (installable (and archive (not built-in)))
161 (status (if desc (package-desc-status desc) "orphan"))
162 (incompatible-reason (package--incompatible-p desc))
163 (signed (if desc (package-desc-signed desc)))
164 (maintainers (or (cdr (assoc :maintainer extras))
165 (cdr (assoc :maintainers extras))))
166 (authors (cdr (assoc :authors extras)))
167 (news (and-let* (pkg-dir
168 ((not built-in))
169 (file (expand-file-name "news" pkg-dir))
170 ((file-regular-p file))
171 ((file-readable-p file)))
172 file)))
173 (when (string= status "avail-obso")
174 (setq status "available obsolete"))
175 (when incompatible-reason
176 (setq status "incompatible"))
177 (princ (format "Package %S is %s.\n\n" name status))
178
179 ;; TODO: Remove the string decorations and reformat the strings
180 ;; for future l10n.
181 (package--print-help-section "Status")
182 (cond (built-in
183 (insert (propertize (capitalize status)
184 'font-lock-face 'package-status-built-in)
185 "."))
186 (pkg-dir
187 (insert (propertize (if (member status '("unsigned" "dependency"))
188 "Installed"
189 (capitalize status))
190 'font-lock-face 'package-status-built-in))
191 (insert (substitute-command-keys " in `"))
192 (let ((dir (abbreviate-file-name
193 (file-name-as-directory
194 (if (file-in-directory-p pkg-dir package-user-dir)
195 (file-relative-name pkg-dir package-user-dir)
196 pkg-dir)))))
197 (help-insert-xref-button dir 'help-package-def pkg-dir))
198 (if (and (package-built-in-p name)
199 (not (package-built-in-p name version)))
200 (insert (substitute-command-keys
201 "',\n shadowing a ")
202 (propertize "built-in package"
203 'font-lock-face 'package-status-built-in))
204 (insert (substitute-quotes "'")))
205 (if signed
206 (insert ".")
207 (insert " (unsigned)."))
208 (when (and (package-desc-p desc)
209 (not required-by)
210 (member status '("unsigned" "installed")))
211 (insert " ")
212 (package-make-button "Delete"
213 'action #'package-delete-button-action
214 'package-desc desc)))
215 (incompatible-reason
216 (insert (propertize "Incompatible" 'font-lock-face 'font-lock-warning-face)
217 " because it depends on ")
218 (if (stringp incompatible-reason)
219 (insert "Emacs " incompatible-reason ".")
220 (insert "uninstallable packages.")))
221 (installable
222 (insert (capitalize status))
223 (insert " from " (format "%s" archive))
224 (insert " -- ")
225 (package-make-button
226 "Install"
227 'action 'package-install-button-action
228 'package-desc desc))
229 (t (insert (capitalize status) ".")))
230 (insert "\n")
231 (unless (and pkg-dir (not archive)) ; Installed pkgs don't have archive.
232 (package--print-help-section "Archive"
233 (or archive "n/a")))
234 (and version
235 (package--print-help-section "Version"
236 (package-version-join version)))
237 (when commit
238 (package--print-help-section "Commit" commit))
239 (when desc
240 (package--print-help-section "Summary"
241 (package-desc-summary desc)))
242
243 (setq reqs (if desc (package-desc-reqs desc)))
244 (when reqs
245 (package--print-help-section "Requires")
246 (let ((first t))
247 (dolist (req reqs)
248 (let* ((name (car req))
249 (vers (cadr req))
250 (text (format "%s-%s" (symbol-name name)
251 (package-version-join vers)))
252 (reason (if (and (listp incompatible-reason)
253 (assq name incompatible-reason))
254 " (not available)" "")))
255 (cond (first (setq first nil))
256 ((>= (+ 2 (current-column) (length text) (length reason))
257 (window-width))
258 (insert ",\n "))
259 (t (insert ", ")))
260 (help-insert-xref-button text 'help-package name)
261 (insert reason)))
262 (insert "\n")))
263 (when required-by
264 (package--print-help-section "Required by")
265 (let ((first t))
266 (dolist (pkg required-by)
267 (let ((text (package-desc-full-name pkg)))
268 (cond (first (setq first nil))
269 ((>= (+ 2 (current-column) (length text))
270 (window-width))
271 (insert ",\n "))
272 (t (insert ", ")))
273 (help-insert-xref-button text 'help-package
274 (package-desc-name pkg))))
275 (insert "\n")))
276 (when website
277 ;; Prefer https for the website of packages on common domains.
278 (when (string-match-p (rx bol "http://" (or "elpa." "www." "git." "")
279 (or "nongnu.org" "gnu.org" "sr.ht"
280 "emacswiki.org" "gitlab.com" "github.com")
281 "/")
282 website)
283 ;; But only if the user has "https" in `package-archives'.
284 (let ((gnu (cdr (assoc "gnu" package-archives))))
285 (and gnu (string-match-p "^https" gnu)
286 (setq website
287 (replace-regexp-in-string "^http" "https" website)))))
288 (package--print-help-section "Website")
289 (help-insert-xref-button website 'help-url website)
290 (insert "\n"))
291 (when keywords
292 (package--print-help-section "Keywords")
293 (dolist (k keywords)
294 (package-make-button
295 k
296 'package-keyword k
297 'action 'package-keyword-button-action)
298 (insert " "))
299 (insert "\n"))
300 (when maintainers
301 (unless (and (listp (car maintainers)) (listp (cdr maintainers)))
302 (setq maintainers (list maintainers)))
303 (package--print-help-section
304 (if (cdr maintainers) "Maintainers" "Maintainer"))
305 (dolist (maintainer maintainers)
306 (when (bolp)
307 (insert (make-string 13 ?\s)))
308 (package--print-email-button maintainer)))
309 (when authors
310 (package--print-help-section (if (cdr authors) "Authors" "Author"))
311 (dolist (author authors)
312 (when (bolp)
313 (insert (make-string 13 ?\s)))
314 (package--print-email-button author)))
315 (let* ((all-pkgs (append (cdr (assq name package-alist))
316 (cdr (assq name package-archive-contents))
317 (let ((bi (assq name package--builtins)))
318 (if bi (list (package--from-builtin bi))))))
319 (other-pkgs (delete desc all-pkgs)))
320 (when other-pkgs
321 (package--print-help-section "Other versions"
322 (mapconcat (lambda (opkg)
323 (let* ((ov (package-desc-version opkg))
324 (dir (package-desc-dir opkg))
325 (from (or (package-desc-archive opkg)
326 (if (stringp dir) "installed" dir))))
327 (if (not ov) (format "%s" from)
328 (format "%s (%s)"
329 (make-text-button (package-version-join ov) nil
330 'font-lock-face 'link
331 'follow-link t
332 'action
333 (lambda (_button)
334 (describe-package opkg)))
335 from))))
336 other-pkgs ", ")
337 ".")))
338
339 (insert "\n")
340
341 (let ((start-of-description (point)))
342 (if built-in
343 ;; For built-in packages, get the description from the
344 ;; Commentary header.
345 (insert (or (lm-commentary (locate-file (format "%s.el" name)
346 load-path
347 load-file-rep-suffixes))
348 ""))
349
350 (if (package-installed-p desc)
351 ;; For installed packages, get the description from the
352 ;; installed files.
353 (insert (package--get-description desc))
354
355 ;; For non-built-in, non-installed packages, get description from
356 ;; the archive.
357 (let* ((basename (format "%s-readme.txt" name))
358 readme-string)
359
360 (package--with-response-buffer (package-archive-base desc)
361 :file basename :noerror t
362 (save-excursion
363 (goto-char (point-max))
364 (unless (bolp)
365 (insert ?\n)))
366 (cl-assert (not enable-multibyte-characters))
367 (setq readme-string
368 ;; The readme.txt files are defined to contain utf-8 text.
369 (decode-coding-region (point-min) (point-max) 'utf-8 t))
370 t)
371 (insert (or readme-string
372 "This package does not provide a description.")))))
373
374 ;; Insert news if available.
375 (when news
376 (insert "\n" (make-separator-line) "\n"
377 (propertize "* News" 'face 'package-help-section-name)
378 "\n\n")
379 (insert-file-contents news))
380
381 ;; Make library descriptions into links.
382 (goto-char start-of-description)
383 (package--describe-add-library-links)
384 ;; Make URLs in the description into links.
385 (goto-char start-of-description)
386 (browse-url-add-buttons))))
387
388;;;###autoload
389(defun describe-package (package)
390 "Display the full documentation of PACKAGE (a symbol)."
391 (interactive
392 (let* ((guess (or (function-called-at-point)
393 (symbol-at-point))))
394 (require 'finder-inf nil t)
395 ;; Load the package list if necessary (but don't activate them).
396 (unless package--initialized
397 (package-initialize t))
398 (let ((packages (append (mapcar #'car package-alist)
399 (mapcar #'car package-archive-contents)
400 (mapcar #'car package--builtins))))
401 (unless (memq guess packages)
402 (setq guess nil))
403 (setq packages (mapcar #'symbol-name packages))
404 (let ((val
405 (completing-read (format-prompt "Describe package" guess)
406 packages nil t nil nil (when guess
407 (symbol-name guess)))))
408 (list (and (> (length val) 0) (intern val)))))))
409 (if (not (or (package-desc-p package) (and package (symbolp package))))
410 (message "No package specified")
411 (help-setup-xref (list #'describe-package package)
412 (called-interactively-p 'interactive))
413 (with-help-window (help-buffer)
414 (with-current-buffer standard-output
415 (describe-package-1 package)))))
416
417
418(provide 'package-describe)
419;;; package-describe.el ends here