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