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