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