*** empty log message ***
[gnus] / lisp / gnus-topic.el
1 ;;; gnus-topic.el --- a folding minor mode for Gnus group buffers
2 ;; Copyright (C) 1995,96 Free Software Foundation, Inc.
3
4 ;; Author: Ilja Weis <kult@uni-paderborn.de>
5 ;;      Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;;; Code:
28
29 (require 'gnus-load)
30 (require 'gnus-group)
31 (require 'gnus-start)
32 (require 'gnus)
33
34 (defvar gnus-topic-mode nil
35   "Minor mode for Gnus group buffers.")
36
37 (defvar gnus-topic-mode-hook nil
38   "Hook run in topic mode buffers.")
39
40 (defvar gnus-topic-line-format "%i[ %(%{%n%}%) -- %A ]%v\n"
41   "Format of topic lines.
42 It works along the same lines as a normal formatting string,
43 with some simple extensions.
44
45 %i  Indentation based on topic level.
46 %n  Topic name.
47 %v  Nothing if the topic is visible, \"...\" otherwise.
48 %g  Number of groups in the topic.
49 %a  Number of unread articles in the groups in the topic.
50 %A  Number of unread articles in the groups in the topic and its subtopics.
51 ")
52
53 (defvar gnus-topic-indent-level 2
54   "*How much each subtopic should be indented.")
55
56 ;; Internal variables.
57
58 (defvar gnus-topic-active-topology nil)
59 (defvar gnus-topic-active-alist nil)
60
61 (defvar gnus-topology-checked-p nil
62   "Whether the topology has been checked in this session.")
63
64 (defvar gnus-topic-killed-topics nil)
65 (defvar gnus-topic-inhibit-change-level nil)
66 (defvar gnus-topic-tallied-groups nil)
67
68 (defconst gnus-topic-line-format-alist
69   `((?n name ?s)
70     (?v visible ?s)
71     (?i indentation ?s)
72     (?g number-of-groups ?d)
73     (?a (gnus-topic-articles-in-topic entries) ?d)
74     (?A total-number-of-articles ?d)
75     (?l level ?d)))
76
77 (defvar gnus-topic-line-format-spec nil)
78
79 ;;; Utility functions
80
81 (defun gnus-group-topic-name ()
82   "The name of the topic on the current line."
83   (let ((topic (get-text-property (gnus-point-at-bol) 'gnus-topic)))
84     (and topic (symbol-name topic))))
85
86 (defun gnus-group-topic-level ()
87   "The level of the topic on the current line."
88   (get-text-property (gnus-point-at-bol) 'gnus-topic-level))
89
90 (defun gnus-group-topic-unread ()
91   "The number of unread articles in topic on the current line."
92   (get-text-property (gnus-point-at-bol) 'gnus-topic-unread))
93
94 (defun gnus-topic-unread (topic)
95   "Return the number of unread articles in TOPIC."
96   (or (save-excursion
97         (and (gnus-topic-goto-topic topic)
98              (gnus-group-topic-unread)))
99       0))
100
101 (defun gnus-group-topic-p ()
102   "Return non-nil if the current line is a topic."
103   (gnus-group-topic-name))
104
105 (defun gnus-topic-visible-p ()
106   "Return non-nil if the current topic is visible."
107   (get-text-property (gnus-point-at-bol) 'gnus-topic-visible))
108
109 (defun gnus-topic-articles-in-topic (entries)
110   (let ((total 0)
111         number)
112     (while entries
113       (when (numberp (setq number (car (pop entries))))
114         (incf total number)))
115     total))
116
117 (defun gnus-group-topic (group)
118   "Return the topic GROUP is a member of."
119   (let ((alist gnus-topic-alist)
120         out)
121     (while alist
122       (when (member group (cdar alist))
123         (setq out (caar alist)
124               alist nil))
125       (setq alist (cdr alist)))
126     out))
127
128 (defun gnus-group-parent-topic (group)
129   "Return the topic GROUP is member of by looking at the group buffer."
130   (save-excursion
131     (set-buffer gnus-group-buffer)
132     (if (gnus-group-goto-group group)
133         (gnus-current-topic)
134       (gnus-group-topic group))))
135
136 (defun gnus-topic-goto-topic (topic)
137   "Go to TOPIC."
138   (when topic
139     (gnus-goto-char (text-property-any (point-min) (point-max)
140                                        'gnus-topic (intern topic)))))
141
142 (defun gnus-current-topic ()
143   "Return the name of the current topic."
144   (let ((result
145          (or (get-text-property (point) 'gnus-topic)
146              (save-excursion
147                (and (gnus-goto-char (previous-single-property-change
148                                      (point) 'gnus-topic))
149                     (get-text-property (max (1- (point)) (point-min))
150                                        'gnus-topic))))))
151     (when result
152       (symbol-name result))))
153
154 (defun gnus-current-topics ()
155   "Return a list of all current topics, lowest in hierarchy first."
156   (let ((topic (gnus-current-topic))
157         topics)
158     (while topic
159       (push topic topics)
160       (setq topic (gnus-topic-parent-topic topic)))
161     (nreverse topics)))
162
163 (defun gnus-group-active-topic-p ()
164   "Say whether the current topic comes from the active topics."
165   (save-excursion
166     (beginning-of-line)
167     (get-text-property (point) 'gnus-active)))
168
169 (defun gnus-topic-find-groups (topic &optional level all)
170   "Return entries for all visible groups in TOPIC."
171   (let ((groups (cdr (assoc topic gnus-topic-alist)))
172         info clevel unread group lowest params visible-groups entry active)
173     (setq lowest (or lowest 1))
174     (setq level (or level 7))
175     ;; We go through the newsrc to look for matches.
176     (while groups
177       (setq entry (gnus-gethash (setq group (pop groups)) gnus-newsrc-hashtb)
178             info (nth 2 entry)
179             params (gnus-info-params info)
180             active (gnus-active group)
181             unread (or (car entry)
182                        (and (not (equal group "dummy.group"))
183                             active
184                             (- (1+ (cdr active)) (car active))))
185             clevel (or (gnus-info-level info)
186                        (if (member group gnus-zombie-list) 8 9)))
187       (and 
188        unread                           ; nil means that the group is dead.
189        (<= clevel level) 
190        (>= clevel lowest)               ; Is inside the level we want.
191        (or all
192            (if (eq unread t)
193                gnus-group-list-inactive-groups
194              (> unread 0))
195            (and gnus-list-groups-with-ticked-articles
196                 (cdr (assq 'tick (gnus-info-marks info))))
197                                         ; Has right readedness.
198            ;; Check for permanent visibility.
199            (and gnus-permanently-visible-groups
200                 (string-match gnus-permanently-visible-groups group))
201            (memq 'visible params)
202            (cdr (assq 'visible params)))
203        ;; Add this group to the list of visible groups.
204        (push (or entry group) visible-groups)))
205     (nreverse visible-groups)))
206
207 (defun gnus-topic-previous-topic (topic)
208   "Return the previous topic on the same level as TOPIC."
209   (let ((top (cddr (gnus-topic-find-topology
210                     (gnus-topic-parent-topic topic)))))
211     (unless (equal topic (caaar top))
212       (while (and top (not (equal (caaadr top) topic)))
213         (setq top (cdr top)))
214       (caaar top))))
215
216 (defun gnus-topic-parent-topic (topic &optional topology)
217   "Return the parent of TOPIC."
218   (unless topology
219     (setq topology gnus-topic-topology))
220   (let ((parent (car (pop topology)))
221         result found)
222     (while (and topology
223                 (not (setq found (equal (caaar topology) topic)))
224                 (not (setq result (gnus-topic-parent-topic topic 
225                                                            (car topology)))))
226       (setq topology (cdr topology)))
227     (or result (and found parent))))
228
229 (defun gnus-topic-next-topic (topic &optional previous)
230   "Return the next sibling of TOPIC."
231   (let ((parentt (cddr (gnus-topic-find-topology 
232                         (gnus-topic-parent-topic topic))))
233         prev)
234     (while (and parentt
235                 (not (equal (caaar parentt) topic)))
236       (setq prev (caaar parentt)
237             parentt (cdr parentt)))
238     (if previous
239         prev
240       (caaadr parentt))))
241
242 (defun gnus-topic-find-topology (topic &optional topology level remove)
243   "Return the topology of TOPIC."
244   (unless topology
245     (setq topology gnus-topic-topology)
246     (setq level 0))
247   (let ((top topology)
248         result)
249     (if (equal (caar topology) topic)
250         (progn
251           (when remove
252             (delq topology remove))
253           (cons level topology))
254       (setq topology (cdr topology))
255       (while (and topology
256                   (not (setq result (gnus-topic-find-topology
257                                      topic (car topology) (1+ level)
258                                      (and remove top)))))
259         (setq topology (cdr topology)))
260       result)))
261
262 (defvar gnus-tmp-topics nil)
263 (defun gnus-topic-list (&optional topology)
264   "Return a list of all topics in the topology."
265   (unless topology
266     (setq topology gnus-topic-topology 
267           gnus-tmp-topics nil))
268   (push (caar topology) gnus-tmp-topics)
269   (mapcar 'gnus-topic-list (cdr topology))
270   gnus-tmp-topics)
271
272 ;;; Topic parameter jazz
273
274 (defun gnus-topic-parameters (topic)
275   "Return the parameters for TOPIC."
276   (let ((top (gnus-topic-find-topology topic)))
277     (unless top
278       (error "No such topic: %s" topic))
279     (nth 2 (car top))))
280
281 (defun gnus-topic-set-parameters (topic parameters)
282   "Set the topic parameters of TOPIC to PARAMETERS."
283   (let ((top (gnus-topic-find-topology topic)))
284     (unless top
285       (error "No such topic: %s" topic))
286     ;; We may have to extend if there is no parameters here
287     ;; to begin with.
288     (unless (nthcdr 2 (car top))
289       (nconc (car top) (list nil)))
290     (setcar (nthcdr 2 (car top)) parameters)))
291
292 (defun gnus-group-topic-parameters (group)
293   "Compute the group parameters for GROUP taking into account inheretance from topics."
294   (let ((params-list (list (gnus-group-get-parameter group)))
295         topics params param out)
296     (save-excursion
297       (gnus-group-goto-group group)
298       (setq topics (gnus-current-topics))
299       (while topics
300         (push (gnus-topic-parameters (pop topics)) params-list))
301       ;; We probably have lots of nil elements here, so
302       ;; we remove them.  Probably faster than doing this "properly".
303       (setq params-list (delq nil params-list))
304       ;; Now we have all the parameters, so we go through them
305       ;; and do inheretance in the obvious way.
306       (while (setq params (pop params-list))
307         (while (setq param (pop params))
308           (when (atom param)
309             (setq param (cons param t)))
310           ;; Override any old versions of this param.
311           (setq out (delq (assq (car param) out) out))
312           (push param out)))
313       ;; Return the resulting parameter list.
314       out)))
315
316 ;;; General utility funtions
317
318 (defun gnus-topic-enter-dribble ()
319   (gnus-dribble-enter
320    (format "(setq gnus-topic-topology '%S)" gnus-topic-topology)))
321
322 ;;; Generating group buffers
323
324 (defun gnus-group-prepare-topics (level &optional all lowest regexp list-topic topic-level)
325   "List all newsgroups with unread articles of level LEVEL or lower, and
326 use the `gnus-group-topics' to sort the groups.
327 If ALL is non-nil, list groups that have no unread articles.
328 If LOWEST is non-nil, list all newsgroups of level LOWEST or higher."
329   (set-buffer gnus-group-buffer)
330   (let ((buffer-read-only nil)
331         (lowest (or lowest 1)))
332
333     (setq gnus-topic-tallied-groups nil)
334
335     (when (or (not gnus-topic-alist)
336               (not gnus-topology-checked-p))
337       (gnus-topic-check-topology))
338
339     (unless list-topic 
340       (erase-buffer))
341     
342     ;; List dead groups?
343     (when (and (>= level gnus-level-zombie) (<= lowest gnus-level-zombie))
344       (gnus-group-prepare-flat-list-dead 
345        (setq gnus-zombie-list (sort gnus-zombie-list 'string<)) 
346        gnus-level-zombie ?Z
347        regexp))
348     
349     (when (and (>= level gnus-level-killed) (<= lowest gnus-level-killed))
350       (gnus-group-prepare-flat-list-dead 
351        (setq gnus-killed-list (sort gnus-killed-list 'string<))
352        gnus-level-killed ?K
353        regexp))
354
355     ;; Use topics.
356     (when (< lowest gnus-level-zombie)
357       (if list-topic
358           (let ((top (gnus-topic-find-topology list-topic)))
359             (gnus-topic-prepare-topic (cdr top) (car top)
360                                       (or topic-level level) all))
361         (gnus-topic-prepare-topic gnus-topic-topology 0
362                                   (or topic-level level) all))))
363
364   (gnus-group-set-mode-line)
365   (setq gnus-group-list-mode (cons level all))
366   (run-hooks 'gnus-group-prepare-hook))
367
368 (defun gnus-topic-prepare-topic (topicl level &optional list-level all silent)
369   "Insert TOPIC into the group buffer.
370 If SILENT, don't insert anything.  Return the number of unread
371 articles in the topic and its subtopics."
372   (let* ((type (pop topicl))
373          (entries (gnus-topic-find-groups (car type) list-level all))
374          (visiblep (and (eq (nth 1 type) 'visible) (not silent)))
375          (gnus-group-indentation 
376           (make-string (* gnus-topic-indent-level level) ? ))
377          (beg (progn (beginning-of-line) (point)))
378          (topicl (reverse topicl))
379          (all-entries entries)
380          (unread 0)
381          (topic (car type))
382          info entry end active)
383     ;; Insert any sub-topics.
384     (while topicl
385       (incf unread
386             (gnus-topic-prepare-topic 
387              (pop topicl) (1+ level) list-level all
388              (not visiblep))))
389     (setq end (point))
390     (goto-char beg)
391     ;; Insert all the groups that belong in this topic.
392     (while (setq entry (pop entries))
393       (when visiblep 
394         (if (stringp entry)
395             ;; Dead groups.
396             (gnus-group-insert-group-line
397              entry (if (member entry gnus-zombie-list) 8 9)
398              nil (- (1+ (cdr (setq active (gnus-active entry))))
399                     (car active)) nil)
400           ;; Living groups.
401           (when (setq info (nth 2 entry))
402             (gnus-group-insert-group-line 
403              (gnus-info-group info)
404              (gnus-info-level info) (gnus-info-marks info) 
405              (car entry) (gnus-info-method info)))))
406       (when (and (listp entry)
407                  (numberp (car entry))
408                  (not (member (gnus-info-group (setq info (nth 2 entry)))
409                               gnus-topic-tallied-groups)))
410         (push (gnus-info-group info) gnus-topic-tallied-groups)
411         (incf unread (car entry))))
412     (goto-char beg)
413     ;; Insert the topic line.
414     (unless silent
415       (gnus-extent-start-open (point))
416       (gnus-topic-insert-topic-line 
417        (car type) visiblep
418        (not (eq (nth 2 type) 'hidden))
419        level all-entries unread))
420     (goto-char end)
421     unread))
422
423 (defun gnus-topic-remove-topic (&optional insert total-remove hide in-level)
424   "Remove the current topic."
425   (let ((topic (gnus-group-topic-name))
426         (level (gnus-group-topic-level))
427         (beg (progn (beginning-of-line) (point)))
428         buffer-read-only)
429     (when topic
430       (while (and (zerop (forward-line 1))
431                   (> (or (gnus-group-topic-level) (1+ level)) level)))
432       (delete-region beg (point))
433       (setcar (cdadr (gnus-topic-find-topology topic))
434               (if insert 'visible 'invisible))
435       (when hide
436         (setcdr (cdadr (gnus-topic-find-topology topic))
437                 (list hide)))
438       (unless total-remove
439         (gnus-topic-insert-topic topic in-level)))))
440
441 (defun gnus-topic-insert-topic (topic &optional level)
442   "Insert TOPIC."
443   (gnus-group-prepare-topics 
444    (car gnus-group-list-mode) (cdr gnus-group-list-mode)
445    nil nil topic level))
446   
447 (defun gnus-topic-fold (&optional insert)
448   "Remove/insert the current topic."
449   (let ((topic (gnus-group-topic-name))) 
450     (when topic
451       (save-excursion
452         (if (not (gnus-group-active-topic-p))
453             (gnus-topic-remove-topic
454              (or insert (not (gnus-topic-visible-p))))
455           (let ((gnus-topic-topology gnus-topic-active-topology)
456                 (gnus-topic-alist gnus-topic-active-alist)
457                 (gnus-group-list-mode (cons 5 t)))
458             (gnus-topic-remove-topic
459              (or insert (not (gnus-topic-visible-p))) nil nil 9)))))))
460
461 (defun gnus-topic-insert-topic-line (name visiblep shownp level entries 
462                                           &optional unread)
463   (let* ((visible (if visiblep "" "..."))
464          (indentation (make-string (* gnus-topic-indent-level level) ? ))
465          (total-number-of-articles unread)
466          (number-of-groups (length entries))
467          (active-topic (eq gnus-topic-alist gnus-topic-active-alist)))
468     (beginning-of-line)
469     ;; Insert the text.
470     (gnus-add-text-properties 
471      (point)
472      (prog1 (1+ (point)) 
473        (eval gnus-topic-line-format-spec)
474        (gnus-topic-remove-excess-properties)1)
475      (list 'gnus-topic (intern name)
476            'gnus-topic-level level
477            'gnus-topic-unread unread
478            'gnus-active active-topic
479            'gnus-topic-visible visiblep))))
480
481 (defun gnus-topic-update-topic ()
482   "Update all parent topics to the current group."
483   (when (and (eq major-mode 'gnus-group-mode)
484              gnus-topic-mode)
485     (let ((group (gnus-group-group-name))
486           (buffer-read-only nil))
487       (when (and group (gnus-get-info group)
488                  (gnus-topic-goto-topic (gnus-current-topic)))
489         (gnus-topic-update-topic-line (gnus-group-topic-name))
490         (gnus-group-goto-group group)
491         (gnus-group-position-point)))))
492
493 (defun gnus-topic-goto-missing-group (group) 
494   "Place point where GROUP is supposed to be inserted."
495   (let* ((topic (gnus-group-topic group))
496          (groups (cdr (assoc topic gnus-topic-alist)))
497          (g (cdr (member group groups)))
498          (unfound t))
499     (while (and g unfound)
500       (when (gnus-group-goto-group (pop g))
501         (beginning-of-line)
502         (setq unfound nil)))
503     (when unfound
504       (setq g (cdr (member group (reverse groups))))
505       (while (and g unfound)
506         (when (gnus-group-goto-group (pop g))
507           (forward-line 1)
508           (setq unfound nil)))
509       (when unfound
510         (gnus-topic-goto-topic topic)
511         (forward-line 1)))))
512
513 (defun gnus-topic-update-topic-line (topic-name &optional reads)
514   (let* ((top (gnus-topic-find-topology topic-name))
515          (type (cadr top))
516          (children (cddr top))
517          (entries (gnus-topic-find-groups 
518                    (car type) (car gnus-group-list-mode)
519                    (cdr gnus-group-list-mode)))
520          (parent (gnus-topic-parent-topic topic-name))
521          (all-entries entries)
522          (unread 0)
523          old-unread entry)
524     (when (gnus-topic-goto-topic (car type))
525       ;; Tally all the groups that belong in this topic.
526       (if reads
527           (setq unread (- (gnus-group-topic-unread) reads))
528         (while children
529           (incf unread (gnus-topic-unread (caar (pop children)))))
530         (while (setq entry (pop entries))
531           (when (numberp (car entry))
532             (incf unread (car entry)))))
533       (setq old-unread (gnus-group-topic-unread))
534       ;; Insert the topic line.
535       (gnus-topic-insert-topic-line 
536        (car type) (gnus-topic-visible-p)
537        (not (eq (nth 2 type) 'hidden))
538        (gnus-group-topic-level) all-entries unread)
539       (gnus-delete-line))
540     (when parent
541       (forward-line -1)
542       (gnus-topic-update-topic-line
543        parent (- old-unread (gnus-group-topic-unread))))
544     unread))
545
546 (defun gnus-topic-group-indentation ()
547   (make-string 
548    (* gnus-topic-indent-level
549       (or (save-excursion
550             (gnus-topic-goto-topic (gnus-current-topic))
551             (gnus-group-topic-level)) 0)) ? ))
552
553 ;;; Initialization
554
555 (gnus-add-shutdown 'gnus-topic-close 'gnus)
556
557 (defun gnus-topic-close ()
558   (setq gnus-topic-active-topology nil
559         gnus-topic-active-alist nil
560         gnus-topic-killed-topics nil
561         gnus-topic-tallied-groups nil
562         gnus-topology-checked-p nil))
563
564 (defun gnus-topic-check-topology ()  
565   ;; The first time we set the topology to whatever we have
566   ;; gotten here, which can be rather random.
567   (unless gnus-topic-alist
568     (gnus-topic-init-alist))
569
570   (setq gnus-topology-checked-p t)
571   ;; Go through the topic alist and make sure that all topics
572   ;; are in the topic topology.
573   (let ((topics (gnus-topic-list))
574         (alist gnus-topic-alist)
575         changed)
576     (while alist
577       (unless (member (caar alist) topics)
578         (nconc gnus-topic-topology
579                (list (list (list (caar alist) 'visible))))
580         (setq changed t))
581       (setq alist (cdr alist)))
582     (when changed
583       (gnus-topic-enter-dribble))
584     ;; Conversely, go through the topology and make sure that all
585     ;; topologies have alists.
586     (while topics
587       (unless (assoc (car topics) gnus-topic-alist)
588         (push (list (car topics)) gnus-topic-alist))
589       (pop topics)))
590   ;; Go through all living groups and make sure that
591   ;; they belong to some topic.
592   (let* ((tgroups (apply 'append (mapcar (lambda (entry) (cdr entry))
593                                          gnus-topic-alist)))
594          (entry (assoc (caar gnus-topic-topology) gnus-topic-alist))
595          (newsrc gnus-newsrc-alist)
596          group)
597     (while newsrc
598       (unless (member (setq group (gnus-info-group (pop newsrc))) tgroups)
599         (setcdr entry (cons group (cdr entry))))))
600   ;; Go through all topics and make sure they contain only living groups.
601   (let ((alist gnus-topic-alist)
602         topic)
603     (while (setq topic (pop alist))
604       (while (cdr topic)
605         (if (gnus-gethash (cadr topic) gnus-newsrc-hashtb)
606             (setq topic (cdr topic))
607           (setcdr topic (cddr topic)))))))
608
609 (defun gnus-topic-init-alist ()
610   "Initialize the topic structures."
611   (setq gnus-topic-topology
612         (cons (list "Gnus" 'visible)
613               (mapcar (lambda (topic)
614                         (list (list (car topic) 'visible)))
615                       '(("misc")))))
616   (setq gnus-topic-alist
617         (list (cons "misc"
618                     (mapcar (lambda (info) (gnus-info-group info))
619                             (cdr gnus-newsrc-alist)))
620               (list "Gnus")))
621   (gnus-topic-enter-dribble))
622
623 ;;; Maintenance
624
625 (defun gnus-topic-clean-alist ()
626   "Remove bogus groups from the topic alist."
627   (let ((topic-alist gnus-topic-alist)
628         result topic)
629     (unless gnus-killed-hashtb
630       (gnus-make-hashtable-from-killed))
631     (while (setq topic (pop topic-alist))
632       (let ((topic-name (pop topic))
633             group filtered-topic)
634         (while (setq group (pop topic))
635           (if (and (gnus-gethash group gnus-active-hashtb)
636                    (not (gnus-gethash group gnus-killed-hashtb)))
637               (push group filtered-topic)))
638         (push (cons topic-name (nreverse filtered-topic)) result)))
639     (setq gnus-topic-alist (nreverse result))))
640
641 (defun gnus-topic-change-level (group level oldlevel)
642   "Run when changing levels to enter/remove groups from topics."
643   (save-excursion
644     (set-buffer gnus-group-buffer)
645     (when (and gnus-topic-mode 
646                gnus-topic-alist
647                (not gnus-topic-inhibit-change-level))
648       ;; Remove the group from the topics.
649       (when (and (< oldlevel gnus-level-zombie)
650                  (>= level gnus-level-zombie))
651         (let (alist)
652           (forward-line -1)
653           (when (setq alist (assoc (gnus-current-topic) gnus-topic-alist))
654             (setcdr alist (gnus-delete-first group (cdr alist))))))
655       ;; If the group is subscribed. then we enter it into the topics.
656       (when (and (< level gnus-level-zombie)
657                  (>= oldlevel gnus-level-zombie))
658         (let* ((prev (gnus-group-group-name))
659                (gnus-topic-inhibit-change-level t)
660                (gnus-group-indentation
661                 (make-string 
662                  (* gnus-topic-indent-level
663                     (or (save-excursion
664                           (gnus-topic-goto-topic (gnus-current-topic))
665                           (gnus-group-topic-level)) 0)) ? ))
666                (yanked (list group))
667                alist talist end)
668           ;; Then we enter the yanked groups into the topics they belong
669           ;; to. 
670           (when (setq alist (assoc (save-excursion
671                                      (forward-line -1)
672                                      (or
673                                       (gnus-current-topic)
674                                       (caar gnus-topic-topology)))
675                                    gnus-topic-alist))
676             (setq talist alist)
677             (when (stringp yanked)
678               (setq yanked (list yanked)))
679             (if (not prev)
680                 (nconc alist yanked)
681               (if (not (cdr alist))
682                   (setcdr alist (nconc yanked (cdr alist)))
683                 (while (and (not end) (cdr alist))
684                   (when (equal (cadr alist) prev)
685                     (setcdr alist (nconc yanked (cdr alist)))
686                     (setq end t))
687                   (setq alist (cdr alist)))
688                 (unless end
689                   (nconc talist yanked))))))
690         (gnus-topic-update-topic)))))
691
692 (defun gnus-topic-goto-next-group (group props)
693   "Go to group or the next group after group."
694   (if (null group)
695       (gnus-topic-goto-topic (symbol-name (cadr (memq 'gnus-topic props))))
696     (if (gnus-group-goto-group group)
697         t
698       ;; The group is no longer visible.
699       (let* ((list (assoc (gnus-current-topic) gnus-topic-alist))
700              (after (cdr (member group (cdr list)))))
701         ;; First try to put point on a group after the current one.
702         (while (and after
703                     (not (gnus-group-goto-group (car after))))
704           (setq after (cdr after)))
705         ;; Then try to put point on a group before point.
706         (unless after
707           (setq after (cdr (member group (reverse (cdr list)))))
708           (while (and after 
709                       (not (gnus-group-goto-group (car after))))
710             (setq after (cdr after))))
711         ;; Finally, just put point on the topic.
712         (unless after
713           (gnus-topic-goto-topic (car list))
714           (setq after nil))
715         t))))
716
717 ;;; Topic-active functions
718
719 (defun gnus-topic-grok-active (&optional force)
720   "Parse all active groups and create topic structures for them."
721   ;; First we make sure that we have really read the active file. 
722   (when (or force
723             (not gnus-topic-active-alist))
724     (let (groups)
725       ;; Get a list of all groups available.
726       (mapatoms (lambda (g) (when (symbol-value g)
727                               (push (symbol-name g) groups)))
728                 gnus-active-hashtb)
729       (setq groups (sort groups 'string<))
730       ;; Init the variables.
731       (setq gnus-topic-active-topology (list (list "" 'visible)))
732       (setq gnus-topic-active-alist nil)
733       ;; Descend the top-level hierarchy.
734       (gnus-topic-grok-active-1 gnus-topic-active-topology groups)
735       ;; Set the top-level topic names to something nice.
736       (setcar (car gnus-topic-active-topology) "Gnus active")
737       (setcar (car gnus-topic-active-alist) "Gnus active"))))
738
739 (defun gnus-topic-grok-active-1 (topology groups)
740   (let* ((name (caar topology))
741          (prefix (concat "^" (regexp-quote name)))
742          tgroups ntopology group)
743     (while (and groups
744                 (string-match prefix (setq group (car groups))))
745       (if (not (string-match "\\." group (match-end 0)))
746           ;; There are no further hierarchies here, so we just
747           ;; enter this group into the list belonging to this
748           ;; topic.
749           (push (pop groups) tgroups)
750         ;; New sub-hierarchy, so we add it to the topology.
751         (nconc topology (list (setq ntopology 
752                                     (list (list (substring 
753                                                  group 0 (match-end 0))
754                                                 'invisible)))))
755         ;; Descend the hierarchy.
756         (setq groups (gnus-topic-grok-active-1 ntopology groups))))
757     ;; We remove the trailing "." from the topic name.
758     (setq name
759           (if (string-match "\\.$" name)
760               (substring name 0 (match-beginning 0))
761             name))
762     ;; Add this topic and its groups to the topic alist.
763     (push (cons name (nreverse tgroups)) gnus-topic-active-alist)
764     (setcar (car topology) name)
765     ;; We return the rest of the groups that didn't belong
766     ;; to this topic.
767     groups))
768
769 ;;; Topic mode, commands and keymap.
770
771 (defvar gnus-topic-mode-map nil)
772 (defvar gnus-group-topic-map nil)
773
774 (unless gnus-topic-mode-map
775   (setq gnus-topic-mode-map (make-sparse-keymap))
776
777   ;; Override certain group mode keys.
778   (gnus-define-keys
779    gnus-topic-mode-map
780    "=" gnus-topic-select-group
781    "\r" gnus-topic-select-group
782    " " gnus-topic-read-group
783    "\C-k" gnus-topic-kill-group
784    "\C-y" gnus-topic-yank-group
785    "\M-g" gnus-topic-get-new-news-this-topic
786    "AT" gnus-topic-list-active
787    "Gp" gnus-topic-edit-parameters
788    gnus-mouse-2 gnus-mouse-pick-topic)
789
790   ;; Define a new submap.
791   (gnus-define-keys
792    (gnus-group-topic-map "T" gnus-group-mode-map)
793    "#" gnus-topic-mark-topic
794    "\M-#" gnus-topic-unmark-topic
795    "n" gnus-topic-create-topic
796    "m" gnus-topic-move-group
797    "D" gnus-topic-remove-group
798    "c" gnus-topic-copy-group
799    "h" gnus-topic-hide-topic
800    "s" gnus-topic-show-topic
801    "M" gnus-topic-move-matching
802    "C" gnus-topic-copy-matching
803    "\C-i" gnus-topic-indent
804    [tab] gnus-topic-indent
805    "r" gnus-topic-rename
806    "\177" gnus-topic-delete))
807
808 (defun gnus-topic-make-menu-bar ()
809   (unless (boundp 'gnus-topic-menu)
810     (easy-menu-define
811      gnus-topic-menu gnus-topic-mode-map ""
812      '("Topics"
813        ["Toggle topics" gnus-topic-mode t]
814        ("Groups"
815         ["Copy" gnus-topic-copy-group t]
816         ["Move" gnus-topic-move-group t]
817         ["Remove" gnus-topic-remove-group t]
818         ["Copy matching" gnus-topic-copy-matching t]
819         ["Move matching" gnus-topic-move-matching t])
820        ("Topics"
821         ["Show" gnus-topic-show-topic t]
822         ["Hide" gnus-topic-hide-topic t]
823         ["Delete" gnus-topic-delete t]
824         ["Rename" gnus-topic-rename t]
825         ["Create" gnus-topic-create-topic t]
826         ["Mark" gnus-topic-mark-topic t]
827         ["Indent" gnus-topic-indent t])
828        ["List active" gnus-topic-list-active t]))))
829
830 (defun gnus-topic-mode (&optional arg redisplay)
831   "Minor mode for topicsifying Gnus group buffers."
832   (interactive (list current-prefix-arg t))
833   (when (eq major-mode 'gnus-group-mode)
834     (make-local-variable 'gnus-topic-mode)
835     (setq gnus-topic-mode 
836           (if (null arg) (not gnus-topic-mode)
837             (> (prefix-numeric-value arg) 0)))
838     ;; Infest Gnus with topics.
839     (when gnus-topic-mode
840       (when (and menu-bar-mode
841                  (gnus-visual-p 'topic-menu 'menu))
842         (gnus-topic-make-menu-bar))
843       (setq gnus-topic-line-format-spec 
844             (gnus-parse-format gnus-topic-line-format 
845                                gnus-topic-line-format-alist t))
846       (unless (assq 'gnus-topic-mode minor-mode-alist)
847         (push '(gnus-topic-mode " Topic") minor-mode-alist))
848       (unless (assq 'gnus-topic-mode minor-mode-map-alist)
849         (push (cons 'gnus-topic-mode gnus-topic-mode-map)
850               minor-mode-map-alist))
851       (add-hook 'gnus-summary-exit-hook 'gnus-topic-update-topic)
852       (add-hook 'gnus-group-catchup-group-hook 'gnus-topic-update-topic)
853       (add-hook 'gnus-group-update-group-hook 'gnus-topic-update-topic)
854       (make-local-variable 'gnus-group-prepare-function)
855       (setq gnus-group-prepare-function 'gnus-group-prepare-topics)
856       (make-local-variable 'gnus-group-goto-next-group-function)
857       (setq gnus-group-goto-next-group-function 
858             'gnus-topic-goto-next-group)
859       (setq gnus-group-change-level-function 'gnus-topic-change-level)
860       (setq gnus-goto-missing-group-function 'gnus-topic-goto-missing-group)
861       (make-local-variable 'gnus-group-indentation-function)
862       (setq gnus-group-indentation-function
863             'gnus-topic-group-indentation)
864       (gnus-make-local-hook 'gnus-check-bogus-groups-hook)
865       (add-hook 'gnus-check-bogus-groups-hook 'gnus-topic-clean-alist)
866       (setq gnus-topology-checked-p nil)
867       ;; We check the topology.
868       (when gnus-newsrc-alist
869         (gnus-topic-check-topology))
870       (run-hooks 'gnus-topic-mode-hook))
871     ;; Remove topic infestation.
872     (unless gnus-topic-mode
873       (remove-hook 'gnus-summary-exit-hook 'gnus-topic-update-topic)
874       (remove-hook 'gnus-group-change-level-function 
875                    'gnus-topic-change-level)
876       (remove-hook 'gnus-check-bogus-groups-hook 'gnus-topic-clean-alist)
877       (setq gnus-group-prepare-function 'gnus-group-prepare-flat))
878     (when redisplay
879       (gnus-group-list-groups))))
880     
881 (defun gnus-topic-select-group (&optional all)
882   "Select this newsgroup.
883 No article is selected automatically.
884 If ALL is non-nil, already read articles become readable.
885 If ALL is a number, fetch this number of articles.
886
887 If performed over a topic line, toggle folding the topic."
888   (interactive "P")
889   (if (gnus-group-topic-p)
890       (let ((gnus-group-list-mode 
891              (if all (cons (if (numberp all) all 7) t) gnus-group-list-mode)))
892         (gnus-topic-fold all))
893     (gnus-group-select-group all)))
894
895 (defun gnus-mouse-pick-topic (e)
896   "Select the group or topic under the mouse pointer."
897   (interactive "e")
898   (mouse-set-point e)
899   (gnus-topic-read-group nil))
900
901 (defun gnus-topic-read-group (&optional all no-article group)
902   "Read news in this newsgroup.
903 If the prefix argument ALL is non-nil, already read articles become
904 readable.  IF ALL is a number, fetch this number of articles.  If the
905 optional argument NO-ARTICLE is non-nil, no article will be
906 auto-selected upon group entry.  If GROUP is non-nil, fetch that
907 group.
908
909 If performed over a topic line, toggle folding the topic."
910   (interactive "P")
911   (if (gnus-group-topic-p)
912       (let ((gnus-group-list-mode 
913              (if all (cons (if (numberp all) all 7) t) gnus-group-list-mode)))
914         (gnus-topic-fold all))
915     (gnus-group-read-group all no-article group)))
916
917 (defun gnus-topic-create-topic (topic parent &optional previous full-topic)
918   (interactive 
919    (list
920     (read-string "New topic: ")
921     (gnus-current-topic)))
922   ;; Check whether this topic already exists.
923   (when (gnus-topic-find-topology topic)
924     (error "Topic aleady exists"))
925   (unless parent
926     (setq parent (caar gnus-topic-topology)))
927   (let ((top (cdr (gnus-topic-find-topology parent)))
928         (full-topic (or full-topic `((,topic visible)))))
929     (unless top
930       (error "No such parent topic: %s" parent))
931     (if previous
932         (progn
933           (while (and (cdr top)
934                       (not (equal (caaadr top) previous)))
935             (setq top (cdr top)))
936           (setcdr top (cons full-topic (cdr top))))
937       (nconc top (list full-topic)))
938     (unless (assoc topic gnus-topic-alist)
939       (push (list topic) gnus-topic-alist)))
940   (gnus-topic-enter-dribble)
941   (gnus-group-list-groups)
942   (gnus-topic-goto-topic topic))
943
944 (defun gnus-topic-move-group (n topic &optional copyp)
945   "Move the next N groups to TOPIC.
946 If COPYP, copy the groups instead."
947   (interactive
948    (list current-prefix-arg
949          (completing-read "Move to topic: " gnus-topic-alist nil t)))
950   (let ((groups (gnus-group-process-prefix n))
951         (topicl (assoc topic gnus-topic-alist))
952         entry)
953     (mapcar (lambda (g) 
954               (gnus-group-remove-mark g)
955               (when (and
956                      (setq entry (assoc (gnus-current-topic)
957                                         gnus-topic-alist))
958                      (not copyp))
959                 (setcdr entry (gnus-delete-first g (cdr entry))))
960               (nconc topicl (list g)))
961             groups)
962     (gnus-group-position-point))
963   (gnus-topic-enter-dribble)
964   (gnus-group-list-groups))
965
966 (defun gnus-topic-remove-group ()
967   "Remove the current group from the topic."
968   (interactive)
969   (let ((topicl (assoc (gnus-current-topic) gnus-topic-alist))
970         (group (gnus-group-group-name))
971         (buffer-read-only nil))
972     (when (and topicl group)
973       (gnus-delete-line)
974       (gnus-delete-first group topicl))
975     (gnus-group-position-point)))
976
977 (defun gnus-topic-copy-group (n topic)
978   "Copy the current group to a topic."
979   (interactive
980    (list current-prefix-arg
981          (completing-read "Copy to topic: " gnus-topic-alist nil t)))
982   (gnus-topic-move-group n topic t))
983
984 (defun gnus-topic-kill-group (&optional n discard)
985   "Kill the next N groups."
986   (interactive "P")
987   (if (gnus-group-topic-p)
988       (let ((topic (gnus-group-topic-name)))
989         (gnus-topic-remove-topic nil t)
990         (push (gnus-topic-find-topology topic nil nil gnus-topic-topology)
991               gnus-topic-killed-topics))
992     (gnus-group-kill-group n discard)
993     (gnus-topic-update-topic)))
994   
995 (defun gnus-topic-yank-group (&optional arg)
996   "Yank the last topic."
997   (interactive "p")
998   (if gnus-topic-killed-topics
999       (let ((previous 
1000              (or (gnus-group-topic-name)
1001                  (gnus-topic-next-topic (gnus-current-topic))))
1002             (item (cdr (pop gnus-topic-killed-topics))))
1003         (gnus-topic-create-topic
1004          (caar item) (gnus-topic-parent-topic previous) previous
1005          item)
1006         (gnus-topic-goto-topic (caar item)))
1007     (let* ((prev (gnus-group-group-name))
1008            (gnus-topic-inhibit-change-level t)
1009            (gnus-group-indentation
1010             (make-string 
1011              (* gnus-topic-indent-level
1012                 (or (save-excursion
1013                       (gnus-topic-goto-topic (gnus-current-topic))
1014                       (gnus-group-topic-level)) 0)) ? ))
1015            yanked alist)
1016       ;; We first yank the groups the normal way...
1017       (setq yanked (gnus-group-yank-group arg))
1018       ;; Then we enter the yanked groups into the topics they belong
1019       ;; to. 
1020       (setq alist (assoc (save-excursion
1021                            (forward-line -1)
1022                            (gnus-current-topic))
1023                          gnus-topic-alist))
1024       (when (stringp yanked)
1025         (setq yanked (list yanked)))
1026       (if (not prev)
1027           (nconc alist yanked)
1028         (if (not (cdr alist))
1029             (setcdr alist (nconc yanked (cdr alist)))
1030           (while (cdr alist)
1031             (when (equal (cadr alist) prev)
1032               (setcdr alist (nconc yanked (cdr alist)))
1033               (setq alist nil))
1034             (setq alist (cdr alist))))))
1035     (gnus-topic-update-topic)))
1036
1037 (defun gnus-topic-hide-topic ()
1038   "Hide the current topic."
1039   (interactive)
1040   (when (gnus-current-topic)
1041     (gnus-topic-goto-topic (gnus-current-topic))
1042     (gnus-topic-remove-topic nil nil 'hidden)))
1043
1044 (defun gnus-topic-show-topic ()
1045   "Show the hidden topic."
1046   (interactive)
1047   (when (gnus-group-topic-p)
1048     (gnus-topic-remove-topic t nil 'shown)))
1049
1050 (defun gnus-topic-mark-topic (topic &optional unmark)
1051   "Mark all groups in the topic with the process mark."
1052   (interactive (list (gnus-current-topic)))
1053   (save-excursion
1054     (let ((groups (gnus-topic-find-groups topic 9 t)))
1055       (while groups
1056         (funcall (if unmark 'gnus-group-remove-mark 'gnus-group-set-mark)
1057                  (gnus-info-group (nth 2 (pop groups))))))))
1058
1059 (defun gnus-topic-unmark-topic (topic &optional unmark)
1060   "Remove the process mark from all groups in the topic."
1061   (interactive (list (gnus-current-topic)))
1062   (gnus-topic-mark-topic topic t))
1063
1064 (defun gnus-topic-get-new-news-this-topic (&optional n)
1065   "Check for new news in the current topic."
1066   (interactive "P")
1067   (if (not (gnus-group-topic-p))
1068       (gnus-group-get-new-news-this-group n)
1069     (gnus-topic-mark-topic (gnus-group-topic-name))
1070     (gnus-group-get-new-news-this-group)))
1071
1072 (defun gnus-topic-move-matching (regexp topic &optional copyp)
1073   "Move all groups that match REGEXP to some topic."
1074   (interactive
1075    (let (topic)
1076      (nreverse
1077       (list
1078        (setq topic (completing-read "Move to topic: " gnus-topic-alist nil t))
1079        (read-string (format "Move to %s (regexp): " topic))))))
1080   (gnus-group-mark-regexp regexp)
1081   (gnus-topic-move-group nil topic copyp))
1082
1083 (defun gnus-topic-copy-matching (regexp topic &optional copyp)
1084   "Copy all groups that match REGEXP to some topic."
1085   (interactive
1086    (let (topic)
1087      (nreverse
1088       (list
1089        (setq topic (completing-read "Copy to topic: " gnus-topic-alist nil t))
1090        (read-string (format "Copy to %s (regexp): " topic))))))
1091   (gnus-topic-move-matching regexp topic t))
1092
1093 (defun gnus-topic-delete (topic)
1094   "Delete a topic."
1095   (interactive (list (gnus-group-topic-name)))
1096   (unless topic
1097     (error "No topic to be deleted"))
1098   (let ((entry (assoc topic gnus-topic-alist))
1099         (buffer-read-only nil))
1100     (when (cdr entry)
1101       (error "Topic not empty"))
1102     ;; Delete if visible.
1103     (when (gnus-topic-goto-topic topic)
1104       (gnus-delete-line))
1105     ;; Remove from alist.
1106     (setq gnus-topic-alist (delq entry gnus-topic-alist))
1107     ;; Remove from topology.
1108     (gnus-topic-find-topology topic nil nil 'delete)))
1109
1110 (defun gnus-topic-rename (old-name new-name)
1111   "Rename a topic."
1112   (interactive
1113    (let ((topic (gnus-current-topic)))
1114      (list topic
1115            (read-string (format "Rename %s to: " topic)))))
1116   (let ((top (gnus-topic-find-topology old-name))
1117         (entry (assoc old-name gnus-topic-alist)))
1118     (when top
1119       (setcar (cadr top) new-name))
1120     (when entry 
1121       (setcar entry new-name))
1122     (forward-line -1)
1123     (gnus-group-list-groups)))
1124
1125 (defun gnus-topic-indent (&optional unindent)
1126   "Indent a topic -- make it a sub-topic of the previous topic.
1127 If UNINDENT, remove an indentation."
1128   (interactive "P")
1129   (if unindent
1130       (gnus-topic-unindent)
1131     (let* ((topic (gnus-current-topic))
1132            (parent (gnus-topic-previous-topic topic)))
1133       (unless parent
1134         (error "Nothing to indent %s into" topic))
1135       (when topic
1136         (gnus-topic-goto-topic topic)
1137         (gnus-topic-kill-group)
1138         (gnus-topic-create-topic
1139          topic parent nil (cdr (pop gnus-topic-killed-topics)))
1140         (or (gnus-topic-goto-topic topic)
1141             (gnus-topic-goto-topic parent))))))
1142
1143 (defun gnus-topic-unindent ()
1144   "Unindent a topic."
1145   (interactive)
1146   (let* ((topic (gnus-current-topic))
1147          (parent (gnus-topic-parent-topic topic))
1148          (grandparent (gnus-topic-parent-topic parent)))
1149     (unless grandparent
1150       (error "Nothing to indent %s into" topic))
1151     (when topic
1152       (gnus-topic-goto-topic topic)
1153       (gnus-topic-kill-group)
1154       (gnus-topic-create-topic
1155        topic grandparent (gnus-topic-next-topic parent)
1156        (cdr (pop gnus-topic-killed-topics)))
1157       (gnus-topic-goto-topic topic))))
1158
1159 (defun gnus-topic-list-active (&optional force)
1160   "List all groups that Gnus knows about in a topicsified fashion.
1161 If FORCE, always re-read the active file."
1162   (interactive "P")
1163   (when force
1164     (gnus-get-killed-groups))
1165   (gnus-topic-grok-active force)
1166   (let ((gnus-topic-topology gnus-topic-active-topology)
1167         (gnus-topic-alist gnus-topic-active-alist)
1168         gnus-killed-list gnus-zombie-list)
1169     (gnus-group-list-groups 9 nil 1)))
1170
1171 (defun gnus-topic-edit-parameters (group)
1172   "Edit the group parameters of GROUP.
1173 If performed on a topic, edit the topic parameters instead."
1174   (interactive (list (gnus-group-group-name)))
1175   (if group
1176       (gnus-group-edit-group-parameters group)
1177     (if (not (gnus-group-topic-p))
1178         (error "Nothing to edit on the current line.")
1179       (let ((topic (gnus-group-topic-name)))
1180         (gnus-edit-form
1181          (gnus-topic-parameters topic)
1182          "Editing the topic parameters."
1183          `(lambda (form)
1184             (gnus-topic-set-parameters ,topic form)))))))
1185
1186 (provide 'gnus-topic)
1187
1188 ;;; gnus-topic.el ends here