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