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