summaryrefslogtreecommitdiff
path: root/helm-info.el
blob: c484ab6c1aa77796fd7f569d637e363e10263870 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
;;; helm-info.el --- Browse info index with helm -*- lexical-binding: t -*-

;; Copyright (C) 2012 ~ 2019 Thierry Volpiatto <thierry.volpiatto@gmail.com>

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Code:

(require 'cl-lib)
(require 'helm)
(require 'helm-lib)
(require 'helm-utils)
(require 'info)

(declare-function Info-index-nodes "info" (&optional file))
(declare-function Info-goto-node "info" (&optional fork))
(declare-function Info-find-node "info.el" (filename nodename &optional no-going-back))
(defvar Info-history)
(defvar Info-directory-list)

;;; Customize

(defgroup helm-info nil
  "Info-related applications and libraries for Helm."
  :group 'helm)

(defcustom helm-info-default-sources
  '(helm-source-info-elisp
    helm-source-info-cl
    helm-source-info-eieio
    helm-source-info-pages)
  "Default sources to use for looking up symbols at point in Info
files with `helm-info-at-point'."
  :group 'helm-info
  :type '(repeat (choice symbol)))

;;; Build info-index sources with `helm-info-source' class.

(cl-defun helm-info-init (&optional (file (helm-attr 'info-file)))
  ;; Allow reinit candidate buffer when using edebug.
  (helm-aif (and debug-on-error
                 (helm-candidate-buffer))
      (kill-buffer it))
  (unless (helm-candidate-buffer)
    (save-selected-window
      (info file " *helm info temp buffer*")
      (let ((tobuf (helm-candidate-buffer 'global))
            Info-history
            start end line)
        (cl-dolist (node (Info-index-nodes))
          (Info-goto-node node)
          (goto-char (point-min))
          (while (search-forward "\n* " nil t)
            (unless (search-forward "Menu:\n" (1+ (point-at-eol)) t)
              (setq start (point-at-bol)
                    ;; Fix issue #1503 by getting the invisible
                    ;; info displayed on next line in long strings.
                    ;; e.g "* Foo.\n   (line 12)" instead of
                    ;;     "* Foo.(line 12)"
                    end (or (save-excursion
                              (goto-char (point-at-bol))
                              (re-search-forward "(line +[0-9]+)" nil t))
                            (point-at-eol))
                    ;; Long string have a new line inserted before the
                    ;; invisible spec, remove it.
                    line (replace-regexp-in-string
                          "\n" "" (buffer-substring start end)))
              (with-current-buffer tobuf
                (insert line)
                (insert "\n")))))
        (bury-buffer)))))

(defun helm-info-goto (node-line)
  (Info-goto-node (car node-line))
  (helm-goto-line (cdr node-line)))

(defun helm-info-display-to-real (line)
  (and (string-match
        ;; This regexp is stolen from Info-apropos-matches
        "\\* +\\([^\n]*.+[^\n]*\\):[ \t]+\\([^\n]*\\)\\.\\(?:[ \t\n]*(line +\\([0-9]+\\))\\)?" line)
       (cons (format "(%s)%s" (helm-attr 'info-file) (match-string 2 line))
             (string-to-number (or (match-string 3 line) "1")))))

(defclass helm-info-source (helm-source-in-buffer)
  ((info-file :initarg :info-file
              :initform nil
              :custom 'string)
   (init :initform #'helm-info-init)
   (display-to-real :initform #'helm-info-display-to-real)
   (get-line :initform #'buffer-substring)
   (action :initform '(("Goto node" . helm-info-goto)))))

(defmacro helm-build-info-source (fname &rest args)
  `(helm-make-source (concat "Info Index: " ,fname) 'helm-info-source
     :info-file ,fname ,@args))

(defun helm-build-info-index-command (name doc source buffer)
  "Define a helm command NAME with documentation DOC.
Arg SOURCE will be an existing helm source named
`helm-source-info-<NAME>' and BUFFER a string buffer name."
  (defalias (intern (concat "helm-info-" name))
      (lambda ()
        (interactive)
        (helm :sources source
              :buffer buffer
              :candidate-number-limit 1000))
    doc))

(defun helm-define-info-index-sources (var-value &optional commands)
  "Define helm sources named helm-source-info-<NAME>.
Sources are generated for all entries of `helm-default-info-index-list'.
If COMMANDS arg is non-nil, also build commands named `helm-info-<NAME>'.
Where NAME is an element of `helm-default-info-index-list'."
  (cl-loop for str in var-value
           for sym = (intern (concat "helm-source-info-" str))
           do (set sym (helm-build-info-source str))
           when commands
           do (helm-build-info-index-command
               str (format "Predefined helm for %s info." str)
               sym (format "*helm info %s*" str))))

(defun helm-info-index-set (var value)
  (set var value)
  (helm-define-info-index-sources value t))

;;; Search Info files

;; `helm-info' is the main entry point here. It prompts the user for an Info
;; file, then a term in the file's index to jump to.

(defvar helm-info-searched (make-ring 32)
  "Ring of previously searched Info files.")

(defun helm-get-info-files ()
  "Return list of Info files to use for `helm-info'.

Elements of the list are strings of Info file names without
extensions (e.g. \"emacs\" for file \"emacs.info.gz\"). Info
files are found by searching directories in
`Info-directory-list'."
  (info-initialize) ; Build Info-directory-list from INFOPATH (Issue #2118)
  (let ((files (cl-loop for d in (or Info-directory-list
                                     Info-default-directory-list)
                        when (file-directory-p d)
                        append (directory-files d nil "\\.info"))))
    (helm-fast-remove-dups
     (cl-loop for f in files collect
              (helm-file-name-sans-extension f))
     :test 'equal)))

(defcustom helm-default-info-index-list
  (helm-get-info-files)
  "Info files to search in with `helm-info'."
  :group 'helm-info
  :type  '(repeat (choice string))
  :set   'helm-info-index-set)

(defun helm-info-search-index (candidate)
  "Search the index of CANDIDATE's Info file using the function
helm-info-<CANDIDATE>."
  (let ((helm-info-function
         (intern-soft (concat "helm-info-" candidate))))
    (when (fboundp helm-info-function)
      (funcall helm-info-function)
      (ring-insert helm-info-searched candidate))))

(defun helm-def-source--info-files ()
  "Return a `helm' source for Info files."
  (helm-build-sync-source "Helm Info"
    :candidates
    (lambda () (copy-sequence helm-default-info-index-list))
    :candidate-number-limit 999
    :candidate-transformer
    (lambda (candidates)
      (sort candidates #'string-lessp))
    :nomark t
    :action '(("Search index" . helm-info-search-index))))

;;;###autoload
(defun helm-info (&optional refresh)
  "Preconfigured `helm' for searching Info files' indices.

With a prefix argument \\[universal-argument], set REFRESH to non-nil.

Optional parameter REFRESH, when non-nil, reevaluates
`helm-default-info-index-list'.  If the variable has been
customized, set it to its saved value.  If not, set it to its
standard value.  See `custom-reevaluate-setting' for more.

REFRESH is useful when new Info files are installed.  If
`helm-default-info-index-list' has not been customized, the new
Info files are made available."
  (interactive "P")
  (let ((default (unless (ring-empty-p helm-info-searched)
                   (ring-ref helm-info-searched 0))))
    (when refresh
      (custom-reevaluate-setting 'helm-default-info-index-list))
    (helm :sources (helm-def-source--info-files)
          :buffer "*helm Info*"
          :preselect (and default
                          (concat "\\_<" (regexp-quote default) "\\_>")))))

;;;; Info at point

;; `helm-info-at-point' is the main entry point here. It searches for the
;; symbol at point through the Info sources defined in
;; `helm-info-default-sources' and jumps to it.

(defvar helm-info--pages-cache nil
  "Cache for all Info pages on the system.")

(defvar helm-source-info-pages
  (helm-build-sync-source "Info Pages"
    :init #'helm-info-pages-init
    :candidates (lambda () helm-info--pages-cache)
    :action '(("Show with Info" .
               (lambda (node-str)
                 (info (replace-regexp-in-string
                        "^[^:]+: " "" node-str)))))
    :requires-pattern 2)
  "Helm source for Info pages.")

(defun helm-info-pages-init ()
  "Collect candidates for initial Info node Top."
  (or helm-info--pages-cache
      (let ((info-topic-regexp "\\* +\\([^:]+: ([^)]+)[^.]*\\)\\."))
        (save-selected-window
          (info "dir" " *helm info temp buffer*")
          (Info-find-node "dir" "top")
          (goto-char (point-min))
          (while (re-search-forward info-topic-regexp nil t)
            (push (match-string-no-properties 1)
                  helm-info--pages-cache))
          (kill-buffer)))))

;;;###autoload
(defun helm-info-at-point ()
  "Preconfigured `helm' for searching info at point."
  (interactive)
  (cl-loop for src in helm-info-default-sources
           for name = (if (symbolp src)
                          (assoc 'name (symbol-value src))
                        (assoc 'name src))
           unless name
           do (warn "Couldn't build source `%S' without its info file" src))
  (helm :sources helm-info-default-sources
        :buffer "*helm info*"))

(provide 'helm-info)

;; Local Variables:
;; byte-compile-warnings: (not obsolete)
;; coding: utf-8
;; indent-tabs-mode: nil
;; End:

;;; helm-info.el ends here