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