*** empty log message ***
[gnus] / lisp / gnus-topic.el
index 17f52bc..81e16b6 100644 (file)
@@ -1,5 +1,5 @@
-;;; gnus-topic.el --- a folding group mode for Gnus
-;; Copyright (C) 1995 Free Software Foundation, Inc.
+;;; gnus-topic.el --- a folding minor mode for Gnus group buffers
+;; Copyright (C) 1995,96 Free Software Foundation, Inc.
 
 ;; Author: Ilja Weis <kult@uni-paderborn.de>
 ;;     Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
 (require 'gnus)
 (eval-when-compile (require 'cl))
 
-(defvar gnus-group-topic-face 'bold
-  "*Face used to highlight topic headers.")
+(defvar gnus-topic-mode nil
+  "Minor mode for Gnus group buffers.")
 
-(defvar gnus-group-topics '(("no" "^no" nil) ("misc" "." nil))
-  "*Alist of newsgroup topics.
-This alist has entries of the form
+(defvar gnus-topic-mode-hook nil
+  "Hook run in topic mode buffers.")
 
-   (TOPIC REGEXP SHOW)
+(defvar gnus-topic-line-format "%i[ %(%{%n%}%) -- %a ]%v\n"
+  "Format of topic lines.
+It works along the same lines as a normal formatting string,
+with some simple extensions.
 
-where TOPIC is the name of the topic a group is put in if it matches
-REGEXP.  A group can only be in one topic at a time.
-
-If SHOW is nil, newsgroups will be inserted according to
-`gnus-group-topic-topics-only', otherwise that variable is ignored and
-the groups are always shown if SHOW is true or never if SHOW is a
-number.")
-
-(defvar gnus-topic-names nil
-  "A list of all topic names.")
-
-(defvar gnus-topic-names nil
-  "A list of all topic names.")
+%i  Indentation based on topic level.
+%n  Topic name.
+%v  Nothing if the topic is visible, \"...\" otherwise.
+%g  Number of groups in the topic.
+%a  Number of unread articles in the groups in the topic.
+")
 
 (defvar gnus-group-topic-topics-only nil
   "*If non-nil, only the topics will be shown when typing `l' or `L'.")
@@ -57,9 +52,26 @@ number.")
 (defvar gnus-topic-unique t
   "*If non-nil, each group will only belong to one topic.")
 
+(defvar gnus-topic-hide-subtopics t
+  "*If non-nil, hide subtopics along with groups.")
+
 ;; Internal variables.
 
-(defvar gnus-topics-not-listed nil)
+(defvar gnus-topic-killed-topics nil)
+(defvar gnus-topic-inhibit-change-level nil)
+
+
+(defconst gnus-topic-line-format-alist
+  `((?n name ?s)
+    (?v visible ?s)
+    (?i indentation ?s)
+    (?g number-of-groups ?d)
+    (?a number-of-articles ?d)
+    (?l level ?d)))
+
+(defvar gnus-topic-line-format-spec nil)
+(defvar gnus-topic-active-topology nil)
+(defvar gnus-topic-active-alist nil)
 
 ;; Functions.
 
@@ -67,167 +79,735 @@ number.")
   "The name of the topic on the current line."
   (get-text-property (gnus-point-at-bol) 'gnus-topic))
 
+(defun gnus-group-topic-level ()
+  "The level of the topic on the current line."
+  (get-text-property (gnus-point-at-bol) 'gnus-topic-level))
+
+(defun gnus-topic-init-alist ()
+  "Initialize the topic structures."
+  (setq gnus-topic-topology
+       (cons (list "Gnus" 'visible)
+             (mapcar (lambda (topic)
+                       (list (list (car topic) 'visible)))
+                     '(("misc")))))
+  (setq gnus-topic-alist
+       (list (cons "misc"
+                   (mapcar (lambda (info) (gnus-info-group info))
+                           (cdr gnus-newsrc-alist)))
+             (list "Gnus")))
+  (gnus-topic-enter-dribble))
+
 (defun gnus-group-prepare-topics (level &optional all lowest regexp list-topic)
   "List all newsgroups with unread articles of level LEVEL or lower, and
 use the `gnus-group-topics' to sort the groups.
 If ALL is non-nil, list groups that have no unread articles.
 If LOWEST is non-nil, list all newsgroups of level LOWEST or higher."
   (set-buffer gnus-group-buffer)
+  (setq gnus-topic-indentation "")
   (let ((buffer-read-only nil)
         (lowest (or lowest 1))
        tlist info)
     
-    (or list-topic (erase-buffer))
+    (unless list-topic 
+      (erase-buffer))
     
     ;; List dead groups?
-    (and (>= level gnus-level-zombie) (<= lowest gnus-level-zombie)
-         (gnus-group-prepare-flat-list-dead 
-          (setq gnus-zombie-list (sort gnus-zombie-list 'string<)) 
-         gnus-level-zombie ?Z
-          regexp))
+    (when (and (>= level gnus-level-zombie) (<= lowest gnus-level-zombie))
+      (gnus-group-prepare-flat-list-dead 
+       (setq gnus-zombie-list (sort gnus-zombie-list 'string<)) 
+       gnus-level-zombie ?Z
+       regexp))
     
-    (and (>= level gnus-level-killed) (<= lowest gnus-level-killed)
-         (gnus-group-prepare-flat-list-dead 
-          (setq gnus-killed-list (sort gnus-killed-list 'string<))
-         gnus-level-killed ?K
-          regexp))
+    (when (and (>= level gnus-level-killed) (<= lowest gnus-level-killed))
+      (gnus-group-prepare-flat-list-dead 
+       (setq gnus-killed-list (sort gnus-killed-list 'string<))
+       gnus-level-killed ?K
+       regexp))
     
     ;; Use topics.
-    (if (< lowest gnus-level-zombie)
-        (let ((topics (gnus-topic-find-groups list-topic level all))
-              topic how)
-         (setq gnus-topic-names topics)
-          (while topics
-            (setq topic (car (car topics))
-                 tlist (cdr (car topics))
-                  how (nth 2 (assoc topic gnus-group-topics))
-                  topics (cdr topics))
-
-           ;; Insert the topic.
-           (unless list-topic
-             (add-text-properties 
-              (point)
-              (progn
-                (insert topic "\n")
-                (point))
-              (list 'mouse-face gnus-mouse-face
-                    'face gnus-group-topic-face
-                    'gnus-topic topic)))
-
-           ;; We insert the groups for the topics we want to have. 
-            (if (and (or (and (not how) (not gnus-group-topic-topics-only))
-                        (and how (not (numberp how))))
-                    (not (member topic gnus-topics-not-listed)))
-               (progn
-                 (setq gnus-topics-not-listed
-                       (delete topic gnus-topics-not-listed))
-                 (setq tlist (nreverse tlist))
-                 (while tlist
-                   (setq info (car tlist))
-                   (gnus-group-insert-group-line 
-                    nil (car info) (car (cdr info)) (nth 3 info) 
-                    (car (gnus-gethash (car info) gnus-newsrc-hashtb))
-                    (nth 4 info))
-                   (setq tlist (cdr tlist))))
-             (setq gnus-topics-not-listed
-                   (cons topic gnus-topics-not-listed)))))))
+    (when (< lowest gnus-level-zombie)
+      (if list-topic
+         (let ((top (gnus-topic-find-topology list-topic)))
+           (gnus-topic-prepare-topic (cdr top) (car top) level all))
+       (gnus-topic-prepare-topic gnus-topic-topology 0 level all))))
 
   (gnus-group-set-mode-line)
   (setq gnus-group-list-mode (cons level all))
   (run-hooks 'gnus-group-prepare-hook))
 
-(defun gnus-topic-find-groups (&optional topic level all)
-  "Find all topics and all groups in all topics.
-If TOPIC, just find the groups in that topic."
-  (let ((newsrc (cdr gnus-newsrc-alist))
-       (topics (if topic
-                   (list (list topic))
-                 (mapcar (lambda (e) (list (car e)))
-                         gnus-group-topics)))
-       (topic-alist (if topic (list (assoc topic gnus-group-topics))
-                      gnus-group-topics))
-        info clevel unread group w lowest gtopic)
+(defun gnus-topic-prepare-topic (topic level &optional list-level all)
+  "Insert TOPIC into the group buffer."
+  (let* ((type (pop topic))
+        (entries (gnus-topic-find-groups (car type) list-level all))
+        (visiblep (eq (nth 1 type) 'visible))
+        info entry)
+    ;; Insert the topic line.
+    (gnus-topic-insert-topic-line 
+     (car type) visiblep
+     (not (eq (nth 2 type) 'hidden))
+     level entries)
+    (when visiblep
+      ;; Insert all the groups that belong in this topic.
+      (while entries
+       (setq entry (pop entries)
+             info (nth 2 entry))
+       (gnus-group-insert-group-line 
+        (gnus-info-group info)
+        (gnus-info-level info) (gnus-info-marks info) 
+        (car entry) (gnus-info-method info))))
+    ;; Insert any sub-topics.
+    (when (or visiblep
+             (and (not gnus-topic-hide-subtopics)
+                  (eq (nth 2 type) 'shown)))
+      (while topic
+       (gnus-topic-prepare-topic (pop topic) (1+ level) list-level all)))))
+
+(defun gnus-topic-find-groups (topic &optional level all)
+  "Return entries for all visible groups in TOPIC."
+  (let ((groups (cdr (assoc topic gnus-topic-alist)))
+        info clevel unread group w lowest gtopic params visible-groups entry)
     (setq lowest (or lowest 1))
-    (setq all (or all nil))
     (setq level (or level 7))
     ;; We go through the newsrc to look for matches.
-    (while newsrc
-      (setq info (car newsrc)
-            group (car info)
-            newsrc (cdr newsrc)
-            unread (car (gnus-gethash group gnus-newsrc-hashtb)))
+    (while groups
+      (setq entry (gnus-gethash (pop groups) gnus-newsrc-hashtb)
+           info (nth 2 entry)
+            group (gnus-info-group info)
+           params (gnus-info-params info)
+            unread (car entry))
       (and 
        unread                          ; nil means that the group is dead.
-       (<= (setq clevel (car (cdr info))) level) 
+       (<= (setq clevel (gnus-info-level info)) level) 
        (>= clevel lowest)              ; Is inside the level we want.
        (or all
-          (eq unread t)
+          (and gnus-group-list-inactive-groups
+               (eq unread t))
           (> unread 0)
-          (cdr (assq 'tick (nth 3 info)))) ; Has right readedness.
-       (progn
-        ;; So we find out what topic this group belongs to.  First we
-        ;; check the group parameters.
-        (setq gtopic (cdr (assq 'topic (nth 5 info))))
-        ;; On match, we add it.
-        (and (stringp gtopic) 
-             (or (not topic)
-                 (string= gtopic topic))
-             (if (setq e (assoc gtopic topics))
-                 (setcdr e (cons info (cdr e)))
-               (setq topics (cons (list gtopic info) topics))))
-        ;; We look through the topic alist for further matches, if
-        ;; needed.  
-        (if (or (not gnus-topic-unique) (not (stringp gtopic)))
-            (let ((ts topic-alist))
-              (while ts
-                (if (string-match (nth 1 (car ts)) group)
-                    (progn
-                      (setcdr (setq e (assoc (car (car ts)) topics))
-                              (cons info (cdr e)))
-                      (and gnus-topic-unique (setq ts nil))))
-                (setq ts (cdr ts))))))))
-    topics))
-
-(defun gnus-topic-remove-topic ()
+          (cdr (assq 'tick (gnus-info-marks info))) ; Has right readedness.
+          ;; Check for permanent visibility.
+          (and gnus-permanently-visible-groups
+               (string-match gnus-permanently-visible-groups group))
+          (memq 'visible params)
+          (cdr (assq 'visible params)))
+       ;; Add this group to the list of visible groups.
+       (push entry visible-groups)))
+    (nreverse visible-groups)))
+
+(defun gnus-topic-remove-topic (&optional insert total-remove hide)
   "Remove the current topic."
   (let ((topic (gnus-group-topic-name))
+       (level (gnus-group-topic-level))
+       (beg (progn (beginning-of-line) (point)))
        buffer-read-only)
-    (if (not topic) 
-       ()
-      (setq gnus-topics-not-listed (cons topic gnus-topics-not-listed))
-      (forward-line 1)
-      (delete-region (point) 
-                    (or (next-single-property-change (point) 'gnus-topic)
-                        (point-max))))))
+    (when topic
+      (while (and (zerop (forward-line 1))
+                 (> (or (gnus-group-topic-level) (1+ level)) level)))
+      (delete-region beg (point))
+      (setcar (cdr (car (cdr (gnus-topic-find-topology topic))))
+             (if insert 'visible 'invisible))
+      (when hide
+       (setcdr (cdr (car (cdr (gnus-topic-find-topology topic))))
+               (list hide)))
+      (unless total-remove
+       (gnus-topic-insert-topic topic)))))
 
 (defun gnus-topic-insert-topic (topic)
   "Insert TOPIC."
-  (setq gnus-topics-not-listed (delete topic gnus-topics-not-listed))
   (gnus-group-prepare-topics 
    (car gnus-group-list-mode) (cdr gnus-group-list-mode)
    nil nil topic))
   
-(defun gnus-topic-fold ()
+(defun gnus-topic-fold (&optional insert)
   "Remove/insert the current topic."
   (let ((topic (gnus-group-topic-name))) 
     (when topic
       (save-excursion
-       (if (not (member topic gnus-topics-not-listed))
-           ;; If the topic is visible, we remove it.
-           (gnus-topic-remove-topic) 
-         ;; If not, we insert it.
-         (forward-line 1)
-         (gnus-topic-insert-topic topic))))))
-
-;; Written by "jeff (j.d.) sparkes" <jsparkes@bnr.ca>.
-(defun gnus-group-add-to-topic (n topic)
-  "Add the current group to a topic."
+       (if (not (gnus-group-active-topic-p))
+           (gnus-topic-remove-topic
+            (or insert (not (gnus-topic-visible-p))))
+         (let ((gnus-topic-topology gnus-topic-active-topology)
+               (gnus-topic-alist gnus-topic-active-alist))
+           (gnus-topic-remove-topic
+            (or insert (not (gnus-topic-visible-p))))))))))
+
+(defun gnus-group-topic-p ()
+  "Return non-nil if the current line is a topic."
+  (get-text-property (gnus-point-at-bol) 'gnus-topic))
+
+(defun gnus-topic-visible-p ()
+  "Return non-nil if the current topic is visible."
+  (get-text-property (gnus-point-at-bol) 'gnus-topic-visible))
+
+(defun gnus-topic-insert-topic-line (name visiblep shownp level entries)
+  (let* ((visible (if (and visiblep shownp) "" "..."))
+        (indentation (make-string (* 2 level) ? ))
+        (number-of-articles (gnus-topic-articles-in-topic entries))
+        (number-of-groups (length entries))
+        (active-topic (eq gnus-topic-alist gnus-topic-active-alist)))
+    (setq gnus-topic-indentation "")
+    (beginning-of-line)
+    ;; Insert the text.
+    (add-text-properties 
+     (point)
+     (prog1 (1+ (point)) 
+       (eval gnus-topic-line-format-spec)
+       (gnus-group-remove-excess-properties))
+     (list 'gnus-topic name
+          'gnus-topic-level level
+          'gnus-active active-topic
+          'gnus-topic-visible visiblep))))
+
+(defun gnus-topic-previous-topic (topic)
+  "Return the previous topic on the same level as TOPIC."
+  (let ((top (cdr (cdr (gnus-topic-find-topology
+                       (gnus-topic-parent-topic topic))))))
+    (unless (equal topic (car (car (car top))))
+      (while (and top (not (equal (car (car (car (cdr top)))) topic)))
+       (setq top (cdr top)))
+      (car (car (car top))))))
+
+(defun gnus-topic-parent-topic (topic &optional topology)
+  "Return the parent of TOPIC."
+  (unless topology
+    (setq topology gnus-topic-topology))
+  (let ((parent (car (pop topology)))
+       result found)
+    (while (and topology
+               (not (setq found (equal (car (car (car topology))) topic)))
+               (not (setq result (gnus-topic-parent-topic topic 
+                                                          (car topology)))))
+      (setq topology (cdr topology)))
+    (or result (and found parent))))
+
+(defun gnus-topic-find-topology (topic &optional topology level remove)
+  "Return the topology of TOPIC."
+  (unless topology
+    (setq topology gnus-topic-topology)
+    (setq level 0))
+  (let ((top topology)
+       result)
+    (if (equal (car (car topology)) topic)
+       (progn
+         (when remove
+           (delq topology remove))
+         (cons level topology))
+      (setq topology (cdr topology))
+      (while (and topology
+                 (not (setq result (gnus-topic-find-topology
+                                    topic (car topology) (1+ level)
+                                    (and remove top)))))
+       (setq topology (cdr topology)))
+      result)))
+
+(defun gnus-topic-check-topology ()  
+  ;; The first time we set the topology to whatever we have
+  ;; gotten here, which can be rather random.
+  (unless gnus-topic-alist
+    (gnus-topic-init-alist))
+
+  (let ((topics (gnus-topic-list))
+       (alist gnus-topic-alist)
+       changed)
+    (while alist
+      (unless (member (car (car alist)) topics)
+       (nconc gnus-topic-topology
+              (list (list (list (car (car alist)) 'visible))))
+       (setq changed t))
+      (setq alist (cdr alist)))
+    (when changed
+      (gnus-topic-enter-dribble)))
+  (let* ((tgroups (apply 'append (mapcar (lambda (entry) (cdr entry))
+                                        gnus-topic-alist)))
+        (entry (assoc (caar gnus-topic-topology) gnus-topic-alist))
+        (newsrc gnus-newsrc-alist)
+        group)
+    (while newsrc
+      (unless (member (setq group (gnus-info-group (pop newsrc))) tgroups)
+       (setcdr entry (cons group (cdr entry)))))))
+
+(defvar gnus-tmp-topics nil)
+(defun gnus-topic-list (&optional topology)
+  (unless topology
+    (setq topology gnus-topic-topology 
+         gnus-tmp-topics nil))
+  (push (car (car topology)) gnus-tmp-topics)
+  (mapcar 'gnus-topic-list (cdr topology))
+  gnus-tmp-topics)
+
+(defun gnus-topic-enter-dribble ()
+  (gnus-dribble-enter
+   (format "(setq gnus-topic-topology '%S)" gnus-topic-topology)))
+
+(defun gnus-topic-articles-in-topic (entries)
+  (let ((total 0)
+       number)
+    (while entries
+      (when (numberp (setq number (car (pop entries))))
+       (incf total number)))
+    total))
+
+(defun gnus-group-parent-topic ()
+  "Return the topic the current group belongs in."
+  (let ((group (gnus-group-group-name)))
+    (if group
+       (gnus-group-topic group)
+      (gnus-group-topic-name))))
+
+(defun gnus-group-topic (group)
+  "Return the topic GROUP is a member of."
+  (let ((alist gnus-topic-alist)
+       out)
+    (while alist
+      (when (member group (cdr (car alist)))
+       (setq out (car (car alist))
+             alist nil))
+      (setq alist (cdr alist)))
+    out))
+
+(defun gnus-topic-goto-topic (topic)
+  (goto-char (point-min))
+  (while (and (not (equal topic (gnus-group-topic-name)))
+             (zerop (forward-line 1))))
+  (gnus-group-topic-name))
+  
+(defun gnus-topic-update-topic ()
+  (when (and (eq major-mode 'gnus-group-mode)
+            gnus-topic-mode)
+    (let ((group (gnus-group-group-name)))
+      (gnus-topic-goto-topic (gnus-group-parent-topic))
+      (gnus-topic-update-topic-line)
+      (gnus-group-goto-group group)
+      (gnus-group-position-point))))
+
+(defun gnus-topic-update-topic-line ()
+  (let* ((buffer-read-only nil)
+        (topic (gnus-group-topic-name))
+        (entry (gnus-topic-find-topology topic))
+        (level (car entry))
+        (type (nth 1 entry))
+        (entries (gnus-topic-find-groups (car type)))
+        (visiblep (eq (nth 1 type) 'visible)))
+    ;; Insert the topic line.
+    (when topic
+      (gnus-delete-line)
+      (gnus-topic-insert-topic-line 
+       (car type) visiblep
+       (not (eq (nth 2 type) 'hidden)) level entries))))
+
+(defun gnus-topic-grok-active (&optional force)
+  "Parse all active groups and create topic structures for them."
+  ;; First we make sure that we have really read the active file. 
+  (when (or force
+           (not gnus-topic-active-alist))
+    (when (or force
+             (not gnus-have-read-active-file))
+      (let ((gnus-read-active-file t))
+       (gnus-read-active-file)))
+    (let (topology groups alist)
+      ;; Get a list of all groups available.
+      (mapatoms (lambda (g) (when (symbol-value g)
+                             (push (symbol-name g) groups)))
+               gnus-active-hashtb)
+      (setq groups (sort groups 'string<))
+      ;; Init the variables.
+      (setq gnus-topic-active-topology '(("" visible)))
+      (setq gnus-topic-active-alist nil)
+      ;; Descend the top-level hierarchy.
+      (gnus-topic-grok-active-1 gnus-topic-active-topology groups)
+      ;; Set the top-level topic names to something nice.
+      (setcar (car gnus-topic-active-topology) "Gnus active")
+      (setcar (car gnus-topic-active-alist) "Gnus active"))))
+
+(defun gnus-topic-grok-active-1 (topology groups)
+  (let* ((name (caar topology))
+        (prefix (concat "^" (regexp-quote name)))
+        tgroups nprefix ntopology group)
+    (while (and groups
+               (string-match prefix (setq group (car groups))))
+      (if (not (string-match "\\." group (match-end 0)))
+         ;; There are no further hierarchies here, so we just
+         ;; enter this group into the list belonging to this
+         ;; topic.
+         (push (pop groups) tgroups)
+       ;; New sub-hierarchy, so we add it to the topology.
+       (nconc topology (list (setq ntopology 
+                                   (list (list (substring 
+                                                group 0 (match-end 0))
+                                               'invisible)))))
+       ;; Descend the hierarchy.
+       (setq groups (gnus-topic-grok-active-1 ntopology groups))))
+    ;; We remove the trailing "." from the topic name.
+    (setq name
+         (if (string-match "\\.$" name)
+             (substring name 0 (match-beginning 0))
+           name))
+    ;; Add this topic and its groups to the topic alist.
+    (push (cons name (nreverse tgroups)) gnus-topic-active-alist)
+    (setcar (car topology) name)
+    ;; We return the rest of the groups that didn't belong
+    ;; to this topic.
+    groups))
+
+(defun gnus-group-active-topic-p ()
+  "Return whether the current active comes from the active topics."
+  (save-excursion
+    (beginning-of-line)
+    (get-text-property (point) 'gnus-active)))
+
+;;; Topic mode, commands and keymap.
+
+(defvar gnus-topic-mode-map nil)
+(defvar gnus-group-topic-map nil)
+
+(unless gnus-topic-mode-map
+  (setq gnus-topic-mode-map (make-sparse-keymap))
+
+  ;; Override certain group mode keys.
+  (gnus-define-keys
+   gnus-topic-mode-map
+   "=" gnus-topic-select-group
+   "\r" gnus-topic-select-group
+   " " gnus-topic-read-group
+   "\C-k" gnus-topic-kill-group
+   "\C-y" gnus-topic-yank-group
+   "\M-g" gnus-topic-get-new-news-this-topic
+   "\C-i" gnus-topic-indent
+   "AT" gnus-topic-list-active
+   gnus-mouse-2 gnus-mouse-pick-topic)
+
+  ;; Define a new submap.
+  (gnus-define-keys
+   (gnus-group-topic-map "T" gnus-group-mode-map)
+   "#" gnus-topic-mark-topic
+   "n" gnus-topic-create-topic
+   "m" gnus-topic-move-group
+   "c" gnus-topic-copy-group
+   "h" gnus-topic-hide-topic
+   "s" gnus-topic-show-topic
+   "M" gnus-topic-move-matching
+   "C" gnus-topic-copy-matching
+   "r" gnus-topic-rename
+   "\177" gnus-topic-delete))
+
+(defun gnus-topic-make-menu-bar ()
+  (unless (boundp 'gnus-topic-menu)
+    (easy-menu-define
+     gnus-topic-menu gnus-topic-mode-map ""
+     '("Topics"
+       ["Toggle topics" gnus-topic-mode t]
+       ("Groups"
+       ["Copy" gnus-topic-copy-group t]
+       ["Move" gnus-topic-move-group t]
+       ["Copy matching" gnus-topic-copy-matching t]
+       ["Move matching" gnus-topic-move-matching t])
+       ("Topics"
+       ["Show" gnus-topic-show-topic t]
+       ["Hide" gnus-topic-hide-topic t]
+       ["Delete" gnus-topic-delete t]
+       ["Rename" gnus-topic-rename t]
+       ["Create" gnus-topic-create-topic t]
+       ["Mark" gnus-topic-mark-topic t]
+       ["Indent" gnus-topic-indent t])
+       ["List active" gnus-topic-list-active t]))))
+
+
+(defun gnus-topic-mode (&optional arg redisplay)
+  "Minor mode for topicsifying Gnus group buffers."
+  (interactive (list current-prefix-arg t))
+  (when (eq major-mode 'gnus-group-mode)
+    (make-local-variable 'gnus-topic-mode)
+    (setq gnus-topic-mode 
+         (if (null arg) (not gnus-topic-mode)
+           (> (prefix-numeric-value arg) 0)))
+    (make-local-variable 'gnus-group-prepare-function)
+    ;; Infest Gnus with topics.
+    (when gnus-topic-mode
+      (when (and menu-bar-mode
+                (gnus-visual-p 'topic-menu 'menu))
+       (gnus-topic-make-menu-bar))
+      (setq gnus-topic-line-format-spec 
+           (gnus-parse-format gnus-topic-line-format 
+                              gnus-topic-line-format-alist t))
+      (unless (assq 'gnus-topic-mode minor-mode-alist)
+       (push '(gnus-topic-mode " Topic") minor-mode-alist))
+      (unless (assq 'gnus-topic-mode minor-mode-map-alist)
+       (push (cons 'gnus-topic-mode gnus-topic-mode-map)
+             minor-mode-map-alist))
+      (add-hook 'gnus-summary-exit-hook 'gnus-topic-update-topic)
+      (setq gnus-group-prepare-function 'gnus-group-prepare-topics)
+      (setq gnus-group-change-level-function 'gnus-topic-change-level)
+      (run-hooks 'gnus-topic-mode-hook)
+      ;; We check the topology.
+      (gnus-topic-check-topology))
+    ;; Remove topic infestation.
+    (unless gnus-topic-mode
+      (remove-hook 'gnus-summary-exit-hook 'gnus-topic-update-topic)
+      (remove-hook 'gnus-group-change-level-function 
+                  'gnus-topic-change-level)
+      (setq gnus-group-prepare-function 'gnus-group-prepare-flat))
+    (when redisplay
+      (gnus-group-list-groups))))
+    
+(defun gnus-topic-select-group (&optional all)
+  "Select this newsgroup.
+No article is selected automatically.
+If ALL is non-nil, already read articles become readable.
+If ALL is a number, fetch this number of articles."
+  (interactive "P")
+  (if (gnus-group-topic-p)
+      (let ((gnus-group-list-mode 
+            (if all (cons (if (numberp all) all 7) t) gnus-group-list-mode)))
+       (gnus-topic-fold all))
+    (gnus-group-select-group all)))
+
+(defun gnus-mouse-pick-topic (e)
+  "Select the group or topic under the mouse pointer."
+  (interactive "e")
+  (mouse-set-point e)
+  (gnus-topic-read-group nil))
+
+(defun gnus-topic-read-group (&optional all no-article group)
+  "Read news in this newsgroup.
+If the prefix argument ALL is non-nil, already read articles become
+readable.  IF ALL is a number, fetch this number of articles.  If the
+optional argument NO-ARTICLE is non-nil, no article will be
+auto-selected upon group entry.  If GROUP is non-nil, fetch that
+group."
+  (interactive "P")
+  (if (gnus-group-topic-p)
+      (let ((gnus-group-list-mode 
+            (if all (cons (if (numberp all) all 7) t) gnus-group-list-mode)))
+       (gnus-topic-fold all))
+    (gnus-group-read-group all no-article group)))
+
+(defun gnus-topic-create-topic (topic parent &optional previous)
+  (interactive 
+   (list
+    (read-string "Create topic: ")
+    (completing-read "Parent topic: " gnus-topic-alist nil t)))
+  ;; Check whether this topic already exists.
+  (when (gnus-topic-find-topology topic)
+    (error "Topic aleady exists"))
+  (unless parent
+    (setq parent (car (car gnus-topic-topology))))
+  (let ((top (cdr (gnus-topic-find-topology parent))))
+    (unless top
+      (error "No such parent topic: %s" parent))
+    (if previous
+       (progn
+         (while (and (cdr top)
+                     (not (equal (car (car (car (cdr top)))) previous)))
+           (setq top (cdr top)))
+         (setcdr top (cons (list (list topic 'visible)) (cdr top))))
+      (nconc top (list (list (list topic 'visible)))))
+    (unless (assoc topic gnus-topic-alist)
+      (push (list topic) gnus-topic-alist)))
+  (gnus-topic-enter-dribble)
+  (gnus-group-list-groups))
+
+(defun gnus-topic-move-group (n topic &optional copyp)
+  "Move the current group to a topic."
   (interactive
    (list current-prefix-arg
-        (completing-read "Add to topic: " gnus-topic-names)))
-  (let ((groups (gnus-group-process-prefix n)))
-    (mapcar (lambda (g) (gnus-group-add-parameter g (cons 'topic topic)))
+        (completing-read "Move to topic: " gnus-topic-alist nil t)))
+  (let ((groups (gnus-group-process-prefix n))
+       (topicl (assoc topic gnus-topic-alist))
+       entry)
+    (unless topicl
+      (error "No such topic: %s" topic))
+    (mapcar (lambda (g) 
+             (gnus-group-remove-mark g)
+             (when (and
+                    (setq entry (assoc (gnus-group-topic g) gnus-topic-alist))
+                    (not copyp))
+               (setcdr entry (delete g (cdr entry))))
+             (nconc topicl (list g)))
            groups)
-    (gnus-group-position-point)))
+    (gnus-group-position-point))
+  (gnus-topic-enter-dribble)
+  (gnus-group-list-groups))
+
+(defun gnus-topic-copy-group (n topic)
+  "Copy the current group to a topic."
+  (interactive
+   (list current-prefix-arg
+        (completing-read "Copy to topic: " gnus-topic-alist nil t)))
+  (gnus-topic-move-group n topic t))
+
+(defun gnus-topic-change-level (group level oldlevel)
+  "Run when changing levels to enter/remove groups from topics."
+  (when (and gnus-topic-mode 
+            (not gnus-topic-inhibit-change-level))
+    ;; Remove the group from the topics.
+    (when (and (< oldlevel gnus-level-zombie)
+              (>= level gnus-level-zombie))
+      (let (alist)
+       (when (setq alist (assoc (gnus-group-topic group) gnus-topic-alist))
+         (setcdr alist (delete group (cdr alist))))))
+    ;; If the group is subscribed. then we enter it into the topics.
+    (when (and (< level gnus-level-zombie)
+              (>= oldlevel gnus-level-zombie))
+      (let ((entry (assoc (caar gnus-topic-topology) gnus-topic-alist)))
+       (setcdr entry (cons group (cdr entry)))))))
+
+(defun gnus-topic-kill-group (&optional n discard)
+  "Kill the next N groups."
+  (interactive "P")
+  (if (gnus-group-topic-p)
+      (let ((topic (gnus-group-topic-name)))
+       (gnus-topic-remove-topic nil t)
+       (push (gnus-topic-find-topology topic nil nil gnus-topic-topology)
+             gnus-topic-killed-topics))
+    (gnus-group-kill-group n discard)))
+  
+(defun gnus-topic-yank-group (&optional arg)
+  "Yank the last topic."
+  (interactive "p")
+  (if gnus-topic-killed-topics
+      (let ((previous (gnus-group-parent-topic))
+           (item (nth 1 (pop gnus-topic-killed-topics))))
+       (gnus-topic-create-topic
+        (car item) (gnus-topic-parent-topic previous) previous))
+    (let* ((prev (gnus-group-group-name))
+          (gnus-topic-inhibit-change-level t)
+          yanked group alist)
+      ;; We first yank the groups the normal way...
+      (setq yanked (gnus-group-yank-group arg))
+      ;; Then we enter the yanked groups into the topics they belong
+      ;; to. 
+      (setq alist (assoc (save-excursion
+                          (forward-line -1)
+                          (gnus-group-parent-topic))
+                        gnus-topic-alist))
+      (when (stringp yanked)
+       (setq yanked (list yanked)))
+      (if (not prev)
+         (nconc alist yanked)
+       (if (not (cdr alist))
+           (setcdr alist (nconc yanked (cdr alist)))
+         (while (cdr alist)
+           (when (equal (car (cdr alist)) prev)
+             (setcdr alist (nconc yanked (cdr alist)))
+             (setq alist nil))
+           (setq alist (cdr alist))))))))
+
+(defun gnus-topic-hide-topic ()
+  "Hide all subtopics under the current topic."
+  (interactive)
+  (when (gnus-group-topic-p)
+    (gnus-topic-remove-topic nil nil 'hidden)))
+
+(defun gnus-topic-show-topic ()
+  "Show the hidden topic."
+  (interactive)
+  (when (gnus-group-topic-p)
+    (gnus-topic-remove-topic t nil 'shown)))
+
+(defun gnus-topic-mark-topic (topic)
+  "Mark all groups in the topic with the process mark."
+  (interactive (list (gnus-group-parent-topic)))
+  (let ((groups (cdr (gnus-topic-find-groups topic))))
+    (while groups
+      (gnus-group-set-mark (gnus-info-group (nth 2 (pop groups)))))))
+
+(defun gnus-topic-get-new-news-this-topic (&optional n)
+  "Check for new news in the current topic."
+  (interactive "P")
+  (if (not (gnus-group-topic-p))
+      (gnus-group-get-new-news-this-group n)
+    (gnus-topic-mark-topic (gnus-group-topic-name))
+    (gnus-group-get-new-news-this-group)))
+
+(defun gnus-topic-move-matching (regexp topic &optional copyp)
+  "Move all groups that match REGEXP to some topic."
+  (interactive
+   (let (topic)
+     (nreverse
+      (list
+       (setq topic (completing-read "Move to topic: " gnus-topic-alist nil t))
+       (read-string (format "Move to %s (regexp): " topic))))))
+  (gnus-group-mark-regexp regexp)
+  (gnus-topic-move-group nil topic copyp))
+
+(defun gnus-topic-copy-matching (regexp topic &optional copyp)
+  "Copy all groups that match REGEXP to some topic."
+  (interactive
+   (let (topic)
+     (nreverse
+      (list
+       (setq topic (completing-read "Copy to topic: " gnus-topic-alist nil t))
+       (read-string (format "Copy to %s (regexp): " topic))))))
+  (gnus-topic-move-matching regexp topic t))
+
+(defun gnus-topic-delete (topic)
+  "Delete a topic."
+  (interactive (list (gnus-group-topic-name)))
+  (unless topic
+    (error "No topic to be deleted"))
+  (let ((entry (assoc topic gnus-topic-alist))
+       (buffer-read-only nil))
+    (when (cdr entry)
+      (error "Topic not empty"))
+    ;; Delete if visible.
+    (when (gnus-topic-goto-topic topic)
+      (gnus-delete-line))
+    ;; Remove from alist.
+    (setq gnus-topic-alist (delq entry gnus-topic-alist))
+    ;; Remove from topology.
+    (gnus-topic-find-topology topic nil nil 'delete)))
+
+(defun gnus-topic-rename (old-name new-name)
+  "Rename a topic."
+  (interactive
+   (let (topic)
+     (list
+      (setq topic (completing-read "Rename topic: " gnus-topic-alist nil t))
+      (read-string (format "Rename %s to: " topic)))))
+  (let ((top (gnus-topic-find-topology old-name))
+       (entry (assoc old-name gnus-topic-alist)))
+    (when top
+      (setcar (car (cdr top)) new-name))
+    (when entry 
+      (setcar entry new-name))
+    (gnus-group-list-groups)))
+
+(defun gnus-topic-indent (&optional unindent)
+  "Indent a topic -- make it a sub-topic of the previous topic.
+If UNINDENT, remove an indentation."
+  (interactive "P")
+  (if unindent
+      (gnus-topic-unindent)
+    (let* ((topic (gnus-group-parent-topic))
+          (parent (gnus-topic-previous-topic topic)))
+      (unless parent
+       (error "Nothing to indent %s into" topic))
+      (when topic
+       (gnus-topic-goto-topic topic)
+       (gnus-topic-kill-group)
+       (gnus-topic-create-topic topic parent)))))
+
+(defun gnus-topic-unindent ()
+  "Unindent a topic."
+  (interactive)
+  (let* ((topic (gnus-group-parent-topic))
+        (parent (gnus-topic-parent-topic topic))
+        (grandparent (gnus-topic-parent-topic parent)))
+    (unless grandparent
+      (error "Nothing to indent %s into" topic))
+    (when topic
+      (gnus-topic-goto-topic topic)
+      (gnus-topic-kill-group)
+      (gnus-topic-create-topic topic grandparent))))
+
+(defun gnus-topic-list-active (&optional force)
+  "List all groups that Gnus knows about in a topicsified fashion.
+If FORCE, always re-read the active file."
+  (interactive "P")
+  (gnus-topic-grok-active)
+  (let ((gnus-topic-topology gnus-topic-active-topology)
+       (gnus-topic-alist gnus-topic-active-alist))
+    (gnus-group-list-groups 9 nil 1)))
+
+(provide 'gnus-topic)
 
 ;;; gnus-topic.el ends here